找回密码
 立即注册
首页 业界区 业界 【EF Core】带主键实体与无主键实体

【EF Core】带主键实体与无主键实体

党新苗 昨天 21:16
上一次老周已介绍了 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、创建一个控制台应用。
  1. dotnet new console -n Demo -o .
复制代码
有伙伴会问:这个用 Copilot 能不能执行?可以,比如这样:
1.png

它生成的命令少了 -o . ,你可以手动补上。
2.png

如果你不想它自动执行命令,那不要点“继续”,复制命令文本后,点“取消”就好。若继续,它会直接执行命令。
尽管可以这样用,但这样做特愚蠢!你直接打个命令都比这个快了。写实体类的时候,如果你不想重复敲 get 和 set,倒可以用它辅助。当然,VS其实也会提示的,你按个 Tab 就会生成了。这个东西虽然好用,但有时候也挺烦的,按个 Tab 就出一堆东西(如果不想禁掉它,可以按 Esc 键取消提示)。如果你真不想用它,可以到设置里面找到【文本编辑器】-【建议】,去掉 Inline Suggest: Enabled 的选项即可。
3.png

 
2、定义实体类。这次咱们用一个 Pet 类,表示你家的宠物。
  1. public class Pet
  2. {
  3.     public int id { get; set; }
  4.     public string Name { get; set; }
  5.     public int Age { get; set; }
  6.     public string Cate { get; set; }
  7. }
复制代码
这个你倒可以用辅助工具写。注意这里老周故意把标识属性改为小写,即 id,而不是 Id。待会咱们看看 EF 能不能识别。
3、为项目添加包。
  1. dotnet add package Microsoft.EntityFrameworkCore
  2. dotnet add package Microsoft.EntityFrameworkCore.Sqlite
复制代码
4、写数据上下文类。
  1. public class MyDbContext : DbContext
  2. {
  3.     public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
  4.     {
  5.     }
  6.     public DbSet<Pet> Pets { get; set; }
  7. }
复制代码
5、这一次咱们的连接字符串不在 MyDbContext 内部配置,而是外部构建 Options 来配置。
  1. // 创建选项类实例
  2. DbContextOptions<MyDbContext> options = new DbContextOptionsBuilder<MyDbContext>()
  3.     .<strong>UseSqlite(</strong><strong>"Data Source=mydb.db"</strong><strong>)</strong>
  4.     .Options;
  5. // 实例化 DbContext
  6. using var dc = new MyDbContext(options);
复制代码
6、这个例子中,咱们不创建数据库,只是验证一下,全小写的 id 属性是否能被识别为主键。
  1. /*
  2.     此处我们不创建数据库,只是看看它能不能识别出主键
  3. */
  4. // 获取实体列表
  5. foreach (var ent in dc.Model.GetEntityTypes())
  6. {
  7.     Console.Write($"表名: {ent.GetTableName()}");
  8.     // 查找主键
  9.     var rmykey = ent.FindPrimaryKey();
  10.     if (rmykey != null)
  11.     {
  12.         Console.WriteLine($",主键: {string.Join(", ", rmykey.Properties.Select(p => p.Name))}");
  13.     }
  14.     // 实体中的列(属性)
  15.     foreach (var property in ent.GetProperties())
  16.     {
  17.         Console.WriteLine($"\t列名: {property.Name} 类型: {property.ClrType.Name}");
  18.     }
  19. }
复制代码
dc.Model.GetEntityTypes 方法能够返回模型中所有实体的信息。GetTableName 返回实体对应的数据表名,FindPrimaryKey 方法找出此实体类的主键。最后,GetProperties 方法获取实体类属性对应的列。
上述代码运行后得到的结果如下:
  1. 表名: Pets,主键: id
  2.     列名: id 类型: Int32
  3.     列名: Age 类型: Int32
  4.     列名: Cate 类型: String
  5.     列名: Name 类型: String
复制代码
好,看来,小写的 id 属性是可以被识别为主键的(老周不再分析 EF Core 源代码了,不然这博文就没人看了,其实是通过约定实现的)。同理,我们还可以验证一下,全小写的 petid 能不能识别。把 Pet 类改为:
  1. public class Pet
  2. {
  3.     public int petid { get; set; }
  4.     ……
  5. }
复制代码
至少咱们知道,PetId 是肯定能被识别为主键的,现在验证一下全小写的id的属性。再次运行程序,得到:
  1. 表名: Pets,主键: <strong>petid</strong>
  2.     列名: petid 类型: Int32
  3.     列名: Age 类型: Int32
  4.     列名: Cate 类型: String
  5.     列名: Name 类型: String<br>
复制代码
这个示例证明:Id 和 Id 都能被约定识别为主键,并且不区分大小写
那么,如果属性的名称不是 Id 呢,比如这样改:
  1. public class Pet
  2. {
  3.     public int <strong>BugId </strong>{ get; set; }
  4.     public string Name { get; set; } = string.Empty;
  5.     public int Age { get; set; }
  6.     public string? Cate { get; set; }
  7. }
复制代码
再次运行一下,结果不出所料。
4.png

这段鸟语说了啥?它说 Pet 这厮必须定义主键,如果你不想要主键,那得明确地把实体配置为无主键。怎么配置为无主键咱们后文再说,现在先说说“预制菜”约定无法自动识别出主键,咱们如何手动配置。
1、简单做法,用特性在 BugId 属性上批注一下。
  1. <strong>[PrimaryKey(nameof(BugId))]
  2. </strong>public class Pet
  3. {
  4.     ……
  5. }
复制代码
这种方法最简单,但老周个人不推荐,因为不集中配置,不好管理。当然,只是老周不推荐,没说不可以用啊。
2、通过 ModelBuilder 来配置,这个在 DbContext 的派生类中重写 OnModelCreating 方法。
  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3.      modelBuilder.Entity<Pet>().<strong>HasKey(x =></strong><strong> x.BugId)</strong>;
  4. }
复制代码
老周比较推荐这种方法,因为它把所有实体的配置全集中一处,将来有改动也好搞,也不容易忘这个忘那个的。两种方法任选其一,不需要同时用。
再次运行程序,看到想要的结果了。
  1. 表名: Pets,<strong>主键: BugId</strong>
  2.     列名: BugId 类型: Int32
  3.     列名: Age 类型: Int32
  4.     列名: Cate 类型: String
  5.     列名: Name 类型: String
复制代码
 
有时候你可能会想:我的代码中并不需要访问主键,主键仅留给 EF 自己用于生成 SQL 语句,那我能不能把影子属性作为主键呢?答案是 Yes 的。先简单说说影子属性(Shadow Property)是什么,一句话斯基:你的实体类中未定义的,但模型中定义了的属性
同理,你的 DbContext 子类需要重写 OnModelCreating 方法。
  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3.      // 这一行很重要
  4.      modelBuilder.Entity<Pet>().<strong>Property(typeof(int), "HideId"</strong><strong>)</strong>;
  5.      // 设置主键
  6.      modelBuilder.Entity<Pet>()
  7.          .HasKey("HideId");
  8. }
复制代码
Pet 类可以去掉作为主键的属性。
  1. public class Pet
  2. {
  3.     public string Name { get; set; } = string.Empty;
  4.     public int Age { get; set; }
  5.     public string? Cate { get; set; }
  6. }
复制代码
由于影子属性在实体类未定义,EF Core 并不能确定其类型能不能成为主键。因此,在定义主键前应该让 EF 知道作为主键的影子属性是支持的类型,如 int。
  1. modelBuilder.Entity<Pet>().Property(typeof(int), "HideId");
复制代码
上面的例子就是把影子属性 HideId 作为 Pet 实体的主键。运行结果如下:
  1. 表名: Pets,<strong>主键: HideId</strong>
  2.     列名: HideId 类型: Int32
  3.     列名: Age 类型: Int32
  4.     列名: Cate 类型: String
  5.     列名: Name 类型: String
复制代码
主键也可以由多个属性(列)组成。比如,咱们让 HideId 和 Name 组成主键。
  1. protected override void OnModelCreating(ModelBuilder modelBuilder) {     // 这一行很重要     modelBuilder.Entity<Pet>().Property(typeof(int), "HideId");     // 设置主键     modelBuilder.Entity()         .HasKey([b]"HideId"[/b][b], nameof(Pet.Name)[/b]); }
复制代码
得到的运行结果如下:
  1. 表名: Pets,<strong>主键: HideId, Name</strong>
  2.     列名: HideId 类型: Int32
  3.     列名: Name 类型: String
  4.     列名: Age 类型: Int32
  5.     列名: Cate 类型: String
复制代码
 
下面咱们演示一下把 string 类型的属性映射到 SQL Server 数据表的 unique identifier 列。
1、用以下 SQL 脚本(使用的是 SQL Server)创建数据库和数据表。
  1. -- 创建数据库
  2. CREATE DATABASE Test;
  3. GO
  4. -- 切换到刚刚创建的数据库
  5. USE Test;
  6. GO
  7. -- 创建表
  8. CREATE TABLE Productions (
  9.     -- 这个是主键,插入时如果未提供值,则用 NEWID() 产生的值
  10.     Pid UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
  11.     -- 产品名称
  12.     ProdName NVARCHAR(40) NOT NULL,
  13.     -- 生产年份
  14.     Year INT,
  15.     -- 产品尺寸
  16.     Size DECIMAL(6,2),
  17.     -- 产品颜色
  18.     Color NVARCHAR(10),
  19.     -- 备注
  20.     Remark NVARCHAR(MAX)
  21. );
复制代码
2、创建控制台 .NET 项目(此处省略250个字)。
3、定义实体类(这个可以用 dotnet ef dbcontext 命令生成,不过老周一向习惯纯手写,生成的实体类有时候要回头修改)。
  1. public class Production
  2. {
  3.     public <strong>string Pid {</strong> get; set; } = null!;
  4.     public string ProdName { get; set; } = string.Empty;
  5.     public int? Year { get; set; }
  6.     public decimal? Size { get; set; }
  7.     public string? Color { get; set; }
  8.     public string? Remark { get; set; }
  9. }
复制代码
Pid 属性要作为主键用的,注意这里老周故意让其默认值为 null,这样在 EF 上下文添加实体时使用数据库生成的值(否则会报错)。null 后面有个感叹号(!)这个可以避免编译器的 Nullable 警告,具体情况你可以找微软官方文档,有详细说明。就是微软文档写得太好了,导致很多基础知识老周都不必重复介绍了。
4、派生 DbContext 的子类。
  1. public class MyContext : DbContext
  2. {
  3.     public DbSet<Production> Productions { get; set; }
  4.     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  5.     {
  6.         // 配置连接字符串
  7.         optionsBuilder.UseSqlServer(@"Server=(localdb)\MSSQLLocalDB;Database=Test;Trusted_Connection=True");
  8.     }
  9.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  10.     {
  11.         modelBuilder.<strong>Entity</strong><strong><Production></strong>(entbd =>
  12.         {
  13.             entbd.Property(p => p.Pid)
  14.                  .ValueGeneratedOnAdd();
  15.             // 产品名称为必须,且有长度限制
  16.             entbd.Property(p => p.ProdName)
  17.                  .IsRequired()
  18.                  .HasMaxLength(40);
  19.             // 产品尺寸的精度要求
  20.             entbd.Property(p => p.Size)
  21.                  .HasPrecision(6, 2);
  22.             // 产品颜色的长度限制
  23.             entbd.Property(p => p.Color)
  24.                  .HasMaxLength(10);
  25.             // 主键
  26.             entbd.HasKey(p => p.Pid);
  27.         });
  28.     }
  29. }
复制代码
ModelBuilder 的 Entity 方法可以获得一个 EntityTypeBuilder 对象(上面老周是调用了带 Action 委托的重载,方便多次调用 EntityTypeBuilder 实例的成员)。EntityTypeBuilder 类内部封装了 InternalEntityTypeBuilder 对象,各种配置方法实际调用了此 InternalEntityTypeBuilder 对象的成员。
5、在 Program.cs 文件中,写一下测试代码。咱们向数据库存入两条记录。
  1. // 实例化上下文
  2. MyContext dc = new MyContext();
  3. // 新建两条记录
  4. Production p1 = new()
  5. {
  6.     ProdName = "五角裤",
  7.     Year = 2025,
  8.     Size = 67.33m,
  9.     Color = "白色",
  10.     Remark = "纯化学合成纤维,无自然成份"
  11. };
  12. Production p2 = new()
  13. {
  14.     ProdName = "无领衬衫",
  15.     Year = 2025,
  16.     Size = 47.00m,
  17.     Color = "黄色",
  18.     Remark = "冰丝,炎炎夏日,如同把冰块披在身上"
  19. };
  20. dc.Productions.AddRange(p1, p2);
  21. // 保存到数据库
  22. dc.SaveChanges();
  23. dc.Dispose();
复制代码
如果代码顺利运行,则数据库中就有两条新记录了。
  1. select * from dbo.Productions
复制代码
5.png

当然了,对于 UNIQUE IDENTIFIER 类型的主键,.NET CLR 实体类的属性除了可以用字符串类型,也可以用 Guid 类型。原理也是一样的,这里老周就不演示了,相信大伙伴们都会的。
===========================================================================================================
 接下来看看无主键的实体。这个其实没什么特别的知识要掌握的,但你得记住一条:无主键的实体只能 SELECT,不能用于 INSERT、UPDATE、DELETE 操作。一句话斯基总结就是:只能查询不能更新
咱们还是整个例子吧。
1、用以下SQL脚本创建数据库和数据表。
  1. create database DemoSome;
  2. GO
  3. use DemoSome;
  4. GO
  5. -- 创建表
  6. create table HandsomeBoys
  7. (
  8.     BoyID int IDENTITY,
  9.     [Name] NVARCHAR(25) not null,
  10.     Age int,
  11.     City NVARCHAR(10),
  12.     PhoneNo NVARCHAR(11),
  13.     Email NVARCHAR(40),
  14.     CONSTRAINT [PK_HandsomeBoys] PRIMARY KEY CLUSTERED (BoyID ASC)
  15. );
  16. GO
复制代码
2、向数据表 INSERT 几条数据用于测试,随便写,略。
3、创建.NET控制台应用程序,略。
4、定义实体类(可以用 dotnet ef 工具生成,也可以纯手打)。
  1. public class HandsomBoy
  2. {
  3.     public int ID { get; set; }
  4.     public string Name { get; set; } = string.Empty;
  5.     public string? Email { get; set; }
  6.     public string? City { get; set; }
  7.     public int? Age { get; set; }
  8.     public string? PhoneNo { get; set; }
  9. }
复制代码
5、写数据库上下文类,构建模型。
  1. public class DemoDB : DbContext
  2. {
  3.     public DemoDB(DbContextOptions<DemoDB> options)
  4.             :base(options) { }
  5.     public DbSet<HandsomBoy> HandsomeBoys { get; set; }
  6.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  7.     {
  8.         // 无主键
  9.        <strong> modelBuilder.Entity<HandsomBoy>().HasNoKey();
  10. </strong>        // 表映射
  11.         var entbd = modelBuilder.Entity<HandsomBoy>().ToTable("HandsomeBoys");
  12.         // 属性映射
  13.         entbd.Property(p => p.ID).HasColumnName("BoyID");
  14.         entbd.Property(p => p.Name)
  15.             .IsRequired()
  16.             .HasMaxLength(25);
  17.         entbd.Property(p => p.City).HasMaxLength(10);
  18.         entbd.Property(p => p.Email).HasMaxLength(40);
  19.         entbd.Property(p => p.PhoneNo).HasMaxLength(11);
  20.     }
  21. }
复制代码
注意要调用 HasNoKey 方法配置实体为无主键,不然会报错。
6、测试。
  1. // 配置选项
  2. DbContextOptions<DemoDB> options = new DbContextOptionsBuilder<DemoDB>()
  3.         .UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=DemoSome;Trusted_Connection=True")
  4.         // 记录日志
  5.         .LogTo(msg => Console.WriteLine(msg))
  6.         .EnableSensitiveDataLogging()
  7.         .Options;
  8. using var dc = new DemoDB(options);
  9. // 查询数据
  10. var q = from b in dc.HandsomeBoys
  11.         select b;
  12. foreach (var x in q)
  13. {
  14.     Console.WriteLine($"ID={x.ID}, Name={x.Name}, Age={x.Age}, City={x.City}, Phone={x.PhoneNo}");
  15. }
复制代码
结果如下:
  1. ID=1, Name=小陈, Age=35, City=珠海, Phone=15562021200
  2. ID=2, Name=老周, Age=105, City=东莞, Phone=13888582588
  3. ID=3, Name=老丁, Age=45, City=中山, Phone=15840991234
复制代码
无主键实体也可以用特性批注。
  1. <strong>[Keyless]
  2. </strong>public class HandsomBoy
  3. {
  4.     ……
  5. }
复制代码
两种方法,二选一。
好了,今天就聊到这儿了。
 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册