找回密码
 立即注册
首页 业界区 业界 【EF Core】使用外部 Model

【EF Core】使用外部 Model

兜蛇 2025-8-24 17:21:46
对于模型的配置,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 ServerMicrosoft.EntityFrameworkCore.Metadata.ConventionsSqlServerConventionSetBuilderCreateModelBuilder
SQLiteMicrosoft.EntityFrameworkCore.Metadata.ConventionsSqliteConventionSetBuilderCreateModelBuilder
PostgreSQLNpgsql.EntityFrameworkCore.PostgreSQL.Metadata.ConventionsNpgsqlConventionSetBuilderCreateModelBuilder
 这样一来,咱们配置外部模型就跟在 OnModelCreating 方法中一样了。
下面老周用一个示例让大伙伴们掌握使用方法。
第一步,写实体。
  1. public class Ultraman
  2. {
  3.     /// <summary>
  4.     /// 标识
  5.     /// </summary>
  6.     public int Uid { get; set; }
  7.     /// <summary>
  8.     /// 称号
  9.     /// </summary>
  10.     public required string Nick { get; set; }
  11.     /// <summary>
  12.     /// 年龄
  13.     /// </summary>
  14.     public int Age { get; set; }
  15.     /// <summary>
  16.     /// 特征
  17.     /// </summary>
  18.     public Speciality Spec { get; set; } = new();
  19. }
  20. /// <summary>
  21. /// 特性
  22. /// </summary>
  23. public class Speciality
  24. {
  25.     public int Id { get; set; }
  26.     /// <summary>
  27.     /// 身高
  28.     /// </summary>
  29.     public decimal Height { get; set; }
  30.     /// <summary>
  31.     /// 体重
  32.     /// </summary>
  33.     public decimal Weight { get; set; }
  34.     /// <summary>
  35.     /// 飞行速度
  36.     /// </summary>
  37.     public decimal FlightSpeed { get; set; }
  38. }
复制代码
Ultraman 表示超人,Speciality 表示超人的某些特征,如身高、体重、飞行速度。
第二步,派生 DbContext 类。
  1. public class DemoDbContext : DbContext
  2. {
  3.     public DemoDbContext(DbContextOptions<DemoDbContext> options)
  4.         : base(options)
  5.     {
  6.     }
  7.     public DbSet<Ultraman> Ultramen { get; setpublic DbSet<Speciality> SpecialSet { get; set; }
  8. }
复制代码
第三步,老周用一个 ModelHelper 类,公开静态的 BuildModel 方法。配置好模型后直接返回。
  1. public static class ModelHelper
  2. {
  3.     public static IModel BuildModel()
  4.     {
  5.         <strong>ModelBuilder builder </strong><strong>=</strong><strong> SqliteConventionSetBuilder.CreateModelBuilder();</strong>
  6.         builder.Entity<Ultraman>(et =>
  7.         {
  8.             // 主键
  9.             et.HasKey(x => x.Uid).HasName("PK_ultra_id");
  10.             // 长度约束
  11.             et.Property(x => x.Nick).HasMaxLength(25);
  12.             // 表映射
  13.             et.ToTable("tb_ultras", tb =>
  14.             {
  15.                 tb.Property(d => d.Uid).HasColumnName("ultr_id");
  16.                 tb.Property(d => d.Nick).HasColumnName("ultr_nick");
  17.                 tb.Property(x => x.Age).HasColumnName("ultr_age");
  18.             });
  19.             // 关系:一对一
  20.             et.HasOne(x => x.Spec)
  21.                 .WithOne()
  22.                 .HasForeignKey<Ultraman>("spec_id")
  23.                 .HasPrincipalKey<Speciality>(s => s.Id)
  24.                 .HasConstraintName("FK_ultra_spec");
  25.         });
  26.         builder.Entity<Speciality>(et =>
  27.         {
  28.             et.HasKey(c => c.Id).HasName("PK_spid");
  29.             // 表/列映射
  30.             et.ToTable("tb_spec", tb =>
  31.             {
  32.                 tb.Property(q => q.Id).HasColumnName("sp_id");
  33.                 tb.Property(q => q.Height).HasColumnName("sp_height");
  34.                 tb.Property(q => q.FlightSpeed).HasColumnName("sp_flightspeed");
  35.             });
  36.             // 精度控制
  37.             et.Property(k => k.FlightSpeed).HasPrecision(7, 2);
  38.             et.Property(m => m.Height).HasPrecision(5, 2);
  39.             et.Property(o => o.Weight).HasPrecision(3, 1);
  40.         });
  41.         // 返回模型
  42.         return<strong> builder.Model.FinalizeModel();</strong>
  43.     }
  44. }
复制代码
配置模型的过程相信大伙们都很熟了。上面代码两处关键:
1、调用 SqliteConventionSetBuilder.CreateModelBuilder 方法生成 ModelBuilder 实例。老周这次用的是 SQLite 数据库;
2、模型配置完后,通过 ModelBuilder 实例的 Model 属性来获取模型实例的引用。按照约定,应该调用模型的 FinalizeModel 方法,返回模型的最终形态(只读或 RuntimeModel)。
第四步,通过 Options 来配置数据库与模型相关参数,再传给 DbContext 的子类构造函数,就能达到全局共享选项和模型的目的。
  1. // 生成外部模型
  2. <strong>IModel extModel = ModelHelper.BuildModel();
  3. </strong>// 打印一下模型结构
  4. Console.WriteLine(<strong>extModel.ToDebugString()</strong>);
  5. Console.WriteLine("------------------------------------------------");
  6. // 选项
  7. var options = new DbContextOptionsBuilder<DemoDbContext>()
  8.             .UseSqlite("data source=test.db")   // 连接字符串
  9.             <strong>.UseModel(extModel)</strong>                 // 重要:使用外部模型
  10.             // .LogTo(log => Console.WriteLine(log))    // 日志
  11.             .Options;                           // 获取构建的选项实例
复制代码
首先当然是调用咱们刚写好的静态方法生成模型实例,应用外部模型很TM简单的,只要调用 UseModel 方法,把模型实例传递进去就好了。
ToDebugString 方法可以在生成模型中各实体的详细信息,就像这样:
  1. Model:
  2.   EntityType: Speciality
  3.     Properties:
  4.       Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
  5.       FlightSpeed (decimal) Required
  6.       Height (decimal) Required
  7.       Weight (decimal) Required
  8.     Keys:
  9.       Id PK
  10.   EntityType: Ultraman
  11.     Properties:
  12.       Uid (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
  13.       Age (int) Required
  14.       Nick (string) Required MaxLength(25)
  15.       spec_id (no field, int) Shadow Required FK Index
  16.     Navigations:
  17.       Spec (Speciality) ToPrincipal Speciality
  18.     Keys:
  19.       Uid PK
  20.     Foreign keys:
  21.       Ultraman {'spec_id'} -> Speciality {'Id'} Unique Required Cascade ToPrincipal: Spec
  22.     Indexes:
  23.       spec_id Unique
复制代码
在创建数据库前,在调试阶段,我们可以打印这信息来检查一下由实体构建的模型(Code First)是否正确。
第五步,用上面的选项类初始化上下文对象,先创建数据库,并写入几条数据记录。
  1. using (var c = new DemoDbContext(options))
  2. {
  3.     bool res = c.Database.EnsureCreated();
  4.     if (res)
  5.     {
  6.         c.Ultramen.Add(new()
  7.         {
  8.             Nick = "赛文",
  9.             Age = 17000,
  10.             Spec = new()
  11.             {
  12.                 Height = 40.0M,         // 米
  13.                 Weight = 35000.0M,      // 吨
  14.                 FlightSpeed = 7.0M      // 马赫
  15.             }
  16.         });
  17.         c.Ultramen.Add(new()
  18.         {
  19.             Nick = "爱迪",
  20.             Age = 8000,
  21.             Spec = new()
  22.             {
  23.                 Height = 50.0M,
  24.                 Weight = 44000.0M,
  25.                 FlightSpeed = 9.0M
  26.             }
  27.         });
  28.         c.Ultramen.Add(new()
  29.         {
  30.             Nick = "戴拿",
  31.             Age = 22,       // 飞鸟信年龄
  32.             Spec = new()
  33.             {
  34.                 Height = 55.0M,
  35.                 Weight = 45000.0M,
  36.                 FlightSpeed = 8.0M
  37.             }
  38.         });
  39.         c.Ultramen.Add(new()
  40.         {
  41.             Nick = "盖亚",
  42.             Age = 20,       // 高山我梦年龄
  43.             Spec = new()
  44.             {
  45.                 Height = 50.0M,
  46.                 Weight = 42000.0M,
  47.                 FlightSpeed = 20.0M
  48.             }
  49.         });
  50.         // 保存
  51.         c.SaveChanges();
  52.     }
  53. }
复制代码
第六步,把上面插入的记录查询出来。
  1. using (var c = new DemoDbContext(options))
  2. {
  3.     Console.WriteLine("{0,-5}{1,-7}{2,-7}{3,-7}{4,-10}", "名称", "年龄", "身高(米)", "体重(吨)", "飞行速度(马赫)");
  4.     Console.WriteLine("-------------------------------------------------------");
  5.     var q = <strong>c.Ultramen.Include(x =></strong><strong> x.Spec).ToList()</strong>;
  6.     foreach (Ultraman um in q)
  7.     {
  8.         Console.WriteLine($"{um.Nick,-5}{um.Age,-10}{um.Spec.Height,-12}{um.Spec.Weight,-13}{um.Spec.FlightSpeed,-10}");
  9.     }
  10. }
复制代码
这里要高度注意:c.Ultramen 访问 Ultraman 集合时,与 Ultraman 一对一的 Speciality 实体并没有加载。此处我们需要查询整个关系的数据,所以得调用  Include 方法把 Speciality 集合的数据也 SELECT 出来。ToList 方法真正触发 SQL 语句的生成和发送到数据库执行(与 LINQ 一样的原理)。
如果你嫌调用 Include 方法麻烦,可以在配置模型时让其默认预加载。
  1. builder.Entity<Ultraman>(et =>
  2. {
  3.    ……
  4.     // 关系:一对一
  5.     et.HasOne(x => x.Spec)
  6.         .WithOne()
  7.         .HasForeignKey<Ultraman>("spec_id")
  8.         .HasPrincipalKey<Speciality>(s => s.Id)
  9.         .HasConstraintName("FK_ultra_spec");
  10.     <strong>et.Navigation(k </strong><strong>=></strong><strong> k.Spec).AutoInclude();</strong>
  11. });
复制代码
如果导航属性是个集合,引用的记录比较多,还是不要自动 Include 了,手动的好一些。
示例查询结果如下:
  1. 名称   年龄      身高(米)    体重(吨)    飞行速度(马赫)  
  2. -------------------------------------------------------
  3. 赛文   17000     40.0        35000.0      7.0      
  4. 爱迪   8000      50.0        44000.0      9.0      
  5. 戴拿   22        55.0        45000.0      8.0
  6. 盖亚   20        50.0        42000.0      20.0
复制代码
使用 dotnet ef dbcontext optimize 命令生成的运行时模型也可以通过 UseModel 方法引用的,道理一样。但要注意,运行时的模型不能用来创建数据库的。
好了,今天就水到这里了。
 

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册