上一次老周已介绍了 EF Core 框架自动发现实体和实体成员的原理。涉及到对源码的分析,可能大伙伴们都看得气压升高了。故这一次老周不带各位去分析源码了,咱们聊一聊熟悉又陌生的关键词——主键。说它熟悉,是因为只要咱们创建数据表,99%会用到;说它陌生,是指在 EF Core 中与主键相关的细节。
Primary Key,翻译为“主键”(这个翻译老周没意见,但 Thread 翻译成“线程”感觉莫名其妙)。按其命名,即是一张表中主要的键,用于表明某行记录在表中是唯一的。有大伙伴会说,那 Unique 约束也可以啊。是的,但还要有一个条件,就是不能为空值,所以,可以说主键是 UNIQUE 和 NOT NULL 的结合。
数据表的主键可以是一列,也可以是多列。
好了,概念说完了,咱们说回 EF。按照预置的约定(老周上一文中介绍),将属性发现为主键的原则有:
1、属性名为 Id;
2、属性名为实体类名 + Id,如 ProductId、OrderId 等。
最常用的类型是 int,自动增长。也可以用 GUID,GUID 属性的类型可以定义为 Guid,也可以是 string。老周,有例子吗?有,咱们玩几个,咱们使用 Sqlite 数据库来演示。
1、创建一个控制台应用。- dotnet new console -n Demo -o .
复制代码 有伙伴会问:这个用 Copilot 能不能执行?可以,比如这样:
它生成的命令少了 -o . ,你可以手动补上。
如果你不想它自动执行命令,那不要点“继续”,复制命令文本后,点“取消”就好。若继续,它会直接执行命令。
尽管可以这样用,但这样做特愚蠢!你直接打个命令都比这个快了。写实体类的时候,如果你不想重复敲 get 和 set,倒可以用它辅助。当然,VS其实也会提示的,你按个 Tab 就会生成了。这个东西虽然好用,但有时候也挺烦的,按个 Tab 就出一堆东西(如果不想禁掉它,可以按 Esc 键取消提示)。如果你真不想用它,可以到设置里面找到【文本编辑器】-【建议】,去掉 Inline Suggest: Enabled 的选项即可。
2、定义实体类。这次咱们用一个 Pet 类,表示你家的宠物。- public class Pet
- {
- public int id { get; set; }
- public string Name { get; set; }
- public int Age { get; set; }
- public string Cate { get; set; }
- }
复制代码 这个你倒可以用辅助工具写。注意这里老周故意把标识属性改为小写,即 id,而不是 Id。待会咱们看看 EF 能不能识别。
3、为项目添加包。- dotnet add package Microsoft.EntityFrameworkCore
- dotnet add package Microsoft.EntityFrameworkCore.Sqlite
复制代码 4、写数据上下文类。- public class MyDbContext : DbContext
- {
- public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
- {
- }
- public DbSet<Pet> Pets { get; set; }
- }
复制代码 5、这一次咱们的连接字符串不在 MyDbContext 内部配置,而是外部构建 Options 来配置。- // 创建选项类实例
- DbContextOptions<MyDbContext> options = new DbContextOptionsBuilder<MyDbContext>()
- .<strong>UseSqlite(</strong><strong>"Data Source=mydb.db"</strong><strong>)</strong>
- .Options;
- // 实例化 DbContext
- using var dc = new MyDbContext(options);
复制代码 6、这个例子中,咱们不创建数据库,只是验证一下,全小写的 id 属性是否能被识别为主键。- /*
- 此处我们不创建数据库,只是看看它能不能识别出主键
- */
- // 获取实体列表
- foreach (var ent in dc.Model.GetEntityTypes())
- {
- Console.Write($"表名: {ent.GetTableName()}");
- // 查找主键
- var rmykey = ent.FindPrimaryKey();
- if (rmykey != null)
- {
- Console.WriteLine($",主键: {string.Join(", ", rmykey.Properties.Select(p => p.Name))}");
- }
- // 实体中的列(属性)
- foreach (var property in ent.GetProperties())
- {
- Console.WriteLine($"\t列名: {property.Name} 类型: {property.ClrType.Name}");
- }
- }
复制代码 dc.Model.GetEntityTypes 方法能够返回模型中所有实体的信息。GetTableName 返回实体对应的数据表名,FindPrimaryKey 方法找出此实体类的主键。最后,GetProperties 方法获取实体类属性对应的列。
上述代码运行后得到的结果如下:- 表名: Pets,主键: id
- 列名: id 类型: Int32
- 列名: Age 类型: Int32
- 列名: Cate 类型: String
- 列名: Name 类型: String
复制代码 好,看来,小写的 id 属性是可以被识别为主键的(老周不再分析 EF Core 源代码了,不然这博文就没人看了,其实是通过约定实现的)。同理,我们还可以验证一下,全小写的 petid 能不能识别。把 Pet 类改为:- public class Pet
- {
- public int petid { get; set; }
- ……
- }
复制代码 至少咱们知道,PetId 是肯定能被识别为主键的,现在验证一下全小写的id的属性。再次运行程序,得到:- 表名: Pets,主键: <strong>petid</strong>
- 列名: petid 类型: Int32
- 列名: Age 类型: Int32
- 列名: Cate 类型: String
- 列名: Name 类型: String<br>
复制代码 这个示例证明:Id 和 Id 都能被约定识别为主键,并且不区分大小写。
那么,如果属性的名称不是 Id 呢,比如这样改:- public class Pet
- {
- public int <strong>BugId </strong>{ get; set; }
- public string Name { get; set; } = string.Empty;
- public int Age { get; set; }
- public string? Cate { get; set; }
- }
复制代码 再次运行一下,结果不出所料。
这段鸟语说了啥?它说 Pet 这厮必须定义主键,如果你不想要主键,那得明确地把实体配置为无主键。怎么配置为无主键咱们后文再说,现在先说说“预制菜”约定无法自动识别出主键,咱们如何手动配置。
1、简单做法,用特性在 BugId 属性上批注一下。- <strong>[PrimaryKey(nameof(BugId))]
- </strong>public class Pet
- {
- ……
- }
复制代码 这种方法最简单,但老周个人不推荐,因为不集中配置,不好管理。当然,只是老周不推荐,没说不可以用啊。
2、通过 ModelBuilder 来配置,这个在 DbContext 的派生类中重写 OnModelCreating 方法。- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity<Pet>().<strong>HasKey(x =></strong><strong> x.BugId)</strong>;
- }
复制代码 老周比较推荐这种方法,因为它把所有实体的配置全集中一处,将来有改动也好搞,也不容易忘这个忘那个的。两种方法任选其一,不需要同时用。
再次运行程序,看到想要的结果了。- 表名: Pets,<strong>主键: BugId</strong>
- 列名: BugId 类型: Int32
- 列名: Age 类型: Int32
- 列名: Cate 类型: String
- 列名: Name 类型: String
复制代码
有时候你可能会想:我的代码中并不需要访问主键,主键仅留给 EF 自己用于生成 SQL 语句,那我能不能把影子属性作为主键呢?答案是 Yes 的。先简单说说影子属性(Shadow Property)是什么,一句话斯基:你的实体类中未定义的,但模型中定义了的属性。
同理,你的 DbContext 子类需要重写 OnModelCreating 方法。- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- // 这一行很重要
- modelBuilder.Entity<Pet>().<strong>Property(typeof(int), "HideId"</strong><strong>)</strong>;
- // 设置主键
- modelBuilder.Entity<Pet>()
- .HasKey("HideId");
- }
复制代码 Pet 类可以去掉作为主键的属性。- public class Pet
- {
- public string Name { get; set; } = string.Empty;
- public int Age { get; set; }
- public string? Cate { get; set; }
- }
复制代码 由于影子属性在实体类未定义,EF Core 并不能确定其类型能不能成为主键。因此,在定义主键前应该让 EF 知道作为主键的影子属性是支持的类型,如 int。- modelBuilder.Entity<Pet>().Property(typeof(int), "HideId");
复制代码 上面的例子就是把影子属性 HideId 作为 Pet 实体的主键。运行结果如下:- 表名: Pets,<strong>主键: HideId</strong>
- 列名: HideId 类型: Int32
- 列名: Age 类型: Int32
- 列名: Cate 类型: String
- 列名: Name 类型: String
复制代码 主键也可以由多个属性(列)组成。比如,咱们让 HideId 和 Name 组成主键。- protected override void OnModelCreating(ModelBuilder modelBuilder) { // 这一行很重要 modelBuilder.Entity<Pet>().Property(typeof(int), "HideId"); // 设置主键 modelBuilder.Entity() .HasKey([b]"HideId"[/b][b], nameof(Pet.Name)[/b]); }
复制代码 得到的运行结果如下:- 表名: Pets,<strong>主键: HideId, Name</strong>
- 列名: HideId 类型: Int32
- 列名: Name 类型: String
- 列名: Age 类型: Int32
- 列名: Cate 类型: String
复制代码
下面咱们演示一下把 string 类型的属性映射到 SQL Server 数据表的 unique identifier 列。
1、用以下 SQL 脚本(使用的是 SQL Server)创建数据库和数据表。- -- 创建数据库
- CREATE DATABASE Test;
- GO
- -- 切换到刚刚创建的数据库
- USE Test;
- GO
- -- 创建表
- CREATE TABLE Productions (
- -- 这个是主键,插入时如果未提供值,则用 NEWID() 产生的值
- Pid UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
- -- 产品名称
- ProdName NVARCHAR(40) NOT NULL,
- -- 生产年份
- Year INT,
- -- 产品尺寸
- Size DECIMAL(6,2),
- -- 产品颜色
- Color NVARCHAR(10),
- -- 备注
- Remark NVARCHAR(MAX)
- );
复制代码 2、创建控制台 .NET 项目(此处省略250个字)。
3、定义实体类(这个可以用 dotnet ef dbcontext 命令生成,不过老周一向习惯纯手写,生成的实体类有时候要回头修改)。- public class Production
- {
- public <strong>string Pid {</strong> get; set; } = null!;
- public string ProdName { get; set; } = string.Empty;
- public int? Year { get; set; }
- public decimal? Size { get; set; }
- public string? Color { get; set; }
- public string? Remark { get; set; }
- }
复制代码 Pid 属性要作为主键用的,注意这里老周故意让其默认值为 null,这样在 EF 上下文添加实体时使用数据库生成的值(否则会报错)。null 后面有个感叹号(!)这个可以避免编译器的 Nullable 警告,具体情况你可以找微软官方文档,有详细说明。就是微软文档写得太好了,导致很多基础知识老周都不必重复介绍了。
4、派生 DbContext 的子类。- public class MyContext : DbContext
- {
- public DbSet<Production> Productions { get; set; }
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- // 配置连接字符串
- optionsBuilder.UseSqlServer(@"Server=(localdb)\MSSQLLocalDB;Database=Test;Trusted_Connection=True");
- }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.<strong>Entity</strong><strong><Production></strong>(entbd =>
- {
- entbd.Property(p => p.Pid)
- .ValueGeneratedOnAdd();
- // 产品名称为必须,且有长度限制
- entbd.Property(p => p.ProdName)
- .IsRequired()
- .HasMaxLength(40);
- // 产品尺寸的精度要求
- entbd.Property(p => p.Size)
- .HasPrecision(6, 2);
- // 产品颜色的长度限制
- entbd.Property(p => p.Color)
- .HasMaxLength(10);
- // 主键
- entbd.HasKey(p => p.Pid);
- });
- }
- }
复制代码 ModelBuilder 的 Entity 方法可以获得一个 EntityTypeBuilder 对象(上面老周是调用了带 Action 委托的重载,方便多次调用 EntityTypeBuilder 实例的成员)。EntityTypeBuilder 类内部封装了 InternalEntityTypeBuilder 对象,各种配置方法实际调用了此 InternalEntityTypeBuilder 对象的成员。
5、在 Program.cs 文件中,写一下测试代码。咱们向数据库存入两条记录。- // 实例化上下文
- MyContext dc = new MyContext();
- // 新建两条记录
- Production p1 = new()
- {
- ProdName = "五角裤",
- Year = 2025,
- Size = 67.33m,
- Color = "白色",
- Remark = "纯化学合成纤维,无自然成份"
- };
- Production p2 = new()
- {
- ProdName = "无领衬衫",
- Year = 2025,
- Size = 47.00m,
- Color = "黄色",
- Remark = "冰丝,炎炎夏日,如同把冰块披在身上"
- };
- dc.Productions.AddRange(p1, p2);
- // 保存到数据库
- dc.SaveChanges();
- dc.Dispose();
复制代码 如果代码顺利运行,则数据库中就有两条新记录了。- select * from dbo.Productions
复制代码
当然了,对于 UNIQUE IDENTIFIER 类型的主键,.NET CLR 实体类的属性除了可以用字符串类型,也可以用 Guid 类型。原理也是一样的,这里老周就不演示了,相信大伙伴们都会的。
===========================================================================================================
接下来看看无主键的实体。这个其实没什么特别的知识要掌握的,但你得记住一条:无主键的实体只能 SELECT,不能用于 INSERT、UPDATE、DELETE 操作。一句话斯基总结就是:只能查询不能更新。
咱们还是整个例子吧。
1、用以下SQL脚本创建数据库和数据表。- create database DemoSome;
- GO
- use DemoSome;
- GO
- -- 创建表
- create table HandsomeBoys
- (
- BoyID int IDENTITY,
- [Name] NVARCHAR(25) not null,
- Age int,
- City NVARCHAR(10),
- PhoneNo NVARCHAR(11),
- Email NVARCHAR(40),
- CONSTRAINT [PK_HandsomeBoys] PRIMARY KEY CLUSTERED (BoyID ASC)
- );
- GO
复制代码 2、向数据表 INSERT 几条数据用于测试,随便写,略。
3、创建.NET控制台应用程序,略。
4、定义实体类(可以用 dotnet ef 工具生成,也可以纯手打)。- public class HandsomBoy
- {
- public int ID { get; set; }
- public string Name { get; set; } = string.Empty;
- public string? Email { get; set; }
- public string? City { get; set; }
- public int? Age { get; set; }
- public string? PhoneNo { get; set; }
- }
复制代码 5、写数据库上下文类,构建模型。- public class DemoDB : DbContext
- {
- public DemoDB(DbContextOptions<DemoDB> options)
- :base(options) { }
- public DbSet<HandsomBoy> HandsomeBoys { get; set; }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- // 无主键
- <strong> modelBuilder.Entity<HandsomBoy>().HasNoKey();
- </strong> // 表映射
- var entbd = modelBuilder.Entity<HandsomBoy>().ToTable("HandsomeBoys");
- // 属性映射
- entbd.Property(p => p.ID).HasColumnName("BoyID");
- entbd.Property(p => p.Name)
- .IsRequired()
- .HasMaxLength(25);
- entbd.Property(p => p.City).HasMaxLength(10);
- entbd.Property(p => p.Email).HasMaxLength(40);
- entbd.Property(p => p.PhoneNo).HasMaxLength(11);
- }
- }
复制代码 注意要调用 HasNoKey 方法配置实体为无主键,不然会报错。
6、测试。- // 配置选项
- DbContextOptions<DemoDB> options = new DbContextOptionsBuilder<DemoDB>()
- .UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=DemoSome;Trusted_Connection=True")
- // 记录日志
- .LogTo(msg => Console.WriteLine(msg))
- .EnableSensitiveDataLogging()
- .Options;
- using var dc = new DemoDB(options);
- // 查询数据
- var q = from b in dc.HandsomeBoys
- select b;
- foreach (var x in q)
- {
- Console.WriteLine($"ID={x.ID}, Name={x.Name}, Age={x.Age}, City={x.City}, Phone={x.PhoneNo}");
- }
复制代码 结果如下:- ID=1, Name=小陈, Age=35, City=珠海, Phone=15562021200
- ID=2, Name=老周, Age=105, City=东莞, Phone=13888582588
- ID=3, Name=老丁, Age=45, City=中山, Phone=15840991234
复制代码 无主键实体也可以用特性批注。- <strong>[Keyless]
- </strong>public class HandsomBoy
- {
- ……
- }
复制代码 两种方法,二选一。
好了,今天就聊到这儿了。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |