作为ORM框架,EF Core要完成实体类对象和数据库中数据关系的映射,也就是实体类与数据库表的映射,以及实体类的属性与数据库表的列映射。
1. 约定大于配置
为了减少配置工作量,EF Core采用了”约定大于配置”的设计原则,也就是说EF Core会默认按照约定根据实体类以及DbContext的定义来实现和数据库表的映射配置,除非用户显式地指定了配置规则。
EF Core中的默认约定规则有很多,我们并不需要专门去学习、记忆,只要使用默认约定规则即可,在默认约定规则不满足要求的情况下,我们再去显式地指定规则。
下面只列出几个主要的约定规则:
- 数据库表名采用上下文类中对应的DbSet的属性名。
- 数据库表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型兼容的类型。
- 数据库表列的可空性取决于对应实体类属性的可空性。
- 名字为Id的属性为主键,如果主键为short、int或者long类型,则主键默认采用自动增长类型的列。
2. Data Annotation
优点:简单 缺点:耦合
Data Annotation(数据注释)指的是可以使用.NET提供的Attribute对实体类、属性等进行标注的方式来实现实体类配置。
比如通过[Table(“T_Books”)],我们可以把实体类对应的表名配置为T_Books;
通过[Required],我们可以把属性对应的数据库表列配置为”不可为空”;
通过[MaxLength(20)],我们可以把属性对应的数据库表配置为”最大长度为20″。
Data Annotation配置
[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属于官方的推荐用法,主要有以下两点原因。
- Fluent API能够更好地进行职责分离。实体类只负责进行抽象的描述,不涉及与数据库相关的细节,所有和数据库相关的细节被放到配置类中,这样我们能更方便地进行大型项目的管理。
- Fluent API的功能更强大。Fluent API几乎能实现Data Annotation的所有功能,而Data Annotation则不支持Fluent API的一些功能
Data Annotation和Fluent API是可以一起使用的。
如果同样的内容用这两种方式都配置,那么Fluent API的优先级高于Data Annotation。
基于此,在开发人员社区中有两种实体类的配置方案。
- 混合方案:优先使用Data Annotation,因为Data Annotation的使用更简单。在DataAnnotation无法实现的地方,再使用Fluent API进行配置
- 单一方案:只使用Fluent API
选择混合方案的开发人员不仅是因为考虑到了Data Annotation的使用更简单,而且考虑到了实体类上添加的[MaxLength(50)]、[Required]等Attribute可以被ASP.NET Core中的验证框架等复用。
但是个人不赞同这种做法。
因为基于分层的设计原则,我们不建议直接把实体类对象传递到视图层。
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实体类进行映射
modelBuilder.Entity<Blog>().ToView("blogsView")
4.2 排除属性映射
默认情况下,一个实体类的所有属性都会映射到数据库表中,如果想让EF Core忽略一个属性,就可以用Ignore配置。
modelBuilder.Entity<Book>().Ignore(b=>b.AuthorName);
4.3 数据库表列名
数据库表中的列名默认和属性名一样,我们可以使用HasColumnName方法配置一个不同的列名。
modelBuilder.Entity<Book>().Property(n => n.Price).HasColumnName("APrice");
4.4 列数据类型
EF Core默认会根据实体类的属性类型、最大长度等确定字段的数据类型,我们可以使用HasColumnType为列指定数据类型。
modelBuilder.Entity<Book>().Property(k => k.AuthorName).HasColumnType("varchar(200)");
4.5 主键
EF Core默认把名字为Id或者”实体类型+Id”的属性作为主键,我们可以用HasKey配置其他属性作为主键。
modelBuilder.Entity<Book>().HasKey(a=>a.Price);
为了保持项目命名的统一以及代码的简洁,这里建议开发人员采用默认的Id作为主键。
EF Core中也支持用多个属性作为复合主键,不过由于复合主键问题很多,不符合如今的软件设计的主流做法,因此不对其进行介绍。
4.6 索引
EF Core中可以用HasIndex方法配置索引。
modelBuilder.Entity<Book>().HasIndex(b=>b.Id);
EF Core也支持多个属性组成的复合索引,只要给HasIndex方法传递由一个或多个属性的名字组成的匿名类对象即可。
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属性对应的列定义为索引下面两种方法都可以
modelBuilder.Entity<Book>().HasIndex("Price");
modelBuilder.Entity<Book>().HasIndex(b=>b.Price);
同样地,用来获取实体类属性的Property方法也有多个重载方法,因此想要把Price属性对应的数据库表列的名字定位为No,我们可以用下面两种方法
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会在把数据保存到数据库之后,把自增主键的值自动赋值给主键属性
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算法的支持。