本文所讲述内容,大伙伴们不必要完全掌握,毕竟,dotnet ef 工具会帮助咱们生成相关代码。不过,为了让各位能够真正了解它,老周会做一次纯手搓代码。

所谓优化后的模型,相当于把实体的配置进行“硬编码”,程序会执行更少的代码来提升效率。放到实际代码编写上,这个活叫运行时模型,对应的类就是 RuntimeModel。与运行时模型相关的类型都是以“Runtime”开头的,对应着模型各个部分。例如:

1、实体—— RuntimeEntityType 类;

2、属性—— RuntimeProperty 类;

3、主键—— RuntimeKey 类;

4、导航—— RuntimeNavigation 类。导航属性使用专门的类,而不归类于 RuntimeProperty;

5、外键—— RuntimeForeignKey 类;

6、索引—— RuntimeIndex 类;

7、其他,这里就不全列出了。

有大伙伴可能会想:都定义实体类了,为什么还要构建 Model 呢?因为这体类本身无法提供额外的信息,比如谁是主键,映射到哪个表哪个列,有哪些列是索引……而且,ef core 还要跟踪每个实体对象的状态,以便在必要时更新数据库。有太多的附加信息要处理,所以得有专门的类型去封装这些信息。

除了上面列出的专用运行时类型,EF Core 框架还用到另一个重要的东西去描述实体——Annotations(批注,或者叫注释)。EF Core 框架还定义了一个 Annotation 类,专门保存批注信息。批注集合是一个字典,故一条批注是以 Name-Value 的形式存在的。Name 是字符串,Value 是任意类型。咱们在代码中能访问到的常用名称由 RelationalAnnotationNames 公开,如 TableName 字段可以返回表示表名称的字符串。另外,不同数据库提供者也可以公开专用的批注名称,如 SQL Server 数据库公开了 SqlServerAnnotationNames,不过,此类位于 Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal 命名空间,帮助文档上是查不到的,属于留给内部使用的。当然,咱们在代码中是可以访问的。

批注名称是用冒号来分隔层次的,有点像 ASP.NET Core 的配置树。比如,表示 SQL Server 标识列的批注是这样的:SqlServer:Identity,它的值是字符串,格式为 1,1 表示从1开始,增量为1。如果从1000开始增量为5,就是 1000,5(用逗号分隔)。

现在说回 RuntimeModel 类。实例化后,通过对应的方法调用来添加模型:

1、AddEntityType 方法,向模型添加实体,返回 RuntimeEntityType 实例;

2、调用 RuntimeEntityType 实例的 AddProperty 方法添加属性,返回 RuntimeProperty 实例;

3、调用 RuntimeEntityType 实例的 AddKey 方法创建主键,返回 RuntimeKey 实例,随后要调用 SetPrimaryKey 方法设置主键;调用 AddForeignKey 方法添加外键,返回 RuntimeForeignKey 实例。

……

总结一下思路:创模型,添实体,加属性,设主键,置外键,最后补批注

上面就是本文所需的知识,运行时模型在建立时所调用的方法成员参数较多,而且不是所有参数都会用到,一般建议写上参数名。用命名参数就不必按顺序传参。这些参数等下面实际演练时再解释。

===============================================================================================

好了,理论上准备得差不多,接下来老周将纯手动打造一个运行时模型。由于运行时模型是不能创建数据库和表的,所以咱们采用 DB First 方案,先创建数据库。本次老周使用 SQL Server,使用以下脚本创建数据库(建议把脚本存为文件,后面写代码时可以参考):

USE master;
GO -- 创建数据库
CREATE DATABASE ToyDB;
GO
USE ToyDB;
GO -- 创建表
CREATE TABLE dbo.tb_boys
(
boy_id INT IDENTITY NOT NULL,
[name] NVARCHAR(20) NOT NULL,
[nick] NVARCHAR(20) NULL,
CONSTRAINT [PK_boy_id] PRIMARY KEY (boy_id ASC)
);
GO CREATE TABLE dbo.tb_toys
(
toy_id INT IDENTITY(100, 1) NOT NULL,
toy_desc NVARCHAR(50) NOT NULL,
toy_cate NVARCHAR(10) NOT NULL,
f_boy_id INT NULL, -- 外键
CONSTRAINT [PK_toy_id] PRIMARY KEY (toy_id ASC),
CONSTRAINT [FK_toy_boy] FOREIGN KEY (f_boy_id) REFERENCES dbo.tb_boys (boy_id) ON DELETE CASCADE
);
GO

tb_boys 表代表某男孩,tb_toys 表代表他拥有的玩具,每个 boy 在小时候都有自己喜欢的 toy,正如每个进城的农民工都怀揣着梦想一般;随着 boy 们的长大,许多 toy 逐渐被淡忘;随着时光的流逝,农民工们的梦已被风雨打碎,留下千丝万缕的遗恨与怅惘。

咱们程序中定义的实体类与数据表的名称是不相同的,所以后期要做映射。

public class Boy
{
public int ID { get; set; }
public string Name { get; set; } = string.Empty;
public string? Nick { get; set; } // 导航属性
public ICollection<Toy> MyToys { get; set; } = new List<Toy>();
} public class Toy
{
public int ToyID { get; set; }
public string Desc { get; set; } = string.Empty;
public string Cate { get; set; } = "经典玩具"; // 导航属性的反向引用,外键隐藏
public Boy? TheBoy { get; set; }
}

两个类都有导航属性,每个 Boy 的童年不可能只有一件玩具的,每件玩具专属于喜欢它的主人,因此这是一对多的关系。

好了,下面是重头戏,看看怎么构建 RuntimeModel。

1、实例化模型。

RuntimeModel model = new(
skipDetectChanges: false, // 开启更改跟踪
modelId: Guid.NewGuid(), // 模型ID
entityTypeCount: 2, // 模型中有两个实体类型
typeConfigurationCount: 0 // 未使用自定义类型转换
);

skipDetectChanges:99%选 false,因为我们希望跟踪模型的变更。modelId:模型的ID,GUID类型,随便分配,不会重复的。entityTypeCount:这个模型包含几个实体类型,这里明确指出(属于是硬编码了)。typeConfigurationCount:保留默认0即可,实体的类型映射,这里咱们用不上,98%的情形也是用不上的。

2、添加 Boy 实体。

var boyEnt = model.AddEntityType(
name: "DB.Boy", // 一般包含命名空间
type: typeof(Boy), // CLR 类型
baseType: null, // 没有基类(指除Object以外)
propertyCount: 3, // 属性个数,不含导航属性
navigationCount: 1, // 有一个导航属性
keyCount: 1 // 有一个主键
);

name 表示实体名称,一般用实体类的完整类型名(含命名空间+类名)。type 表示实体的类型 Type。baseType:如果 Boy 有基类,要附上基类型。这里说的基类是除 Object 以外,此处,Boy 可以认为无基类型。sharedClrType = false,不是共享的实体类型(多个实体共用一个CLR类型)。propertyCount 表示此实体包含属性个数,应为3,不包括导航属性,导航属性有家族背景,需要特殊对待。navigationCount 表示包含多少个导航属性,这里是一个。keyCount 表示实体有几个主键,这里一个。

3、添加 Boy 实体的属性。注意,属性是加在 RuntimeEntity 中。

var boyIdProp = boyEnt.AddProperty(
name: nameof(Boy.ID), // 属性名
clrType: typeof(int), // 属性类型
nullable: false, // 不为null
// 引用CLR属性成员
propertyInfo: typeof(Boy).GetProperty("ID", BindingFlags.
Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
// 无字段成员
fieldInfo: null,
// 优先使用属性来设置值
propertyAccessMode: PropertyAccessMode.PreferProperty,
// 插入时生成值
valueGenerated: ValueGenerated.OnAdd,
// 实体存入数据库后不允许修改此属性
afterSaveBehavior: PropertySaveBehavior.Throw,
// 默认值0
sentinel: 0
);
// SQL Server 值生成配置-标识列
boyIdProp.AddAnnotation(SqlServerAnnotationNames.
ValueGenerationStrategy, SqlServerValueGenerationStrategy.
IdentityColumn);
var nameProp = boyEnt.AddProperty(
name: nameof(Boy.Name),
clrType: typeof(string),
nullable: false,
propertyInfo: typeof(Boy).GetProperty("Name", BindingFlags.
Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
maxLength: 20, // 长度限制
propertyAccessMode: PropertyAccessMode.PreferProperty
);
var nickProp = boyEnt.AddProperty(
name: nameof(Boy.Nick),
clrType: typeof(string),
nullable: true,
maxLength: 20,
propertyInfo: typeof(Boy).GetProperty(nameof(Boy.Nick),
BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.
Instance),
propertyAccessMode: PropertyAccessMode.PreferProperty
);

name:属性名,一般与 CLR 类型的属性名相同。clrType:属性值的类型。propertyInfo:引用CLR属性的成员(通过反射来设置或读取值),设置值时要求用 set 访问器。fieldInfo:存储属性值的字段成员,没有的话也可以为 null。propertyAccessMode:表示 EF Core 如何访问属性,这是一个 PropertyAccessMode 枚举,主要值有:

A、Field:必须通过字段来读写属性。使用该方式,实体类的属性必须定义隐藏字段。

B、FieldDuringConstruction:只在实体实例化(调用构造器)时才通字段访问属性。

C、Property:直接读写属性本身。

D、PreferField:先尝试通过字段来读写属性,如果没有,直接访问属性。

E:PreferFieldDuringConstruction:仅在实例构造时,尝试通过字段访问属性,若不行改为直接访问属性。

F:PreferProperty:通过属性直接访问。如果没有属性,或 get/set 不存在(通常不会这样),就改为通过字段读写。

上述枚举值要根据你的 propertyInfo 和 fieldInfo 参数来配置。

nullable:表示此属性是否可为 null。valueGenerated:值生成,如果是标识列,应选择 ValueGenerated.OnAdd,插入数据时生成。beforeSaveBehavior 和 afterSaveBehavior:指示属性值在写入数据库前后是否允许修改。对于标识列,afterSaveBehavior 参数可以设置为 Throw,即如果数据已存入数据库,那么后面就不让你修改(不能改生成的主键)。maxLength:一般用于字符类型,最大长度。unicode:一般用于字符类型,是否使用 Unicode。precision 和 scale:一般用于浮点数,指定精度和位数。sentinel:如果属性未设置,给它一个默认值。

注意:boyid是标识列,要用批注表明它是标识列。

boyIdProp.AddAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy, SqlServerValueGenerationStrategy.IdentityColumn);

4、给 Boy 添加主键。

// 配置主键
var boyKey = boyEnt.AddKey([boyIdProp]);
boyEnt.SetPrimaryKey(boyKey);
// 主键名称
boyKey.AddAnnotation(RelationalAnnotationNames.Name, "PK_boy_id");

主键的名称通过批注来设置。AddKey 方法引用一个属性列表,即哪些属性设置为键。

5、 Toy 实体的配置差不多。

var toyEnt = model.AddEntityType(
name: "DB.Toy",
type: typeof(Toy),
baseType: null,
propertyCount: 3, // 属性数量不含导航属性
navigationCount: 1, // 导航属性个数
keyCount: 1, // 主键个数
foreignKeyCount: 1 // 包含外键
); var toyidProp = toyEnt.AddProperty(
name: nameof(Toy.ToyID),
clrType: typeof(int),
nullable: false,
valueGenerated: ValueGenerated.OnAdd,
afterSaveBehavior: PropertySaveBehavior.Throw,
propertyAccessMode: PropertyAccessMode.PreferProperty,
propertyInfo: typeof(Toy).GetProperty(nameof(Toy.ToyID),
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
sentinel: 0
); // 配置标识列
toyidProp.AddAnnotation(SqlServerAnnotationNames.
ValueGenerationStrategy, SqlServerValueGenerationStrategy.
IdentityColumn);
var toydescProp = toyEnt.AddProperty(
name: nameof(Toy.Desc),
clrType: typeof(string),
nullable: false,
maxLength: 50,
propertyInfo: typeof(Toy).GetProperty(nameof(Toy.Desc),
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
propertyAccessMode: PropertyAccessMode.PreferProperty
); var toycateProp = toyEnt.AddProperty(
name: nameof(Toy.Cate),
clrType: typeof(string),
maxLength: 10,
propertyInfo: typeof(Toy).GetProperty(nameof(Toy.Cate),
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
propertyAccessMode: PropertyAccessMode.PreferProperty
);
// 以下属性为影子属性,映射到外键
var toytoboyIdProp = toyEnt.AddProperty(
name: "boy_id",
clrType: typeof(int),
propertyInfo: null, // 影子属性不需要引用属性成员
nullable: true
);

boy_id 是影子属性,因为不需要与 CLR 类型映射,所以,propertyInfo 和 fieldInfo 参数不需要。虽然这货没有在实体类中定义,但 EF Core 内部会用一个字典来管理它。实际上 EF Core 为实体类存了不止一份值,比如“快照”。即当数据从数据库查询出来时会做一个快照,后面会用实体的值跟快照比较,以确定实体是否被修改了。

6、给 Toy 实体设置主键和外键。

 // 设置主键
var toyKey = toyEnt.AddKey([toyidProp]);
// 主键名
toyKey.AddAnnotation(RelationalAnnotationNames.Name, "PK_toy_id");
toyEnt.SetPrimaryKey(toyKey);
// 设置外键
var toyforekey = toyEnt.AddForeignKey(
properties: [toytoboyIdProp],
principalKey: boyKey,
principalEntityType: boyEnt,
deleteBehavior: DeleteBehavior.Cascade,
unique: false // 一对多,这里不唯一
);
// 外键名称
toyforekey.AddAnnotation(RelationalAnnotationNames.Name,
"FK_toy_boy");

由于 Toy 表中会出现重复的 Boy ID,所以,AddForeignKey 方法的 unique 参数为 false。

7、导航属性,不管是 Boy 中的导航属性还是 Toy 中的,共用一个外键信息。

var boytotoyNav = boyEnt.AddNavigation(
name: nameof(Boy.MyToys),
foreignKey: toyforekey,
onDependent: false,
clrType: typeof(ICollection<Toy>),
propertyInfo: typeof(Boy).GetProperty(nameof(Boy.MyToys),
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
fieldInfo: null,
propertyAccessMode: PropertyAccessMode.PreferProperty
); var toytoboyNav = toyEnt.AddNavigation(
name: nameof(Toy.TheBoy),
foreignKey: toyforekey,
onDependent: true,
clrType: typeof(Boy),
propertyInfo: typeof(Toy).GetProperty(nameof(Toy.TheBoy),
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
fieldInfo: null,
propertyAccessMode: PropertyAccessMode.PreferProperty
);

导航属性的添加和一般属性差不多。注意 onDependent 参数的设置,对于 Boy 来说,它应该是“父”,即主体(被 Toy 的外键引用),要设置为 false;而对于 Toy 而言为“子”,即依赖于主体,故 onDependent 为 true。

8、由于实体名和属性名都与数据库不同,所以还要做表、列的映射。这些也是通过批注完成的。

// 1、架构
boyEnt.AddAnnotation(RelationalAnnotationNames.Schema, "dbo");
toyEnt.AddAnnotation(RelationalAnnotationNames.Schema, "dbo");
// 2、表名
boyEnt.AddAnnotation(RelationalAnnotationNames.TableName, "tb_boys");
toyEnt.AddAnnotation(RelationalAnnotationNames.TableName, "tb_toys");
// 3、列名
boyIdProp.AddAnnotation(RelationalAnnotationNames.ColumnName,
"boy_id");
nameProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "name");
nickProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "nick"); toyidProp.AddAnnotation(RelationalAnnotationNames.ColumnName,
"toy_id");
toydescProp.AddAnnotation(RelationalAnnotationNames.ColumnName,
"toy_desc");
toycateProp.AddAnnotation(RelationalAnnotationNames.ColumnName,
"toy_cate");
toytoboyIdProp.AddAnnotation(RelationalAnnotationNames.ColumnName,
"f_boy_id");

好了,模型就配好了。下面是完整代码。老周用了一个静态类来封装(你也可以像 dotnet-ef 工具那样,从 RuntimeModel 类派生)。

#pragma warning disable EF1001 // Internal EF Core API usage.
public static class ModelHelpers
{
private static readonly RuntimeModel _model; static ModelHelpers()
{
_model = BuildRuntimeModel();
} public static RuntimeModel DemoModel => _model; private static RuntimeModel BuildRuntimeModel()
{
RuntimeModel model = new(
skipDetectChanges: false, // 开启更改跟踪
modelId: Guid.NewGuid(), // 模型ID
entityTypeCount: 2, // 模型中有两个实体类型
typeConfigurationCount: 0 // 未使用自定义类型转换
); // 配置第一个实体
var boyEnt = model.AddEntityType(
name: "DB.Boy", // 一般包含命名空间
type: typeof(Boy), // CLR 类型
baseType: null, // 没有基类(指除Object以外)
propertyCount: 3, // 属性个数,不含导航属性
navigationCount: 1, // 有一个导航属性
keyCount: 1 // 有一个主键
);
// 添加属性
var boyIdProp = boyEnt.AddProperty(
name: nameof(Boy.ID), // 属性名
clrType: typeof(int), // 属性类型
nullable: false, // 不为null
// 引用CLR属性成员
propertyInfo: typeof(Boy).GetProperty("ID", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
// 无字段成员
fieldInfo: null,
// 优先使用属性来设置值
propertyAccessMode: PropertyAccessMode.PreferProperty,
// 插入时生成值
valueGenerated: ValueGenerated.OnAdd,
// 实体存入数据库后不允许修改此属性
afterSaveBehavior: PropertySaveBehavior.Throw,
// 默认值0
sentinel: 0
);
// SQL Server 值生成配置-标识列
boyIdProp.AddAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy, SqlServerValueGenerationStrategy.IdentityColumn); var nameProp = boyEnt.AddProperty(
name: nameof(Boy.Name),
clrType: typeof(string),
nullable: false,
propertyInfo: typeof(Boy).GetProperty("Name", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
maxLength: 20, // 长度限制
propertyAccessMode: PropertyAccessMode.PreferProperty
); var nickProp = boyEnt.AddProperty(
name: nameof(Boy.Nick),
clrType: typeof(string),
nullable: true,
maxLength: 20,
propertyInfo: typeof(Boy).GetProperty(nameof(Boy.Nick), BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance),
propertyAccessMode: PropertyAccessMode.PreferProperty
); // 配置主键
var boyKey = boyEnt.AddKey([boyIdProp]);
boyEnt.SetPrimaryKey(boyKey);
// 主键名称
boyKey.AddAnnotation(RelationalAnnotationNames.Name, "PK_boy_id"); // 添加第二个实体
var toyEnt = model.AddEntityType(
name: "DB.Toy",
type: typeof(Toy),
baseType: null,
propertyCount: 3, // 属性数量不含导航属性
navigationCount: 1, // 导航属性个数
keyCount: 1, // 主键个数
foreignKeyCount: 1 // 包含外键
);
// 添加属性
var toyidProp = toyEnt.AddProperty(
name: nameof(Toy.ToyID),
clrType: typeof(int),
nullable: false,
valueGenerated: ValueGenerated.OnAdd,
afterSaveBehavior: PropertySaveBehavior.Throw,
propertyAccessMode: PropertyAccessMode.PreferProperty,
propertyInfo: typeof(Toy).GetProperty(nameof(Toy.ToyID), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0
);
// 配置标识列
toyidProp.AddAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy, SqlServerValueGenerationStrategy.IdentityColumn); var toydescProp = toyEnt.AddProperty(
name: nameof(Toy.Desc),
clrType: typeof(string),
nullable: false,
maxLength: 50,
propertyInfo: typeof(Toy).GetProperty(nameof(Toy.Desc), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
propertyAccessMode: PropertyAccessMode.PreferProperty
); var toycateProp = toyEnt.AddProperty(
name: nameof(Toy.Cate),
clrType: typeof(string),
maxLength: 10,
propertyInfo: typeof(Toy).GetProperty(nameof(Toy.Cate), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
propertyAccessMode: PropertyAccessMode.PreferProperty
);
// 以下属性为影子属性,映射到外键
var toytoboyIdProp = toyEnt.AddProperty(
name: "boy_id",
clrType: typeof(int),
propertyInfo: null, // 影子属性不需要引用属性成员
nullable: true
); // 设置主键
var toyKey = toyEnt.AddKey([toyidProp]);
// 主键名
toyKey.AddAnnotation(RelationalAnnotationNames.Name, "PK_toy_id");
toyEnt.SetPrimaryKey(toyKey);
// 设置外键
var toyforekey = toyEnt.AddForeignKey(
properties: [toytoboyIdProp],
principalKey: boyKey,
principalEntityType: boyEnt,
deleteBehavior: DeleteBehavior.Cascade,
unique: false // 一对多,这里不唯一
);
// 外键名称
toyforekey.AddAnnotation(RelationalAnnotationNames.Name, "FK_toy_boy"); // 建立导航关系
var boytotoyNav = boyEnt.AddNavigation(
name: nameof(Boy.MyToys),
foreignKey: toyforekey,
onDependent: false,
clrType: typeof(ICollection<Toy>),
propertyInfo: typeof(Boy).GetProperty(nameof(Boy.MyToys), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null,
propertyAccessMode: PropertyAccessMode.PreferProperty
);
var toytoboyNav = toyEnt.AddNavigation(
name: nameof(Toy.TheBoy),
foreignKey: toyforekey,
onDependent: true,
clrType: typeof(Boy),
propertyInfo: typeof(Toy).GetProperty(nameof(Toy.TheBoy), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null,
propertyAccessMode: PropertyAccessMode.PreferProperty
); // 表、列映射
// 1、架构
boyEnt.AddAnnotation(RelationalAnnotationNames.Schema, "dbo");
toyEnt.AddAnnotation(RelationalAnnotationNames.Schema, "dbo");
// 2、表名
boyEnt.AddAnnotation(RelationalAnnotationNames.TableName, "tb_boys");
toyEnt.AddAnnotation(RelationalAnnotationNames.TableName, "tb_toys");
// 3、列名
boyIdProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "boy_id");
nameProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "name");
nickProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "nick"); toyidProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "toy_id");
toydescProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "toy_desc");
toycateProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "toy_cate");
toytoboyIdProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "f_boy_id"); // 完事,收工 return model;
}
}
#pragma warning restore EF1001 // Internal EF Core API usage.

下面定义上下文类。

public class DemoContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder ob)
{
ob.UseSqlServer(@"server=你的服务器; database=你的数据库")
.LogTo(o => Console.WriteLine(o))
.EnableSensitiveDataLogging(true)
.UseModel(ModelHelpers.DemoModel);
} public DbSet<Boy> BoySet { get; set; }
public DbSet<Toy> ToySet { get; set; }
}

熟悉的配方,不用多解释了。

下面是测试代码。

using DemoContext dc = new();

if (!dc.BoySet.Any())
{
Boy b1 = new()
{
Name = "小张",
Nick = "灰太狼"
};
b1.MyToys.Add(new()
{
Desc = "电动风车",
Cate = "电动玩具"
});
b1.MyToys.Add(new()
{
Desc = "米老鼠高压水枪",
Cate = "气动玩具"
});
dc.BoySet.Add(b1); Boy b2 = new Boy
{
Nick = "哈巴狗",
Name = "小李"
};
b2.MyToys.Add(new() { Desc = "库洛牌", Cate = "卡牌" });
b2.MyToys.Add(new() { Desc = "yoyo", Cate = "光电玩具" });
dc.BoySet.Add(b2); // 更新
dc.SaveChanges();
}

插入的数据如下:

================================================================================================

上面内容是不实用的,只是方便学习,下面老周演示一下如何用 dotnet-ef 工具生成运行时模型。这才是咱们在实际项目中要用的(除非特殊需求,要自己去写)。

1、新建一个控制台项目。

dotnet new console -n Demo -o .

2、添加要用的包。

dotnet add package microsoft.entityframeworkcore
dotnet add package microsoft.entityframeworkcore.sqlserver

3、要使用 dotnet-ef 工具,还得添加设计时包。

dotnet add package microsoft.entityframeworkcore.design

添加完毕后,项目应该引用了三个库:

  <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
</ItemGroup>

设计时库被设置为“私有资产”,你在代码中无法访问。如果你在代码要用,最简单方法是把 IncludeAssets、PrivateAssets 节点干掉。

4、定义实体类。

public class Light
{
/// <summary>
/// 主键
/// </summary>
public Guid LightID { get; set; }
/// <summary>
/// 灯光颜色
/// </summary>
public string[] Color { get; set; } = ["白"];
/// <summary>
/// 功率(瓦)
/// </summary>
public float Power { get; set; }
/// <summary>
/// 是否为 RGB LED 灯
/// </summary>
public bool? IsRGBLed { get; set; }
}

5、定义上下文类。

public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options)
: base(options)
{ } // 配置模型
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Light>().HasKey(e => e.LightID).HasName("PK_light_id");
mb.Entity<Light>().Property(g => g.LightID)
.ValueGeneratedOnAdd();
mb.Entity<Light>().Property(x => x.Color)
.HasMaxLength(200).IsRequired();
mb.Entity<Light>().Property(s => s.Power)
.HasPrecision(5, 2);
mb.Entity<Light>().ToTable("tb_lights", "dbo", tb =>
{
tb.Property(a => a.LightID).HasColumnName("light_id");
tb.Property(a => a.Color).HasColumnName("light_colors");
tb.Property(a => a.Power).HasColumnName("light_pwr");
tb.Property(a => a.IsRGBLed).HasColumnName("is_rgbled");
});
} public DbSet<Light> Lights { get; set; }
}

6、定义一个类,实现 IDesignTimeDbContextFactory 接口。在执行 dotnet ef 命令时可以用来创建 MyDbContext 实例(设计阶段专用)。

public class CustDesigntimeFactory : IDesignTimeDbContextFactory<MyDbContext>
{
public MyDbContext CreateDbContext(string[] args)
{
// 创建选项
DbContextOptions<MyDbContext> options = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlServer(@"server=你的服务器; database=你的数据库")
.LogTo(s => Console.WriteLine(s))
.Options;
// 实例化上下文对象
return new MyDbContext(options);
}
}

代码不用往下写了,这个时候,就可以用 ef 工具生成优化的模型了。

dotnet ef dbcontext optimize -c MyDbContext -n DB -o DB

-c 指定要用到的 DbContext 子类,这里可以省略,工具会自动搜索项目中的 MyDbContext 类。-n 表示生成代码使用的命名空间,这里指定了 DB。-o 表示生成的代码文件存放到哪个目录中,这里存到 DB 目录下(相对于项目目录)。

工具先编译一下项目,然后实例化 MyDbContext 类,执行一遍模型配置,最终生成相关代码。

MyDbContextModel.cs 和 MyDbContextModelBuilder.cs 都是模型类相关。

namespace DB
{
[DbContext(typeof(MyDbContext))]
public partial class MyDbContextModel : RuntimeModel
{
private static readonly bool _useOldBehavior31751 =
System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751; static MyDbContextModel()
{
var model = new MyDbContextModel(); if (_useOldBehavior31751)
{
model.Initialize();
}
else
{
var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024);
thread.Start();
thread.Join(); void RunInitialization()
{
model.Initialize();
}
} model.Customize();
_instance = (MyDbContextModel)model.FinalizeModel();
} private static MyDbContextModel _instance;
public static IModel Instance => _instance; partial void Initialize(); partial void Customize();
}
}

注意,MyDbContextModel 类是 partial 的,意味着你可以自定义扩展它,只要写在其他文件中,若模型修改了,重新运行 ef dbcontext 命令也不会被覆盖。

此类应用了 DbContextAttribute 特性类,表明此模型与 MyDbContext 关联。

LightEntityType.cs 文件就是配置 Light 实体的代码了,看看里面有啥。

    public partial class LightEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"DataBS.Light",
typeof(Light),
baseEntityType,
propertyCount: 4,
keyCount: 1); var lightID = runtimeEntityType.AddProperty(
"LightID",
typeof(Guid),
propertyInfo: typeof(Light).GetProperty("LightID", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(Light).GetField("<LightID>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
afterSaveBehavior: PropertySaveBehavior.Throw,
sentinel: new Guid("00000000-0000-0000-0000-000000000000")); var overrides = new StoreObjectDictionary<RuntimeRelationalPropertyOverrides>();
var lightIDTb_lights = new RuntimeRelationalPropertyOverrides(
lightID,
StoreObjectIdentifier.Table("tb_lights", "dbo"),
true,
"light_id");
overrides.Add(StoreObjectIdentifier.Table("tb_lights", "dbo"), lightIDTb_lights);
lightID.AddAnnotation("Relational:RelationalOverrides", overrides); lightID.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var color = runtimeEntityType.AddProperty(
"Color",
typeof(string[]),
propertyInfo: typeof(Light).GetProperty("Color", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(Light).GetField("<Color>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
maxLength: 200); var overrides0 = new StoreObjectDictionary<RuntimeRelationalPropertyOverrides>();
var colorTb_lights = new RuntimeRelationalPropertyOverrides(
color,
StoreObjectIdentifier.Table("tb_lights", "dbo"),
true,
"light_colors");
overrides0.Add(StoreObjectIdentifier.Table("tb_lights", "dbo"), colorTb_lights);
color.AddAnnotation("Relational:RelationalOverrides", overrides0); color.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var isRGBLed = runtimeEntityType.AddProperty(
"IsRGBLed",
typeof(bool?),
propertyInfo: typeof(Light).GetProperty("IsRGBLed", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(Light).GetField("<IsRGBLed>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true); var overrides1 = new StoreObjectDictionary<RuntimeRelationalPropertyOverrides>();
var isRGBLedTb_lights = new RuntimeRelationalPropertyOverrides(
isRGBLed,
StoreObjectIdentifier.Table("tb_lights", "dbo"),
true,
"is_rgbled");
overrides1.Add(StoreObjectIdentifier.Table("tb_lights", "dbo"), isRGBLedTb_lights);
isRGBLed.AddAnnotation("Relational:RelationalOverrides", overrides1); isRGBLed.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var power = runtimeEntityType.AddProperty(
"Power",
typeof(float),
propertyInfo: typeof(Light).GetProperty("Power", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(Light).GetField("<Power>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
precision: 5,
scale: 2,
sentinel: 0f); var overrides2 = new StoreObjectDictionary<RuntimeRelationalPropertyOverrides>();
var powerTb_lights = new RuntimeRelationalPropertyOverrides(
power,
StoreObjectIdentifier.Table("tb_lights", "dbo"),
true,
"light_pwr");
overrides2.Add(StoreObjectIdentifier.Table("tb_lights", "dbo"), powerTb_lights);
power.AddAnnotation("Relational:RelationalOverrides", overrides2); power.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var key = runtimeEntityType.AddKey(
new[] { lightID });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "PK_light_id"); return runtimeEntityType;
} public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
runtimeEntityType.AddAnnotation("Relational:Schema", "dbo");
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
runtimeEntityType.AddAnnotation("Relational:TableName", "tb_lights");
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); Customize(runtimeEntityType);
} static partial void Customize(RuntimeEntityType runtimeEntityType);
}

是不是和咱们前文中自己手写的很像?所以,实际开发中咱们是不用自己动手写的,用工具生成即可。

MyDbContextAssemblyAttributes.cs 文件有一个程序集级别的特性应用。

[assembly: DbContextModel(typeof(MyDbContext), typeof(MyDbContextModel))]

有了这个特性,EF Core 就能自己查找 RuntimeModel,不需要调用 DbContextOptionsBuilder 的 UseModel 方法来添加外部模型了。

顺便总结一下,EF Core 框架是按这个顺序查找模型的:

1、UseModel 方法指定的外部模型(请看上一篇水文),如果从选项类设置了,就用这个模型;

2、如果选项类没有用 UseModel 方法设置外部模型,那就找一下有没 dotnet-ef 工具生成的模型(就是咱们刚刚做的事)。如果有,就用它,原理是根据程序集上应用的 DbContextModelAttribute 特性找到生成的模型类,如本例中的 MyDbContextModel。接着查找模型类中名为 Instance 的静态属性,读取这个属性的值,就能获取模型实例了。显然,运行时模型实例是静态的,即只实例化一次。

可以看看源代码:

 static IModel? FindCompiledModel(Type contextType)
{
var contextAssembly = contextType.Assembly;
IModel? model = null;
foreach (var modelAttribute in contextAssembly.
GetCustomAttributes<DbContextModelAttribute>())
{
if (modelAttribute.ContextType != contextType)
{
continue;
}
var modelType = modelAttribute.ModelType;
var instanceProperty = modelType.GetProperty("Instance",
BindingFlags.Public | BindingFlags.Static);
if (instanceProperty == null
|| instanceProperty.PropertyType != typeof(IModel))
{
throw new InvalidOperationException(CoreStrings.
CompiledModelMissingInstance(modelType.DisplayName
()));
}
if (model != null)
{
throw new InvalidOperationException(
CoreStrings.CompiledModelDuplicateAttribute(
contextAssembly.FullName, contextType.
DisplayName()));
}
model = (IModel)instanceProperty.GetValue(null)!;
}
return model;
}

3、如果找不到 ef 工具生成的运行时模型,就调用内部模型构建,即调用 DbContext 的 OnModelCreating 方法以设计时形态构建模型。构建完毕后,通过预置约定 RuntimeModelConvention 生成 RuntimeModel。当然了,前面多次提到,如果要在代码中创建数据库或迁移,是不使用运行时模型的,而是直接跑 OnModelCreating 方法的配置代码,当然,预置约定也会全部跑一遍。

=========================================================================================

上周六,老周作为外三路辅助招聘大师,和两家企业的内部招聘人员交流。他们抱怨说现在的码农怎么回事,技术水平咋感觉一代不如一代,是不是现在工具太好用了,还有 AI 辅助,反而让他变菜了?就此老周也发表了自己的荒唐观点:

过度依赖 AI 以及其他工具并不是他们菜的原因,而是他们菜的结果。工具本身没啥,爱用就用,不爱用就不用,就是辅助的,真正干事的还是人类。但核心事件是——他们就是菜,从骨子里透出来的菜,而且,菜还不练!整天做出一副要改革职场的屌样。你们又不是缺人缺到项目写不动的程度,真要是那么缺人,要不分一点给我,我帮你做。既然招不到人项目还是能继续干的,那就不着急,总能找到不菜的人。我不相信国内的程序猿全都那么菜。

如果你想老周分析一下,现在很多码仔那么菜的原因,那对不起了,老周又要说你们不爱听的话了。那些人就是被当今网络环境忽悠成菜鸟的。如果你在十几年前关注过一些培训班,不管培训钢琴、古筝、编程啥的,它们很喜欢一句口号——“XXX 内速成”,比如一个月内速成,三个月内速成。有脑子都知道是P话,但架不住许多驴子和家长会相信。后来央视曝光过,现在“速成”二字很少见了。但是,这些骗子仍旧屎心不改,换个模式接着忽悠。于是出现了学 XXX 拿 YYY 万高薪。不出意外,也有许多驴子相信了。当初还有些大学森问过老周,报不报这些?老周说:“你学校没得学吗?图书馆地震了吗?没开课吗?何必呢?那些‘老师’估计比你们学校的老师还菜”。要是你听了它们的鬼话,估计连面试都过不了,还想什么高薪。

在网上曾看到过一个笑话,A说:上次有个哥们来我们这面试,问了一个 C++ 虚函数表的问题,那家伙直跑了。简历上还写着“精通C++”。然后下面,B网友说“那是北大青X出来的吧”。

现在,坑人模式升级了,不,准确来说没升级,2005 年前后网上就有这些货了,只是那时候没有短视频,也没那么多人上网。现在短视频里教别人编程的,老周可以不客气的说:全是坑人的(包括老周自己录的,也是忽悠你的)。

在老周心中,什么样的视频教程是合格的?如果各位和老周一样足够老的话,一定看过孙鑫老师的 C++ 视频,20集,时长惊人,含金量不用多说。说简单点,视频教程要达到这种层次,才是有观看价值的。你看看现在各种短视频里面,有这种水平的吗?

还是看书好,就算有些书内容不怎么样,但至少内容是相对全面系统化的,视频、博文、贴子可以作为辅助学习。

王经理说老周面试时太仁慈,只问些基础知识,从来不问项目经验。我就给王经理解释了一通。实际应用中,个个项目是不同的,就是有个别的书本会讲项目上的事,但对于实际工作中还是没多大意义的。工作经验是靠总结,每个人的想法都可能不一样,没办法作为知识点让你一条一条去学,学了也没用。其实项目经验这东西,把你放项目里面呆几个月基本就有经验了,不用学的,自然就会。

可是,技术和基础知识则不同,这些都必须去学的。你得先把这一关过了,项目上要安排你做什么你随机应变就行,你只要技术够硬,就马上就能想到这个东西要用什么解决。比如这里是不是要用 Web API 实现,那里是不是要用 SHAXXX 加密一下,那里是不是要用到压缩数据,某个窗口是不是要开后台线程处理,某个工序是不是要创建一个队列来排单……

要是你基础知识没学好,我叫你改一下那个页面,登录加密加个下拉表列,可以选 MD5、SHA1、SHA256 算法,然后提交前用对应的算法 Hash 一下密码。这里你完全可以用反射来做,.NET 的 MD5 等类,都有一个静态的 Create 方法……懂我的意思了吧。但是,如果你连反射是啥都不知道,自然不会想到这样解决,可能会用多 if 语句处理。虽说不是不行,但不如反射来得通用且简便。

说多了,其实就是现在很多人不愿意去实践,学了不去试,懒得动手,等真正要用的时候就不知所措了。

【EF Core】优化后的模型的更多相关文章

  1. EF Core中避免贫血模型的三种行之有效的方法(翻译)

    Paul Hiles: 3 ways to avoid an anemic domain model in EF Core 1.引言 在使用ORM中(比如Entity Framework)贫血领域模型 ...

  2. EF Core 简单使用介绍

    EF Core 是一个ORM(对象关系映射),它使 .NET 开发人员可以使用 .NET对象操作数据库,避免了像ADO.NET访问数据库的代码,开发者只需要编写对象即可. EF Core 支持多种数据 ...

  3. 【asp.net core 系列】8 实战之 利用 EF Core 完成数据操作层的实现

    0. 前言 通过前两篇,我们创建了一个项目,并规定了一个基本的数据层访问接口.这一篇,我们将以EF Core为例演示一下数据层访问接口如何实现,以及实现中需要注意的地方. 1. 添加EF Core 先 ...

  4. 【ASP.NET Core】EF Core 模型与数据库的创建

    大家好,欢迎收看由土星卫视直播的大型综艺节目——老周吹逼逼. 今天咱们吹一下 EF Core 有关的话题.先说说模型和数据库是怎么建起来的,说装逼一点,就是我们常说的 “code first”.就是你 ...

  5. ASP.NET Core 使用 SQLite 教程,EF SQLite教程,修改模型更新数据库,适合初学者看懂详细、简单教程

    SQLIte 操作方便,简单小巧,这里笔者就不再过多介绍,感兴趣可以到以下博文 https://blog.csdn.net/qq_31930499/article/details/80420246 文 ...

  6. EF core的模型映射

    在EF core里,可以通过实现IEntityTypeConfiguration来进行映射. 一.官网文档 https://docs.microsoft.com/en-us/ef/core/what- ...

  7. EF Core中,通过实体类向SQL Server数据库表中插入数据后,实体对象是如何得到数据库表中的默认值的

    我们使用EF Core的实体类向SQL Server数据库表中插入数据后,如果数据库表中有自增列或默认值列,那么EF Core的实体对象也会返回插入到数据库表中的默认值. 下面我们通过例子来展示,EF ...

  8. EF Core使用CodeFirst在MySql中创建新数据库以及已有的Mysql数据库如何使用DB First生成域模型

    官方教程:https://docs.microsoft.com/en-us/aspnet/core/data/?view=aspnetcore-2.1 使用EF CodeFirst在MySql中创建新 ...

  9. EF Core 配置模型

    0 前言 本文的第一节,会概述配置模型的作用(对数据模型的补充描述). 第二节描述两种配置方式,即:数据注释(data annotations)和 Fluent API 方式. 第三节开始,主要是将常 ...

  10. 【ASP.NET Core】EF Core - “影子属性” 深入浅出经典面试题:从浏览器中输入URL到页面加载发生了什么 - Part 1

    [ASP.NET Core]EF Core - “影子属性”   有朋友说老周近来博客更新较慢,确实有些慢,因为有些 bug 要研究,另外就是老周把部分内容转到直播上面,所以写博客的内容减少了一点. ...

随机推荐

  1. SAP PI 配置soap web service

    1.下载,启动ESB(Enterprise Services Builder) 2.找到对应的软件组件以及对应的命名空间: 3.选择对应的命名空间,右键新建文件夹: 点击创建,保存,文件夹对象右键激活 ...

  2. Java变量类型识别

    方法: 1.反射方式,成员变量的类型判断2.isInstance用法3.利用泛型识别类型一.新建测试类 import java.util.Date; import com.cxyapi.generic ...

  3. 开源直播课丨高效稳定易用的数据集成框架——ChunJun类加载原理与实现

    一.直播介绍 前几期,我们为大家分享了ChunJun的数据还原.Hive事务表及传输模块的一些内容,本期我们为大家分享ChunJun类加载原理与实现. 本次直播我们将从Java 类加载器解决类冲突基本 ...

  4. 十、buildroot系统 桌面配置

    4.4.桌面控制 4.4.1.weston 文件夹路径 /common/overlays/10-weston 1.核心设置 配置 Weston 的核心设置 文件 /etc/xdg/weston/wes ...

  5. Redis基于@Cacheable注解实现接口缓存

    说明 @Cacheable 注解在方法上,表示该方法的返回结果是可以缓存的.也就是说,该方法的返回结果会放在缓存中,以便于以后使用相同的参数调用该方法时,会返回缓存中的值,而不会实际执行该方法. 属性 ...

  6. 学习spring cloud记录12-Feign常用自定义配置

    前言 Feign被springboot自动装配,但是Feign也提供了可自定义修改配置,常用的修改是修改日志级别等,下面记录常用的配置. 知识 Feign常用自定义配置 类型 作用 说明 feign. ...

  7. java实现聊天,服务端与客户端代码(UDP)-狂神改

    首先是文件结构: 最后run的是下面两个 代码用的狂神的,不过他写的有点小bug,比如传信息会出现一堆空格(recieve data那里长度不应该用data.lenth()而应该用packet.get ...

  8. idea中运行java程序报Error:(4,25) --九五小庞

    报错信息如下 在网上找了一通,终于找到了解决的方法 参考网上教程安装lomobok插件,pom文件添加依赖之后 出现  Error:(3, 14) java: 程序包lombok不存在 错误 解决方案 ...

  9. SpringBoot启动原理(基于2.3.9.RELEASE版本)

    目录 版本 总体上 main方法上的注解:@SpringBootApplication 源码 @SpringBootConfiguration @ComponentScan @EnableAutoCo ...

  10. GROOVY 列表

    列表的基本操作: import java.io.File class Example { static void main(String[] args){ def list1 = [1,2]; def ...