EntityFrameworkCore映射关系详解

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

EntityFrameworkCore映射关系详解
前⾔
Hello,开始回归开始每周更新⼀到两篇博客,本节我们回归下EF Core基础,来讲述EF Core中到底是如何映射的,废话少说,我们开始。

One-Many Relationship(⼀对多关系)
⾸先我们从最简单的⼀对多关系说起,我们给出需要映射的两个类,⼀个是Blog,另外⼀个则是Post,如下:
public class Blog
{
public int Id { get; set; }
public int Count { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public IEnumerable<Post> Posts { get; set; }
}
public class Post
{
public virtual int Id { get; set; }
public virtual string Title { get; set; }
public virtual string Content { get; set; }
public virtual int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
此时我们从Blog来看,⼀个Blog下对应多个Post,⽽⼀个Post对应只属于⼀个Blog,此时配置关系如下:
public class BlogMap : EntityMappingConfiguration<Blog>
{
public override void Map(EntityTypeBuilder<Blog> b)
{
b.ToTable("Blog");
b.HasKey(k => k.Id);
b.Property(p => p.Count);
b.Property(p => p.Url);
b.Property(p => );
b.HasMany(p => p.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId);
}
}
⽽Post则为如下:
public class PostMap : EntityMappingConfiguration<Post>
{
public override void Map(EntityTypeBuilder<Post> b)
{
b.ToTable("Post");
b.HasKey(k => k.Id);
b.Property(p => p.Title);
b.Property(p => p.Content);
}
}
此时我们利⽤SqlProfiler监控⽣成的SQL语句。

如下:
CREATE TABLE [Blog] (
[Id] int NOT NULL IDENTITY,
[Count] int NOT NULL,
[Name] nvarchar(max),
[Url] nvarchar(max),
CONSTRAINT [PK_Blog] PRIMARY KEY ([Id])
);
CREATE TABLE [Post] (
[Id] int NOT NULL IDENTITY,
[BlogId] int NOT NULL,
[Content] nvarchar(max),
[Title] nvarchar(max),
CONSTRAINT [PK_Post] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([Id]) ON DELETE CASCADE
);
此时我们能够很明确的看到对于Post表上的BlogId建⽴外键BlogId,也就是对应的Blog表上的主键即Id,同时后⾯给出了DELETE CASADE 即进⾏级联删除的标识,也就是说当删除了Blog上的数据,那么此时Post表上对应的数据也会进⾏相应的删除。

同时在⽣成SQL语句时,还对Post上的BlogId创建了索引,如下:
CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);
由上知,对于⼀对多关系中的外键,EF Core会默认创建其索引,当然这⾥的索引肯定是⾮唯⼀⾮聚集索引,聚集索引为其主键。

我们通过数据库上就可以看到,如下:
此时即使我们不配置指定外键为BlogId同样也没⽑病,如下:
b.HasMany(m => m.Posts).WithOne(o => o.Blog);
因为上述我们已经明确写出了BlogId,但是EF Core依然可以为其指定BlogId为外键,现在我们反过来想,要是我们将Post中的BlogId删除,同样进⾏上述映射是否好使呢,经过实际验证确实是可以的,如下:
别着急下结论,我们再来看⼀种情况,现在我们进⾏如下配置并除去Post中的BlogId还是否依然好使呢?
b.HasMany(m => m.Posts);
经过临床认证,也是好使的,能够正确表达我们想要的效果并⾃动添加了外键BlogId列,所以到这⾥我们可以为⼀对多关系下个结论:
⼀对多关系结论
在⼀对多关系中,我们可以通过映射明确指定外键列,也可以不指定,因为EF Core内部会查找是否已经指定其外键列有则直接⽤指定的,没有则⾃动⽣成⼀个外键列,列名为外键列所在的类名+Id。

同时对于⼀对多关系我们可以直接只使⽤HasMany⽅法来配置映射⽽不需要再配置HasOne或者WithOne,上述皆是从正向⾓度去配置映射,因为易于理解,当然反之亦然。

One-One RelationShip (⼀对⼀关系)
对于⼀对⼀关系和多对多关系稍微复杂⼀点,我们来各个击破,我们通过举例⽐如⼀个产品只属于⼀个分类,⽽⼀个分类下只有⼀个产品,如下:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Category Category { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
}
此时我们来进⾏⼀下⼀对⼀关系映射从产品⾓度出发:
public class ProductMap : EntityMappingConfiguration<Product>
{
public override void Map(EntityTypeBuilder<Product> b)
{
b.ToTable("Product");
b.HasKey(k => k.Id);
b.HasOne(o => o.Category).WithOne(o => o.Product);
}
}
此时我们通过 dotnet ef migrations add Initial 初始化就已经出现如下错误:
⼤概意思为未明确Product和Category谁是依赖项,未明确指定导致出现上述错误。

⽽上述对于⼀对多关系则不会出现如此错误,仔细分析不难发现⼀对多已经明确谁是主体,⽽对于⼀对⼀关系⼆者为⼀⼀对应关系,所以EF Core⽆法判断其主体,所以必须我们⼿动去指定。

此时我们若进⾏如下指定你会发现没有lambda表达式提⽰:
b.HasOne(o => o.Category)
.WithOne(o => o.Product)
.HasForeignKey(k=>k.)
还是因为主体关系的原因,我们还是必须指定泛型参数才可以。

如下所⽰:
b.HasOne(o => o.Category)
.WithOne(o => o.Product)
.HasForeignKey<Category>(k => k.ProductId);
此时在Category上创建ProductId外键,同时会对ProductId创建如下的唯⼀⾮聚集索引:
CREATE UNIQUE INDEX [IX_Category_ProductId] ON [Category] ([ProductId]);
Many-Many RelationShip (多对多关系)
多对多关系在EF Core之前版本有直接使⽤的⽅法如HasMany-WithMany,但是在EF Core中则不再提供对应的⽅法,想想多对多关系还是可以通过⼀对多可以得到,⽐如⼀个产品属于多个分类,⽽⼀个分类对应多个产品,典型的多对多关系,但是通过我们的描述则完全可以通过⼀对多关系⽽映射得到,下⾯我们⼀起来看看:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<ProductCategory> ProductCategorys { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public int ProductId { get; set; }
public IEnumerable<ProductCategory> ProductCategorys { get; set; }
}
public class ProductCategory
{
public int ProductId { get; set; }
public Product Product { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
上述我们将给出第三个关联类即ProductCategory,将Product(产品类)和Category(分类类)关联到ProductCategory类,最终我们通过ProductCategory来进⾏映射,如下:
public class ProductCategoryMap : EntityMappingConfiguration<ProductCategory>
{
public override void Map(EntityTypeBuilder<ProductCategory> b)
{
b.ToTable("ProductCategory");
b.HasKey(k => k.Id);
b.HasOne(p => p.Product)
.WithMany(p => p.ProductCategorys)
.HasForeignKey(k => k.ProductId);
b.HasOne(p => p.Category)
.WithMany(p => p.ProductCategorys)
.HasForeignKey(k => k.CategoryId);
}
}
好了到了这⾥为⽌,关于三种映射关系我们介绍完了,是不是就此结束了,远远不是,下⾯我们再来其他属性映射。

键映射
关于键映射中的外键映射上述已经讨论过,下⾯我们来讲讲其他类型键的映射。

备⽤键/可选键映射(HasAlternateKey)
备⽤键/可选键可以为⼀个实体类配置除主键之外的唯⼀标识,⽐如在登录中⽤户名可以作为⽤户的唯⼀标识除了主键标识外,这个时候我们可以为UserName配置可选键,打个⽐⽅这样⼀个场景:⼀个⽤户只能购买⼀本书,在Book表中配置⼀个主键和⽤户Id(例⼦虽然不太恰当却能很好描述可选键的使⽤场景)
public class Book
{
public int Id { get; set; }
public string UserId { get; set; }
}
下⾯我们通过可选键来配置⽤户Id的映射
public class BookMap : EntityMappingConfiguration<Book>
{
public override void Map(EntityTypeBuilder<Book> b)
{
b.ToTable("Book");
b.HasKey(k => k.Id);
b.HasAlternateKey(k => erId);
}
}
最后监控得到如下语句:
看到没,为⽤户Id配置了唯⼀约束:
CONSTRAINT [AK_Book_UserId] UNIQUE ([UserId])
所以我们得出结论:通过可选键我们可以创建唯⼀约束来除主键之外唯⼀标识⾏。

主体键映射(Principal Key)
如果我们想要⼀个外键引⽤⼀个属性⽽不是主键,此时我们可以通过主体键映射来进⾏配置,此时配置主体键映射背后实际上⾃动将其设置为⼀个可选键。

这个就不⽤我们多讲了。

好了到此为⽌我们讲完了键映射,接下来我们再来讲述属性映射:
属性映射
对于C#中string类型若我们不进⾏配置,那么在数据库中将默认设置为NVARCHAR并且长度为MAX且是为可空,如下:
若我们需要设置其长度且为⾮空,此时需要进⾏如下配置:
b.Property(p => ).IsRequired().HasMaxLength(50);
通过HaxMaxLength⽅法来指定最⼤长度,通过IsRequired⽅法来指定为⾮空。

但是此时问题来了,数据库类型对于string有VARCHAR、CHAR、NCAHR类型,那么我们应当如何映射呢?⽐如对于VARCHAR类型,在EF Core中对于数据库列类型我们可以通
过 HasColumnType ⽅法来进⾏映射,那么假设对于数据库类型为VARCHAR长度为50且为⾮空,我们是否可以进⾏如下映射呢?
b.Property(p => )
.IsRequired()
.HasColumnType("VARCHAR")
.HasMaxLength(50);
通过上述迁移出错,我们修改成如下才正确:
b.Property(p => )
.IsRequired()
.HasColumnType("VARCHAR(50)");
解决⼀个,⼜来⼀个,那么对于枚举类型我们⼜该进⾏如何映射呢,枚举对应数据库中的类型为TINYINT,我们进⾏如下设置:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Type Type { get; set; }
public IEnumerable<ProductCategory> ProductCategorys { get; set; }
}
public enum Type
{
[Description("普通")]
General = 0,
[Description("保险")]
Insurance = 1
}
public class ProductMap : EntityMappingConfiguration<Product>
{
public override void Map(EntityTypeBuilder<Product> b)
{
b.ToTable("Product");
b.HasKey(k => k.Id);
b.Property(p => p.Type)
.IsRequired()
.HasColumnType("TINYINT");
}
}
此时则对应⽣成我们想要的类型:
CREATE TABLE [Product] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max),
[Type] TINYINT NOT NULL,
CONSTRAINT [PK_Product] PRIMARY KEY ([Id])
【注意】:此时将其映射成枚举没⽑病上述已经演⽰,但是当我们获取数据时将TINYINT转换成枚举时将出现如下错误:
说到底TINYINT对应C#中的byte类最后尝试将其转换为int才会导致转换失败,所以在定义枚举时记得将其继承⾃byte,如下才好使:
public enum Type : byte
{
[Description("普通")]
General = 0,
[Description("保险")]
Insurance = 1
}
讲完如上映射后,我们再来讲讲默认值映射。

当我们敲默认映射会发现有两个,⼀个是HasDefaultValue,⼀个是HasDefaultValueSql,我们⼀起来看看到底如何⽤:
我们在Product类中添加Count字段:
public int Count { get; set; }
b.Property(p => p.Count).HasDefaultValue(0);
如上是对于int类型如上设置,如果是枚举类型呢,我们来试试:
b.Property(p => p.Type)
.IsRequired()
.HasColumnType("TINYINT").HasDefaultValue(0);
此时迁移将出现如下错误:
也就是说⽆法将枚举值设置成int类型,此时我们应该利⽤HasDefaultValueSql来映射:
b.Property(p => p.Type)
.IsRequired()
.HasColumnType("TINYINT").HasDefaultValueSql("0");
对于默认值映射总结起来就⼀句话:对于C#中的类型和数据库类型⼀致的话⽤HasDefaultValue,否则请⽤HasDefaluValueSql。

【注意】:对于字段类型映射有⼀个奇葩特例,对于⽇期类型DateTime,在数据库中也存在其对应的类型datetime,但是如果我们不⼿动指定类型会默认映射成更精确的⽇期类型即datetime2(7)。

我们在Product类中添加创建时间列,如下:
public DateTime CreatedTime { get; set; }
此时我们不指定其映射类型,此时我们看到在数据库中的类型为datetime2(7)
当然以上映射也没什么问题,但是对于⼤部分对于⽇期类型都是映射成datetime且给定默认时间为当前时间,所以此时需要⼿动进⾏配置,如下:
b.Property(p => p.CreatedTime)
.HasColumnType("DATETIME")
.HasDefaultValueSql("GETDATE()");
说完默认值需要注意的问题,我们再来讲讲计算列的映射,在EF Core中对于计算列映射,在之前版本为ForSqlServerHasComputedColumnSql,⽬前是HasComputedColumnSql。

例如如下这是计算列:
b.Property(p => )
.IsRequired()
.HasComputedColumnSql("((N'Cnblogs'+CONVERT([CHAR](8),[CreatedTime],(112)))+RIGHT(REPLICATE(N'0',(6))+CONVERT([NVARCHAR],[Id],(0)),(6)))");
其中还有关于列名⾃定义的⽅法(HasColumnName),主键是否⾃动⽣成(ValueGeneratedOnAdd)等⽅法以及⾏版本(IsRowVersion)和并发Token(IsConcurrencyToken)。

还有设置索引的⽅法HasIndex
这⾥有⼀个疑问对于string默认设置是为NVARCHAR,其就是unicode,不知为何还有⼀个IsUnicode⽅法,它不也是设置为NVARCHAR的吗,这是什么情况?求解,当我们同时设置IsUnicode⽅法和列类型为VARCHAR时,则还是会⽣成NVARCHAR,可见映射成NVARCHAR 优先级⽐VARCHAR⾼,如下
b.Property(p => )
.IsRequired().IsUnicode()
.HasColumnType("VARCHAR(21)")
.HasComputedColumnSql("((N'Cnblogs'+CONVERT([CHAR](8),[CreatedTime],(112)))+RIGHT(REPLICATE(N'0',(6))+CONVERT([NVARCHAR],[Id],(0)),(6)))");总结
本⽂⼤概就稍微讲解了EF Core中的映射以及⼀些稍微注意的地⽅,刚好今天⽗亲节,在此祝愿天下⽗母健康长寿,我们下节再会!。

相关文档
最新文档