对于模型的配置,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);
});
// 返回模型
returnbuilder.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 方法引用的,道理一样。但要注意,运行时的模型不能用来创建数据库的。
好了,今天就水到这里了。