前言
本节咱们回归下EF Core基础,来说述EF Core中究竟是如何映射的,废话少说,咱们开始。html
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 => p.Name); b.HasMany(p => p.Posts) .WithOne(p => p.Blog) .HasForeignKey(p => p.BlogId); } }
而Post则为以下:app
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语句。以下:ide
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建立了索引,以下:post
CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);
由上知,对于一对多关系中的外键,EF Core会默认建立其索引,固然这里的索引确定是非惟一非汇集索引,汇集索引为其主键。咱们经过数据库上就能够看到,以下:ui
此时即便咱们不配置指定外键为BlogId一样也没毛病,以下:spa
b.HasMany(m => m.Posts).WithOne(o => o.Blog);
由于上述咱们已经明确写出了BlogId,可是EF Core依然能够为其指定BlogId为外键,如今咱们反过来想,要是咱们将Post中的BlogId删除,一样进行上述映射是否好使呢,通过实际验证确实是能够的,以下:code
别着急下结论,咱们再来看一种状况,如今咱们进行以下配置并除去Post中的BlogId仍是否依然好使呢?htm
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 => k.UserId); } }
最后监控获得以下语句:
看到没,为用户Id配置了惟一约束:
CONSTRAINT [AK_Book_UserId] UNIQUE ([UserId])
因此咱们得出结论:经过可选键咱们能够建立惟一约束来除主键以外惟一标识行。
主体键映射(Principal Key)
若是咱们想要一个外键引用一个属性而不是主键,此时咱们能够经过主体键映射来进行配置,此时配置主体键映射背后实际上自动将其设置为一个可选键。这个就不用咱们多讲了。
好了到此为止咱们讲完了键映射,接下来咱们再来说述属性映射:
属性映射
对于C#中string类型若咱们不进行配置,那么在数据库中将默认设置为NVARCHAR而且长度为MAX且是为可空,以下:
若咱们须要设置其长度且为非空,此时须要进行以下配置:
b.Property(p => p.Name).IsRequired().HasMaxLength(50);
经过HaxMaxLength方法来指定最大长度,经过IsRequired方法来指定为非空。可是此时问题来了,数据库类型对于string有VARCHAR、CHAR、NCAHR类型,那么咱们应当如何映射呢?好比对于VARCHAR类型,在EF Core中对于数据库列类型咱们能够经过 HasColumnType 方法来进行映射,那么假设对于数据库类型为VARCHAR长度为50且为非空,咱们是否能够进行以下映射呢?
b.Property(p => p.Name) .IsRequired() .HasColumnType("VARCHAR") .HasMaxLength(50);
经过上述迁移出错,咱们修改为以下才正确:
b.Property(p => p.Name) .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 => p.Name) .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 => p.Name) .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中的映射以及一些稍微注意的地方,恰好今天父亲节,在此祝愿天下父母健康长寿,咱们下节再会!