使用 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> 这几个类型.
以上便是常用的构建模型的知识点, 更多内容在用到时再进行学习.
来源: http://www.bubuko.com/infodetail-3122987.html