01-EF Core笔记之创建模型
使用EF Core的第一步是创建数据模型,模型建的好,下班走的早。EF Core本身已经设置了一系列约定来帮我们快速的创建模型,例如表名、主键字段等,毕竟约定大于配置嘛。如果你想改变默认值,很简单,EF Core提供了Fluent API或Data Annotations两种方式允许我们定制数据模型。
Fluent API 与 Data Annotations
FluentAPI方式和Data Annotations方式,FluentAPI是通过代码语句配置的,Data Annotations是通过特性标注配置的,FluentAPI的方式更加灵活,实现的功能也更多。优先级为:FluentAPI>Data Annotations>Conventions。
数据标注方式比较简单,在类或字段上添加特性标注即可,对实体类型有一定的入侵。
FluentAPI方式通过在OnModelCreating
方法中添加代码逻辑来完成,也可以通过实现IEntityTypeConfiguration<T>
类来完成,方式灵活,更能更加强大。
OnModelCreating
方式:
modelBuilder.Entity<Role>()
.Property(m => m.RoleName)
.IsRequired();
IEntityTypeConfiguration<T>
方式:
先定义IEntityTypeConfiguration<T>
的实现:
public class BookConfigration : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.Name)
.HasMaxLength(100)
.IsRequired();
}
}
然后再OnModelCreating
中添加调用:
//加载单个Configuration
modelBuilder.ApplyConfiguration(new BookConfigration());
//加载程序集中所有Configuration
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
主键、备用键
主键与数据库概念相一致,表示作为数据行的唯一标识;备用键是与主键相对应的一个概念,备用键字段的值可以唯一标识一条数据,它对应数据库的唯一约束。
数据标识方式只能配置主键,使用Key
特性,备用键只能通过FluentAPI进行配置。
FluentAPI方式配置的代码如下:
modelBuilder.Entity<Car>()
.HasKey(c=>c.Id) //主键
.HasAlternateKey(c => c.LicensePlate); //备用键
备用键可以是组合键,通过FluentAPI配置如下:
modelBuilder.Entity<Car>()
.HasAlternateKey(c => new { c.State, c.LicensePlate }); //组合备用键
必填和选填
映射到数据库的必填和可空,在约定情况下,CLR中可为null的属性将被映射为数据库可空字段,不能为null的属性映射为数据库的必填字段。注意:如果CLR中属性不能为null,则无论如何配置都将为必填。
也就是说,如果能为null,则默认都是可空字段,因此在配置时,只需要配置是否为必填即可。
数据标注方式使用Required
特性进行标注。
FluentAPI方式代码如下:
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
最大长度
最大长度设置了数据库字段的长度,针对string类型、byte[]类型有效,默认情况下,EF将控制权交给数据库提供程序来决定。
数据标注方式使用MaxLength(length)
特性进行标注
FluentAPI方式代码如下:
builder.Property(c => c.Name)
.HasMaxLength(100)
.IsRequired();
排除/包含属性或类型
默认情况下,如果你的类型中包含一个字段,那么EF Core都会将它映射到数据库中,导航属性亦是如此。如果不想映射到数据库,需要进行配置。
数据标注方式,使用NotMapped
特性进行标注;
FluentAPI方式使用Ignore
方法,代码如下:
//忽略类型
modelBuilder.Ignore<BlogMetadata>();
//忽略属性
modelBuilder.Entity<Blog>()
.Ignore(b => b.LoadedFromDatabase);
如果一个属性或类型不在实体中,但是又想包含在数据库映射中时,我们只能通过Fluent API进行配置:
//包含类型
modelBuilder.Entity<AuditEntry>();
//包含属性,又叫做阴影属性,它会被映射到数据库中
modelBuilder.Entity<Blog>()
.Property<DateTime>("LastUpdated");
阴影属性
阴影属性指的是在实体中未定义的属性,而在EF Core中模型中为该实体类型定义的属性,这些类型只能通过变更跟踪器进行维护。
阴影属性的定义:
modelBuilder.Entity<Blog>().Property<DateTime>("LastUpdated");
为阴影属性赋值:
context.Entry(myBlog).Property("LastUpdated").CurrentValue = DateTime.Now;
查询时使用阴影属性:
var blogs = context.Blogs
.OrderBy(b => EF.Property<DateTime>(b, "LastUpdated"));
索引
索引是用来提高查询效率的,在EF Core中,索引的定义仅支持FluentAPI方式。
FluentAPI方式代码:
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url);
可以配合唯一约束创建索引:
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.IsUnique();
EF支持复合索引:
modelBuilder.Entity<Person>()
.HasIndex(p => new { p.FirstName, p.LastName });
并发控制
EF Core支持乐观的并发控制,何谓乐观的并发控制呢?原理大致是数据库中每行数据包含一个并发令牌字段,对改行数据的更新都会出发令牌的改变,在发生并行更新时,系统会判断令牌是否匹配,如果不匹配则认为数据已发生变更,此时会抛出异常,造成更新失败。使用乐观的并发控制可提高数据库性能。
按照约定,EF Core不会设置任何并发控制的令牌字段,但是我们可以通过Fluent API或数据标注进行配置。
数据标注使用ConcurrencyCheck
特性标注。除此之外,将数据库字段标记为Timestamp
,则会被认为是RowVersion,也能起到并发控制的功能。
public class Blog
{
public int BlogId { get; set; }
[ConcurrencyCheck]
public string Url { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
FluentAPI 方式代码如下:
//并发控制令牌
modelBuilder.Entity<Person>()
.Property(p => p.LastName)
.IsConcurrencyToken();
//行版本号
modelBuilder.Entity<Blog>()
.Property(p => p.Timestamp)
.IsRowVersion();
实体之间的关系
实体之间的关系,可以参照数据库设计的关系来理解。EF是实体框架,它的实体会映射到关系型数据库中。所以通过关系型数据库的表之间的关系更容易理解实体的关系。
在数据库中,数据表之间的关系可以分为一对一、一对多、多对多三种,在实体之间同样有这三种关系,但是EF Core仅支持一对一、一对多关系,如果要实现多对多关系,则需要通过关系实体进行关联。
一对一的关系
以下面的实体关系为例:
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public BlogImage BlogImage { get; set; }
}
public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }
public int BlogForeignKey { get; set; }
public Blog Blog { get; set; }
}
每一个Blog对应一个BlogImage,通过Blog可以加载到对应的BlogImage对象,对应的数据库配置如下:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasOne(p => p.BlogImage)
.WithOne(i => i.Blog)
.HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}
一对多的关系
以下面的实体对象为例:
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
每个Blog对应多个Post,而每个Post对应一个Blog,对应的数据库配置如下:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.IsRequired();
}
多对多的关系
多对多的关系需要我们定义一个关系表来完成。例如下面的实体对象:
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class PostTag
{
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
Blog和Tag是多对多的关系,显然无论在Blog或Tag中定义外键都不合适,此时就需要一张关系表来进行关联,这张表就是BlogTag表。对应的关系配置如下:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostTag>()
.HasKey(pt => new { pt.PostId, pt.TagId });
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId);
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId);
}
生成的值
这个功能我没有试验成功。按照官方文档,定义如下实体:
public class Book
{
[Key]
public Guid Id { get; set; }
[MaxLength(100)]
public string Name { get; set; }
public decimal Price { get; set; }
public DateTime CreateTime { get; set; }
}
然后定义DateTime值生成器:
public class DateTimeGenerator : ValueGenerator<DateTime>
{
public override bool GeneratesTemporaryValues => true;
public override DateTime Next(EntityEntry entry) => DateTime.Now;
}
最后在FluentAPI中进行配置:
builder.Property(c => c.CreateTime)
.HasValueGenerator<DateTimeGenerator>()
.ValueGeneratedOnAddOrUpdate();
按照我的理解应该可以在添加和更新时设置CreateTime的值,并自动保存到数据库,但是值仅在Context中生成,无法保存到数据库中。或许是我理解的不对,后续再进行研究。
继承
关于继承关系如何在数据库中呈现,目前有三种常见的模式:
- TPH(table-per-hierarchy):一张表存放基类和子类的所有列,使用discriminator列区分类型,目前EF Core仅支持该模式
- TPT(table-per-type ):基类和子类不在同一个表中,子类对应的表中仅包含基类表的主键和基类扩展的字段,目前EF Core不支持该模式
- TPC(table-per-concrete-type):基类和子类不在同一个表中,子类中包含基类的所有字段,目前EF Core不支持该模式
EF Core仅支持TPH模式,基类和子类数据将存储在同一个表中。当发现有继承关系时,EF Core会自动维护一个名为Discriminator
的阴影属性,我们可以设置该字段的属性:
modelBuilder.Entity<Blog>()
.Property("Discriminator")
.HasMaxLength(200);
EF Core允许我们通过FluentAPI的方式自定义鉴别器的列名和每个类对应的值:
modelBuilder.Entity<Blog>()
.HasDiscriminator<string>("blog_type")
.HasValue<Blog>("blog_base")
.HasValue<RssBlog>("blog_rss");
查询类型
查询类型很有用,EF Core不会对它进行跟踪,也不允许新增、修改和删除操作,但是在映射到视图、查询对象、Sql语句查询、只读库的表等情况下用到。
例如创建视图:
db.Database.ExecuteSqlCommand(
@"CREATE VIEW View_BlogPostCounts AS
SELECT b.Name, Count(p.PostId) as PostCount
FROM Blogs b
JOIN Posts p on p.BlogId = b.BlogId
GROUP BY b.Name");
对应的查询视图:
public class BlogPostsCount
{
public string BlogName { get; set; }
public int PostCount { get; set; }
}
使用FluentAPI配置查询视图:
modelBuilder
.Query<BlogPostsCount>().ToView("View_BlogPostCounts")
.Property(v => v.BlogName).HasColumnName("Name");
值转换
值转换允许在写入或读取数据时,将数据进行转换(既可以是同类型转换,例如字符串加密解密,也可以是不同类型转换,例如枚举转换为int或string等)。
这里介绍两个概念
- ModelClrType:模型实体的类型
- ProviderClrType:数据库提供程序支持的类型
举个例子,string类型,对应数据库提供程序也是string类型,而枚举类型,对数据库提供程序来说没有与它对应的类型,则需要进行转换,至于如何转换、转换成什么类型,则有值转换器(Value Converter)进行处理。
值转换器包含两个Func表达式,用以提供ModelClrType和ProviderClrType的互相转换,例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}
该示例代码将的值转化器提供了枚举类型到字符串的互转。这里只是为了演示,真实场景中,EF Core已经提供了枚举到字符串的转换器,我们只需要直接使用即可。
除了使用Func表达式,我们还可以构造值转换器实例,例如:
var converter = new ValueConverter<EquineBeast, string>(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(converter);
EF Core已经内置了常用的值转换器,例如字符串和枚举的转换器,我们可以直接使用:
var converter = new EnumToStringConverter<EquineBeast>();
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(converter);
所有内置的值转换器都是无状态(stateless)的,所以只需要实例化一次,并在多个模型中进行使用。
值转换器还有另外一个用法,即无需实例化转换器,只需要告诉EF Core需要使用的转换器类型即可,例如:
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>();
值转换器的一些限制:
- null值无法进行转换
- 到目前位置还不支持一个字段到多列的转换
- 会影响构造查询参数,如果造成了影响将会生成警告日志
实体构造函数
EF Core支持实体具有有参的构造函数,默认情况下,EF Core使用无参构造函数来实例化实体对象,如果发现实体类型具有有参的构造函数,则优先使用有参的构造函数。
使用有参构造函数需要注意:
- 参数名应与属性的名字、类型相匹配
- 如果参数中不具有所有字段,则在调用构造函数完成后,对未包含字段进行赋值
- 使用懒加载时,构造函数需要能够被代理类访问到,因此需要构造函数为public或protected
- 暂不支持在构造函数中使用导航属性
使用构造函数时,比较好玩的是支持依赖注入,我们可以在构造函数中注入DbContext
、IEntityType
、ILazyLoader
、Action<object, string>
这几个类型。
以上便是常用的构建模型的知识点,更多内容在用到时再进行学习。