EF Core的实体类配置

作为ORM框架,EF Core要完成实体类对象和数据库中数据关系的映射,也就是实体类与数据库表的映射,以及实体类的属性与数据库表的列映射。

1. 约定大于配置

为了减少配置工作量,EF Core采用了”约定大于配置”的设计原则,也就是说EF Core会默认按照约定根据实体类以及DbContext的定义来实现和数据库表的映射配置,除非用户显式地指定了配置规则。

EF Core中的默认约定规则有很多,我们并不需要专门去学习、记忆,只要使用默认约定规则即可,在默认约定规则不满足要求的情况下,我们再去显式地指定规则。

下面只列出几个主要的约定规则:

  1. 数据库表名采用上下文类中对应的DbSet的属性名。
  2. 数据库表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型兼容的类型。
  3. 数据库表列的可空性取决于对应实体类属性的可空性。
  4. 名字为Id的属性为主键,如果主键为short、int或者long类型,则主键默认采用自动增长类型的列。
2. Data Annotation

优点:简单 缺点:耦合

Data Annotation(数据注释)指的是可以使用.NET提供的Attribute对实体类、属性等进行标注的方式来实现实体类配置。

比如通过[Table(“T_Books”)],我们可以把实体类对应的表名配置为T_Books;

通过[Required],我们可以把属性对应的数据库表列配置为”不可为空”;

通过[MaxLength(20)],我们可以把属性对应的数据库表配置为”最大长度为20″。

Data Annotation配置
C#
[Table("T_Books")]
public class Book
{
    /// <summary>
    /// 主键
    /// </summary>
    [MaxLength(50)]
    [Required]
    public long Id { get; set; }
    /// <summary>
    /// 标题
    /// </summary>
    public string? Title { get; set; }
    /// <summary>
    /// 发布日期
    /// </summary>
    public DateTime PubTime { get; set; }
    /// <summary>
    /// 单价
    /// </summary>
    public double Price {  get; set; }
    /// <summary>
    /// 作者名字
    /// </summary>
    [MaxLength(50)]
    [Required]
    public string? AuthorName { get; set; }

}
3. Fluent API

优点:解耦 缺点:复杂

除了可以用Data Annotation对实体类进行配置之外,.NET Core中还提供了Fluent API的方式对实体类进行配置。

看起来Data Annotation的用法很简单,因为只要在实体类上添加Attribute就可以了,我们不需要再写单独的配置类,但是Fluent API属于官方的推荐用法,主要有以下两点原因。

  1. Fluent API能够更好地进行职责分离。实体类只负责进行抽象的描述,不涉及与数据库相关的细节,所有和数据库相关的细节被放到配置类中,这样我们能更方便地进行大型项目的管理。
  2. Fluent API的功能更强大。Fluent API几乎能实现Data Annotation的所有功能,而Data Annotation则不支持Fluent API的一些功能

Data Annotation和Fluent API是可以一起使用的。

如果同样的内容用这两种方式都配置,那么Fluent API的优先级高于Data Annotation。

基于此,在开发人员社区中有两种实体类的配置方案。

  1. 混合方案:优先使用Data Annotation,因为Data Annotation的使用更简单。在DataAnnotation无法实现的地方,再使用Fluent API进行配置
  2. 单一方案:只使用Fluent API

选择混合方案的开发人员不仅是因为考虑到了Data Annotation的使用更简单,而且考虑到了实体类上添加的[MaxLength(50)]、[Required]等Attribute可以被ASP.NET Core中的验证框架等复用。

但是个人不赞同这种做法。

因为基于分层的设计原则,我们不建议直接把实体类对象传递到视图层。

C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
    modelBuilder.Entity<Book>().Ignore(b=>b.AuthorName);
    modelBuilder.Entity<Book>().Property(n => n.Price).HasColumnName("APrice");
}
4. Fluent API基本配置
4.1 视图与实体类映射

可以用下面的代码把blogsView这个数据库中的视图和Blog实体类进行映射

C#
modelBuilder.Entity<Blog>().ToView("blogsView")
4.2 排除属性映射

默认情况下,一个实体类的所有属性都会映射到数据库表中,如果想让EF Core忽略一个属性,就可以用Ignore配置。

C#
 modelBuilder.Entity<Book>().Ignore(b=>b.AuthorName);
4.3 数据库表列名

数据库表中的列名默认和属性名一样,我们可以使用HasColumnName方法配置一个不同的列名。

C#
modelBuilder.Entity<Book>().Property(n => n.Price).HasColumnName("APrice");
4.4 列数据类型

EF Core默认会根据实体类的属性类型、最大长度等确定字段的数据类型,我们可以使用HasColumnType为列指定数据类型。

C#
modelBuilder.Entity<Book>().Property(k => k.AuthorName).HasColumnType("varchar(200)");
4.5 主键

EF Core默认把名字为Id或者”实体类型+Id”的属性作为主键,我们可以用HasKey配置其他属性作为主键。

C#
modelBuilder.Entity<Book>().HasKey(a=>a.Price);

为了保持项目命名的统一以及代码的简洁,这里建议开发人员采用默认的Id作为主键。

EF Core中也支持用多个属性作为复合主键,不过由于复合主键问题很多,不符合如今的软件设计的主流做法,因此不对其进行介绍。

4.6 索引

EF Core中可以用HasIndex方法配置索引。

C#
modelBuilder.Entity<Book>().HasIndex(b=>b.Id);

EF Core也支持多个属性组成的复合索引,只要给HasIndex方法传递由一个或多个属性的名字组成的匿名类对象即可。

C#
 modelBuilder.Entity<Book>().HasIndex(b=> new { b.Id,b.Price});

默认情况下,EF Core中定义的索引不是唯一索引,我们可以用IsUnique方法把索引配置为唯一索引。

我们还可以用IsClustered方法把索引设置为聚集索引。

4.7 重载的方法

在使用Fluent API 的时候还有一点需要注意,Fluent API中的很多方法都有多个重载方法。

比如HasIndex就有HasIndex(params string[] propertyNames)和HasIndex(Expression<Func<TEntity,object>>indexExpression)等重载方法,因此想要把Price属性对应的列定义为索引下面两种方法都可以

C#
modelBuilder.Entity<Book>().HasIndex("Price");
modelBuilder.Entity<Book>().HasIndex(b=>b.Price);

同样地,用来获取实体类属性的Property方法也有多个重载方法,因此想要把Price属性对应的数据库表列的名字定位为No,我们可以用下面两种方法

C#
 modelBuilder.Entity<Book>().Property(b => b.Price).HasColumnName("No");
 modelBuilder.Entity<Book>().Property("Price").HasColumnName("No");

推荐使用lambda表达式的写法,因为这样可以利用C#的强类型检查机制,如果属性名字被写错了,编译器会报错。

如果用””字符的写法,我们的拼写错误是没有那么容易被发现的。

5. 主键类型的选择并不简单

在数据库设计中,对于主键类型来讲,有自动增长的long类型和GUID类型两种常见的方案。

5.1 普通自增

自增long类型的使用非常简单,所有主流数据库系统都内置了对自增列的支持,新插入的数据会由数据库自动赋予一个新增的、不重复的主键值。

自增long类型占用磁盘空间小,可读性强,但是自增long类型的主键在数据库迁移以及分布式系统(如分库分表、数据库集群)中使用起来比较麻烦,而且在高并发插入的时候性能比较差。

由于自增列的值一般都是由数据库生成的,因此无法提前获得新增数据行的主键值,我们需要把数据保存到数据库之后才能获得主键的值。

EF Core会在把数据保存到数据库之后,把自增主键的值自动赋值给主键属性

C#
  using BookDbContext bookDbContext = new BookDbContext();

  var b1 = new Book { AuthorName = "IHCI", Title = "KUAC" };

  bookDbContext.Books.Add(b1);
  await Console.Out.WriteLineAsync($"保存前,Id = {b1.Id}");
  await bookDbContext.SaveChangesAsync();
  await Console.Out.WriteLineAsync($"保存后,Id = {b1.Id}");
运行结果:

可以看到,在SaveChangesAsync保存数据到数据库之前,Id属性的值是默认值0,在保存之后Id属性的值就是新增数据行的主键值了。

5.2 Guid算法

Guid算法使用网卡的MAC地址,时间戳等信息生成一个全球唯一的ID。

由于Guid的全球唯一性,它适用于分布式系统,在进行多数据库数据合并的时候很方便,因此我们也可以用Guid类型作为主键。

值得注意的是,由于Guid算法生成的值是不连续的(即使是SQL Server中NewSequentialId函数生成的Guid也不能根本解决这个问题),因此我们在使用Guid类型作为主键的时候,不能把主键设置为聚集索引。

因为聚集索引是按照顺序保存主键的,在插入Guid类型主键的时候,它将会导致新插入的每条数据都要经历查找合适插入位置的过程,在数据量大的时候将会导致非常糟糕的数据插入性能。

在SQL Server中,可以设置主键为非聚集索引,但是在MySQL中,如果我们使用InnoDB引擎,那么主键是强制使用聚集索引的。

因此,在SQL Server中,如果我们使用Guid类型(也就是uniqueidentifier类型)作为主键,一定不能把主键设置为聚集索引;

在MySQL中,如果使用InnoDB引擎,并且数据插入频繁,一定不要用Guid类型作为主键,如果确实需要用Guid类型作为主键的话,我们只能把这个主键字段作为逻辑主键,而不是作为物理主键;

使用其他数据库管理系统的时候,也请先查阅在对应的数据库管理系统中,是否可以把主键设置为非聚集索引。

5.3 自增+Guid算法

目前,还有一种主键使用策略是把自增主键和Guid结合起来使用,也就是表有两个主键(注意不是复合主键),用自增列作为物理主键,而用Guid作为逻辑主键。

物理主键是在进行表结构设计的时候把自增列设置为主键,而从表结构上我们是看不出来Guid列是主键的,但是在和其他表关联及和外部系统通信的时候(比如前端显示数据的标识的时候),我们都使用Guid列。

这样不仅保证了性能,利用了Guid的优点,而且减少了主键自增导致主键值可被预测带来的安全性问题。

比如,如果网站用自增主键作为参数来展示某个新闻页面的话,网页地址可能是https://www.ptpress.com.cn/News?id=8,这样恶意访问者就可以递增遍历ID,轻松地把网站所有的新闻页面访问到,而如果用Guid作为查询参数的话,网页地址可能是http://www.ptpress.com.cn/News?id=cafe3e46-w6a4-asd1587qwead1,由于Guid的值很难预测,因此恶意访问者遍历所有页面的难度就会大很多。

5.4 Hi/Lo算法

对于普通自增列来讲,每次获取新ID的时候都要锁定自增资源,因此在并发插入数据频繁的情况下,使用普通自增列的数据插入效率相对来讲比较低。

EF Core支持使用Hi/Lo算法来优化自增列的性能。

Hi/Lo算法生成的主键值由两部分组成:高位(Hi)和低位(Lo)。

高位由数据库生成,两个高位之间相隔若干个值;

由程序在本地生成低位,低位的值在本地自增生成。

比如,数据库的两个高位之间相隔10,程序向数据库请求获得一个高位置50。

程序在本地获取主键的时候,会首先获得Hi=50,再加上本地的Lo=0,因此主键值为50;

程序再获取主键的时候,会继续使用之前获得的Hi=50,再加上本地的低位自增,Lo=1,因此主键值为51,以此类推。

当Lo=9之后,再获取主键值,程序发现Hi=50的低位值已经用完了,因此就再向数据库请求一个新的高位值,数据库也许再返回一个Hi=80(因此也许Hi=60和Hi=70已经被其他服务器获取了),然后加上本地的Lo=0,最终获取主键值80,以此类推。

Hi/Lo算法的高位由服务器生成,因此保证了不同进程或者集群中不同服务器获取的高位值不会重复,而本地进程计算的低位则可以保证在本地高效率地生成主键值。

因此,如果普通自增列的性能无法满足项目要求的话,可以考虑Hi/Lo算法,EF Core的SQL Server数据库提供者内置了对Hi/Lo算法的支持,具体用法请参考官方文档。

但是需要注意的是,Hi/Lo算法不是EF Core的标准,如果你使用的不是SQL Server数据库,则需要检查对应数据库的EF Core数据库提供程序是否提供了对Hi/Lo算法的支持。

订阅评论
提醒
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
滚动至顶部