对于模型的配置,98.757%的情况下,我们使用“数据批注”特性类,或者 Fluent API (重写 DbContext 类的 OnModelCreating 方法)进行配置即可。但在少数情况下,可能会考虑在 DbContext 之外配置模型。比如:
- 你的实体类和模型,以及 DbContext 派生不在一个程序集中;
- 你可以想在配置模型时做一些自己特有的扩展;
- 你希望所有 DbContext 的实例共享一个 Model 实例,这样不必在每次实例化上下文时都配置一次模型。
话又说回来,其实就每次实例都配置一次模型也不用耗什么性能,除非实体很多,或特殊情况导致初始化较慢。
使用外部模型后,不仅可以把 DbContextOptions(选项)对象全局共享,连模型也顺便共享了。老周先介绍一下原理,比吃咸菜还简单。我们一般会使用 DbContextOptionsBuilder 类(不管是不是泛型版本)来构建 Options,其中,有一个 UseModel 方法,可以传递一个实现 IModel 接口的对象实例。对,就是模型对象。
于是,问题就聚焦在这个 IModel 接口上,咱们一般不会花十牛二虎之力自己实现 IModel 接口的,并且,EF Core 内部有实现类,叫 Model。虽然咱们可以访问此类,但从框架的角度看显然人家是不希望咱们在代码中使用它的。应用程序代码通过 ModelBuilder 类构建模型,再访问它的 Model 属性来获得模型实例的引用。你如果自己实现 IModel 接口,意义不大的,而且你还要花很多精力去重新实现 EF Core 的各部功能,才能与框架对接。
由 ModelBuilder 类相关 API 可以展现模型构建过程中的各种细节,即设计时模型。DbContext.Database.EnsureCreated 方法、迁移等功能在创建 / 修改数据库时都使用设计时的模型对象,毕竟其包含的元数据比较完整。
在预置约定的作用下,模型的设计时构建结束后,可以生成运行时模型。EnsureCreated 与迁移功能不使用运行时模型(使用了会报错)。但在数据查询、插入、删除、更新这些常规操作时是可以使用运行时模型的。你可以自己编写运行时模型,做法是继承 RuntimeModel 类(位于 Microsoft.EntityFrameworkCore.Metadata 命名空间)。不……过,这个其实也不用你去写的,dotnet-ef 命令行工具使用 dbcontext optimize 命令就可以帮我们生成代码了。为什么用工具生成而不动手去写呢,因为运行时模型的构造和设计时模型其实是相同的——描述实体的特征是相同的。通常我们通过 ModelBuilder 的API构建了模型,没有必要在 RuntimeModel 上又重复写一遍。所以贴心的微软给咱们准备了 ef 工具,代替我们做重复的工作。比如,迁移(Migration)的代码也是要描述实体到数据表的映射的(表名、列名等),这些咱们在 ModelBuilder 中就能做,也没有必要在 Migration 时又重写一番。
ef 工具生成的运行模型也可以传递给选项类的 UseModel 方法的,但文已提过,运行时模型是不能用来创建数据库和表的,只可用于增删改查。
(以下内容是重写的,可能与老周第一次写的内容有些不同,老周尽量按相同的思路写。因为中途去处理一下别的事情,回来发现电脑从睡眠中唤醒直接蓝屏了,草稿未保存,丢了。铭瑄的主板可能要背锅,以前用的 Acer 不会有这问题)
顺便解释一下设计时模型和运行时模型的不同。设计时模型就是我们常写的配置模型的过程,先由框架执行所有预置的约定,自动识别能识别的东西。随后执行咱们自己写的配置代码,完事后生成只读的模型(不用改了)。之后对数据做查询、更改等操作就用最终生成的模型。运行时模型说简单点,就是把模型的置进行“硬编码”,里面有几个实体,哪个类的,类有几个属性。表、列、函数、存储过程映射了哪些。主键是谁,外键是谁,全部用明确的代码写出来。不用执行预置约定,不用自动识别,不用猜测……直接把模型的结构写死了。也就是说,运行时模型执行的代码较少,性能会好一些。注意,这里仅仅指 EF Core 初始化阶段,至于查询数据的过程不受影响(查询也可以用 dotnet-ef 工具生成预编译的查询,原理和生成运行时模型差不多,就是少执行一些代码)。
用 dotnet-ef 工具生成优化代码一般在你发现程序初始化很慢时才考虑,如果不影响性能,可以不优化。
扯远了,回到咱们的主题。由于配置模型过程需要预置约定集合,咱们也没必要自己重写这些功能。同时,预置约定不仅包括 EF Core 部分,各个数据库提供者(如 SQL Server)可能会加入自己特有的约定。所以,我们手动把预置约定添加到集合中也很麻烦的,幸好,贴心的微软又又又为咱们准备了一组静态方法,直接调用就能生成 ModelBuilder 实例返回,非常地方便。
这些静态方法是按数据库提供者分组的:
数据库 | 命名空间 | 类 | 静态方法 |
SQL Server | Microsoft.EntityFrameworkCore.Metadata.Conventions | SqlServerConventionSetBuilder | CreateModelBuilder |
SQLite | Microsoft.EntityFrameworkCore.Metadata.Conventions | SqliteConventionSetBuilder | CreateModelBuilder |
PostgreSQL | Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions | NpgsqlConventionSetBuilder | CreateModelBuilder |
这样一来,咱们配置外部模型就跟在 OnModelCreating 方法中一样了。
下面老周用一个示例让大伙伴们掌握使用方法。
第一步,写实体。
public class Ultraman { /// <summary> /// 标识 /// </summary> public int Uid { get; set; } /// <summary> /// 称号 /// </summary> public required string Nick { get; set; } /// <summary> /// 年龄 /// </summary> public int Age { get; set; } /// <summary> /// 特征 /// </summary> public Speciality Spec { get; set; } = new(); } /// <summary> /// 特性 /// </summary> public class Speciality { public int Id { get; set; } /// <summary> /// 身高 /// </summary> public decimal Height { get; set; } /// <summary> /// 体重 /// </summary> public decimal Weight { get; set; } /// <summary> /// 飞行速度 /// </summary> public decimal FlightSpeed { get; set; } }
Ultraman 表示超人,Speciality 表示超人的某些特征,如身高、体重、飞行速度。
第二步,派生 DbContext 类。
public class DemoDbContext : DbContext { public DemoDbContext(DbContextOptions<DemoDbContext> options) : base(options) { } public DbSet<Ultraman> Ultramen { get; setpublic DbSet<Speciality> SpecialSet { get; set; } }
第三步,老周用一个 ModelHelper 类,公开静态的 BuildModel 方法。配置好模型后直接返回。
public static class ModelHelper { public static IModel BuildModel() { ModelBuilder builder = SqliteConventionSetBuilder.CreateModelBuilder(); builder.Entity<Ultraman>(et => { // 主键 et.HasKey(x => x.Uid).HasName("PK_ultra_id"); // 长度约束 et.Property(x => x.Nick).HasMaxLength(25); // 表映射 et.ToTable("tb_ultras", tb => { tb.Property(d => d.Uid).HasColumnName("ultr_id"); tb.Property(d => d.Nick).HasColumnName("ultr_nick"); tb.Property(x => x.Age).HasColumnName("ultr_age"); }); // 关系:一对一 et.HasOne(x => x.Spec) .WithOne() .HasForeignKey<Ultraman>("spec_id") .HasPrincipalKey<Speciality>(s => s.Id) .HasConstraintName("FK_ultra_spec"); }); builder.Entity<Speciality>(et => { et.HasKey(c => c.Id).HasName("PK_spid"); // 表/列映射 et.ToTable("tb_spec", tb => { tb.Property(q => q.Id).HasColumnName("sp_id"); tb.Property(q => q.Height).HasColumnName("sp_height"); tb.Property(q => q.FlightSpeed).HasColumnName("sp_flightspeed"); }); // 精度控制 et.Property(k => k.FlightSpeed).HasPrecision(7, 2); et.Property(m => m.Height).HasPrecision(5, 2); et.Property(o => o.Weight).HasPrecision(3, 1); }); // 返回模型 return builder.Model.FinalizeModel(); } }
配置模型的过程相信大伙们都很熟了。上面代码两处关键:
1、调用 SqliteConventionSetBuilder.CreateModelBuilder 方法生成 ModelBuilder 实例。老周这次用的是 SQLite 数据库;
2、模型配置完后,通过 ModelBuilder 实例的 Model 属性来获取模型实例的引用。按照约定,应该调用模型的 FinalizeModel 方法,返回模型的最终形态(只读或 RuntimeModel)。
第四步,通过 Options 来配置数据库与模型相关参数,再传给 DbContext 的子类构造函数,就能达到全局共享选项和模型的目的。
// 生成外部模型 IModel extModel = ModelHelper.BuildModel(); // 打印一下模型结构 Console.WriteLine(extModel.ToDebugString()); Console.WriteLine("------------------------------------------------"); // 选项 var options = new DbContextOptionsBuilder<DemoDbContext>() .UseSqlite("data source=test.db") // 连接字符串 .UseModel(extModel) // 重要:使用外部模型 // .LogTo(log => Console.WriteLine(log)) // 日志 .Options; // 获取构建的选项实例
首先当然是调用咱们刚写好的静态方法生成模型实例,应用外部模型很TM简单的,只要调用 UseModel 方法,把模型实例传递进去就好了。
ToDebugString 方法可以在生成模型中各实体的详细信息,就像这样:
Model: EntityType: Speciality Properties: Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd FlightSpeed (decimal) Required Height (decimal) Required Weight (decimal) Required Keys: Id PK EntityType: Ultraman Properties: Uid (int) Required PK AfterSave:Throw ValueGenerated.OnAdd Age (int) Required Nick (string) Required MaxLength(25) spec_id (no field, int) Shadow Required FK Index Navigations: Spec (Speciality) ToPrincipal Speciality Keys: Uid PK Foreign keys: Ultraman {'spec_id'} -> Speciality {'Id'} Unique Required Cascade ToPrincipal: Spec Indexes: spec_id Unique
在创建数据库前,在调试阶段,我们可以打印这信息来检查一下由实体构建的模型(Code First)是否正确。
第五步,用上面的选项类初始化上下文对象,先创建数据库,并写入几条数据记录。
using (var c = new DemoDbContext(options)) { bool res = c.Database.EnsureCreated(); if (res) { c.Ultramen.Add(new() { Nick = "赛文", Age = 17000, Spec = new() { Height = 40.0M, // 米 Weight = 35000.0M, // 吨 FlightSpeed = 7.0M // 马赫 } }); c.Ultramen.Add(new() { Nick = "爱迪", Age = 8000, Spec = new() { Height = 50.0M, Weight = 44000.0M, FlightSpeed = 9.0M } }); c.Ultramen.Add(new() { Nick = "戴拿", Age = 22, // 飞鸟信年龄 Spec = new() { Height = 55.0M, Weight = 45000.0M, FlightSpeed = 8.0M } }); c.Ultramen.Add(new() { Nick = "盖亚", Age = 20, // 高山我梦年龄 Spec = new() { Height = 50.0M, Weight = 42000.0M, FlightSpeed = 20.0M } }); // 保存 c.SaveChanges(); } }
第六步,把上面插入的记录查询出来。
using (var c = new DemoDbContext(options)) { Console.WriteLine("{0,-5}{1,-7}{2,-7}{3,-7}{4,-10}", "名称", "年龄", "身高(米)", "体重(吨)", "飞行速度(马赫)"); Console.WriteLine("-------------------------------------------------------"); var q = c.Ultramen.Include(x => x.Spec).ToList(); foreach (Ultraman um in q) { Console.WriteLine($"{um.Nick,-5}{um.Age,-10}{um.Spec.Height,-12}{um.Spec.Weight,-13}{um.Spec.FlightSpeed,-10}"); } }
这里要高度注意:c.Ultramen 访问 Ultraman 集合时,与 Ultraman 一对一的 Speciality 实体并没有加载。此处我们需要查询整个关系的数据,所以得调用 Include 方法把 Speciality 集合的数据也 SELECT 出来。ToList 方法真正触发 SQL 语句的生成和发送到数据库执行(与 LINQ 一样的原理)。
如果你嫌调用 Include 方法麻烦,可以在配置模型时让其默认预加载。
builder.Entity<Ultraman>(et => { …… // 关系:一对一 et.HasOne(x => x.Spec) .WithOne() .HasForeignKey<Ultraman>("spec_id") .HasPrincipalKey<Speciality>(s => s.Id) .HasConstraintName("FK_ultra_spec"); et.Navigation(k => k.Spec).AutoInclude(); });
如果导航属性是个集合,引用的记录比较多,还是不要自动 Include 了,手动的好一些。
示例查询结果如下:
名称 年龄 身高(米) 体重(吨) 飞行速度(马赫) ------------------------------------------------------- 赛文 17000 40.0 35000.0 7.0 爱迪 8000 50.0 44000.0 9.0 戴拿 22 55.0 45000.0 8.0 盖亚 20 50.0 42000.0 20.0
使用 dotnet ef dbcontext optimize 命令生成的运行时模型也可以通过 UseModel 方法引用的,道理一样。但要注意,运行时的模型不能用来创建数据库的。
好了,今天就水到这里了。