在使用EF Core和設計數據庫的時候,通常一對多、多對多關系使用得比較多,但是一對一關系使用得就比較少了。最近我發現實際上EF Core很好地支持了數據庫的一對一關系。
數據庫
我們先來看看SQL Server數據庫中的表:
Person表代表的是一個人,表中有些字段來簡單描述一個人,其建表語句如下:
CREATE TABLE [dbo].[Person]( [ID] [int] IDENTITY(1,1) NOT NULL, [PersonCode] [nvarchar](50) NULL, [Name] [nvarchar](50) NULL, [Age] [int] NULL, [City] [nvarchar](50) NULL, [CreateTime] [datetime] NULL, CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY], CONSTRAINT [IX_Person] UNIQUE NONCLUSTERED ( [PersonCode] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[Person] ADD CONSTRAINT [DF_Person_CreateTime] DEFAULT (getdate()) FOR [CreateTime] GO
從上面可以看出,除了主鍵ID外,我們還設置了列PersonCode為唯一鍵IX_Person。
然后數據庫中還有張表IdentificationCard,其代表的是一個人的身份證,其中列IdentificationNo是身份證號碼,其建表語句如下:
CREATE TABLE [dbo].[IdentificationCard]( [ID] [int] IDENTITY(1,1) NOT NULL, [IdentificationNo] [nvarchar](50) NULL, [PersonCode] [nvarchar](50) NULL, [CreateTime] [datetime] NULL, CONSTRAINT [PK_IdentificationCard] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY], CONSTRAINT [IX_IdentificationCard] UNIQUE NONCLUSTERED ( [PersonCode] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[IdentificationCard] ADD CONSTRAINT [DF_IdentificationCard_CreateTime] DEFAULT (getdate()) FOR [CreateTime] GO ALTER TABLE [dbo].[IdentificationCard] WITH CHECK ADD CONSTRAINT [FK_IdentificationCard_Person] FOREIGN KEY([PersonCode]) REFERENCES [dbo].[Person] ([PersonCode]) ON UPDATE CASCADE ON DELETE CASCADE GO ALTER TABLE [dbo].[IdentificationCard] CHECK CONSTRAINT [FK_IdentificationCard_Person] GO
其中設置外鍵關系FK_IdentificationCard_Person:通過IdentificationCard表的PersonCode列來關聯Person表的PersonCode列,從而指明一張身份證屬於哪個Person。
然后我們同樣設置了IdentificationCard表的PersonCode列為唯一鍵IX_IdentificationCard,這樣外鍵FK_IdentificationCard_Person表示的實際上就是一對一關系了,因為IdentificationCard表的一行數據通過列PersonCode只能找到一行Person表數據,而現在IdentificationCard表的PersonCode列又是唯一鍵,所以反過來Person表在IdentificationCard表中最多也只能找到一行數據,所以這是個典型的一對一關系。
我們還在FK_IdentificationCard_Person外鍵關系上使用了CASCADE設置了級聯刪除和級聯更新。
EF Core實體
接着我們新建了一個.NET Core控制台項目,使用EF Core的Scaffold-DbContext指令自動從數據庫中生成實體,可以看到通過我們在數據庫中設置的唯一鍵和外鍵,EF Core自動識別出了Person表和IdentificationCard表之間是一對一關系,生成的代碼如下:
Person實體,對應的是數據庫中的Person表,注意其中包含一個屬性IdentificationCard,表示Person表和IdentificationCard表的一對一關系:
using System; using System.Collections.Generic; namespace FFCoreOneToOne.Entities { /// <summary> /// Person實體,對應數據庫中的Person表,可以看到其中有一個IdentificationCard屬性,表示Person實體對應一個IdentificationCard實體 /// </summary> public partial class Person { public int Id { get; set; } public string PersonCode { get; set; } public string Name { get; set; } public int? Age { get; set; } public string City { get; set; } public DateTime? CreateTime { get; set; } public IdentificationCard IdentificationCard { get; set; } } }
IdentificationCard實體,對應的是數據庫中的IdentificationCard表,注意其中包含一個屬性PersonCodeNavigation,表示IdentificationCard表和Person表的一對一關系:
using System; using System.Collections.Generic; namespace FFCoreOneToOne.Entities { /// <summary> /// IdentificationCard實體,對應數據庫中的IdentificationCard表,可以看到其中有一個PersonCodeNavigation屬性,表示IdentificationCard實體對應一個Person實體 /// </summary> public partial class IdentificationCard { public int Id { get; set; } public string IdentificationNo { get; set; } public string PersonCode { get; set; } public DateTime? CreateTime { get; set; } public Person PersonCodeNavigation { get; set; } } }
最后是Scaffold-DbContext指令生成的DbContext類TestDBContext,其中比較重要的地方是OnModelCreating方法中,設置IdentificationCard實體和Person實體間一對一關系的Fluent API代碼,我用注釋詳細闡述了每一步的含義:
using System; using FFCoreOneToOne.Logger; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; namespace FFCoreOneToOne.Entities { public partial class TestDBContext : DbContext { public TestDBContext() { } public TestDBContext(DbContextOptions<TestDBContext> options) : base(options) { } public virtual DbSet<IdentificationCard> IdentificationCard { get; set; } public virtual DbSet<Person> Person { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer("Server=localhost;User Id=sa;Password=1qaz!QAZ;Database=TestDB"); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<IdentificationCard>(entity => { entity.HasIndex(e => e.PersonCode) .HasName("IX_IdentificationCard") .IsUnique(); entity.Property(e => e.Id).HasColumnName("ID"); entity.Property(e => e.CreateTime) .HasColumnType("datetime") .HasDefaultValueSql("(getdate())"); entity.Property(e => e.IdentificationNo).HasMaxLength(50); entity.Property(e => e.PersonCode).HasMaxLength(50); //設置IdentificationCard實體和Person實體的一對一關系 entity.HasOne(d => d.PersonCodeNavigation)//HasOne設置IdentificationCard實體中有一個Person實體,可以通過IdentificationCard實體的PersonCodeNavigation屬性訪問到 .WithOne(p => p.IdentificationCard)//WithOne設置Person實體中有一個IdentificationCard實體,可以通過Person實體的IdentificationCard屬性訪問到 .HasPrincipalKey<Person>(p => p.PersonCode)//設置數據庫中Person表的PersonCode列是一對一關系的主表鍵 .HasForeignKey<IdentificationCard>(d => d.PersonCode)//設置數據庫中IdentificationCard表的PersonCode列是一對一關系的從表外鍵 .OnDelete(DeleteBehavior.Cascade)//由於我們在數據庫中開啟了IdentificationCard表外鍵FK_IdentificationCard_Person的級聯刪除,所以這里也生成了實體級聯刪除的Fluent API .HasConstraintName("FK_IdentificationCard_Person");//設置IdentificationCard實體和Person實體的一對一關系采用的是數據庫外鍵FK_IdentificationCard_Person }); modelBuilder.Entity<Person>(entity => { entity.HasIndex(e => e.PersonCode) .HasName("IX_Person") .IsUnique(); entity.Property(e => e.Id).HasColumnName("ID"); entity.Property(e => e.City).HasMaxLength(50); entity.Property(e => e.CreateTime) .HasColumnType("datetime") .HasDefaultValueSql("(getdate())"); entity.Property(e => e.Name).HasMaxLength(50); entity.Property(e => e.PersonCode) .IsRequired() .HasMaxLength(50); }); } } }
示例代碼
接着我們在.NET Core控制台項目的Program類中定義了些示例代碼,其中AddPersonWithIdentificationCard和AddIdentificationCardWithPerson方法使用DbContext來添加數據到數據庫,RemoveIdentificationCardFromPerson和RemovePersonFromIdentificationCard方法用來演示如何通過實體的導航屬性來刪除數據,最后DeleteAllPersons是清表語句,刪除數據庫中IdentificationCard表和Person表的所有數據。
這里先把示例代碼全部貼出來:
using FFCoreOneToOne.Entities; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; namespace FFCoreOneToOne { class Program { /// <summary> /// 刪除數據庫Person表和IdentificationCard表的所有數據 /// </summary> static void DeleteAllPersons() { using (TestDBContext dbContext = new TestDBContext()) { dbContext.Database.ExecuteSqlCommand("DELETE FROM [dbo].[IdentificationCard]"); dbContext.Database.ExecuteSqlCommand("DELETE FROM [dbo].[Person]"); } } /// <summary> /// 通過添加Person來添加IdentificationCard /// </summary> static void AddPersonWithIdentificationCard() { //通過添加Person實體來添加IdentificationCard實體,將Person實體的IdentificationCard屬性設置為對應的IdentificationCard實體即可 using (TestDBContext dbContext = new TestDBContext()) { var james = new Person() { Name = "James", Age = 30, PersonCode = "P001", City = "Beijing" }; james.IdentificationCard = new IdentificationCard() { IdentificationNo = "510100197512305607" }; var tom = new Person() { Name = "Tom", Age = 35, PersonCode = "P002", City = "Shanghai" }; tom.IdentificationCard = new IdentificationCard() { IdentificationNo = "510100197512305609" }; var sam = new Person() { Name = "Sam", Age = 25, PersonCode = "P003", City = "Chongqing" }; sam.IdentificationCard = new IdentificationCard() { IdentificationNo = "510100197512305605" }; dbContext.Person.Add(james); dbContext.Person.Add(tom); dbContext.Person.Add(sam); dbContext.SaveChanges(); } } /// <summary> /// 通過添加IdentificationCard來添加Person,從EF Core的日志中可以看到使用這種方式還是先執行的插入Person表數據的SQL,再執行的插入IdentificationCard表數據的SQL /// </summary> static void AddIdentificationCardWithPerson() { //通過添加IdentificationCard實體來添加Person實體,將IdentificationCard實體的PersonCodeNavigation屬性設置為對應的Person實體即可 using (TestDBContext dbContext = new TestDBContext()) { var jamesCard = new IdentificationCard() { IdentificationNo = "510100197512305607" }; jamesCard.PersonCodeNavigation = new Person() { Name = "James", Age = 30, PersonCode = "P001", City = "Beijing" }; var tomCard = new IdentificationCard() { IdentificationNo = "510100197512305609" }; tomCard.PersonCodeNavigation = new Person() { Name = "Tom", Age = 35, PersonCode = "P002", City = "Shanghai" }; var samCard = new IdentificationCard() { IdentificationNo = "510100197512305605" }; samCard.PersonCodeNavigation = new Person() { Name = "Sam", Age = 25, PersonCode = "P003", City = "Chongqing" }; dbContext.IdentificationCard.Add(jamesCard); dbContext.IdentificationCard.Add(tomCard); dbContext.IdentificationCard.Add(samCard); dbContext.SaveChanges(); } } /// <summary> /// 通過設置Person實體的IdentificationCard屬性為null來刪除IdentificationCard表的數據 /// </summary> static void RemoveIdentificationCardFromPerson() { //先用DbContext從數據庫中查詢出Person實體,然后設置其IdentificationCard屬性為null,來刪除IdentificationCard表的數據 //注意在查詢Person實體的時候,記得要用EF Core中Eager Loading的Include方法也查詢出IdentificationCard實體,這樣我們在設置Person實體的IdentificationCard屬性為null后,DbContext才能跟蹤到變更,才會在下面調用DbContext.SaveChanges方法時,生成刪除IdentificationCard表數據的SQL語句 using (TestDBContext dbContext = new TestDBContext()) { var james = dbContext.Person.Include(e => e.IdentificationCard).First(e => e.Name == "James"); james.IdentificationCard = null; var tom = dbContext.Person.Include(e => e.IdentificationCard).First(e => e.Name == "Tom"); tom.IdentificationCard = null; var sam = dbContext.Person.Include(e => e.IdentificationCard).First(e => e.Name == "Sam"); sam.IdentificationCard = null; dbContext.SaveChanges(); } } /// <summary> /// 本來這個方法是想用來通過設置IdentificationCard實體的PersonCodeNavigation屬性為null,來刪除Person表的數據,但是結果是還是刪除的IdentificationCard表數據 /// </summary> static void RemovePersonFromIdentificationCard() { //原本我想的是,先用DbContext從數據庫中查詢出IdentificationCard實體,並用EF Core中Eager Loading的Include方法也查詢出Person實體,然后設置IdentificationCard實體的PersonCodeNavigation屬性為null,來刪除Person表的數據 //結果這樣做EF Core最后還是刪除的IdentificationCard表的數據,原因是IdentificationCard表是一對一外鍵關系的從表,設置從表實體的外鍵屬性PersonCodeNavigation為null,EF Core認為的是從表的數據作廢,所以刪除了從表IdentificationCard中的數據,主表Person的數據還在。。。 using (TestDBContext dbContext = new TestDBContext()) { var jamesCard = dbContext.IdentificationCard.Include(e => e.PersonCodeNavigation).First(e => e.IdentificationNo == "510100197512305607"); jamesCard.PersonCodeNavigation = null; var tomCard = dbContext.IdentificationCard.Include(e => e.PersonCodeNavigation).First(e => e.IdentificationNo == "510100197512305609"); tomCard.PersonCodeNavigation = null; var samCard = dbContext.IdentificationCard.Include(e => e.PersonCodeNavigation).First(e => e.IdentificationNo == "510100197512305605"); samCard.PersonCodeNavigation = null; dbContext.SaveChanges(); } } static void Main(string[] args) { DeleteAllPersons(); AddPersonWithIdentificationCard(); AddIdentificationCardWithPerson(); RemoveIdentificationCardFromPerson(); RemovePersonFromIdentificationCard(); Console.WriteLine("Press any key to quit..."); Console.ReadKey(); } } }
AddPersonWithIdentificationCard
首先我們測試AddPersonWithIdentificationCard方法,其通過添加Person實體到數據庫來添加IdentificationCard表的數據,更改Main方法的代碼如下,並執行程序:
static void Main(string[] args) { DeleteAllPersons(); AddPersonWithIdentificationCard(); //AddIdentificationCardWithPerson(); //RemoveIdentificationCardFromPerson(); //RemovePersonFromIdentificationCard(); Console.WriteLine("Press any key to quit..."); Console.ReadKey(); }
執行后數據庫中Person表的數據如下:

IdentificationCard表的數據如下:

AddIdentificationCardWithPerson
然后我們測試AddIdentificationCardWithPerson方法,其通過添加IdentificationCard實體到數據庫來添加Person表的數據,從EF Core的日志中可以看到使用這種方式還是先執行的插入Person表數據的SQL,再執行的插入IdentificationCard表數據的SQL。更改Main方法的代碼如下,並執行程序:
static void Main(string[] args) { DeleteAllPersons(); //AddPersonWithIdentificationCard(); AddIdentificationCardWithPerson(); //RemoveIdentificationCardFromPerson(); //RemovePersonFromIdentificationCard(); Console.WriteLine("Press any key to quit..."); Console.ReadKey(); }
執行后數據庫中Person表的數據如下:

IdentificationCard表的數據如下:

RemoveIdentificationCardFromPerson
然后我們測試RemoveIdentificationCardFromPerson方法,其通過設置Person實體的IdentificationCard屬性為null,來刪除IdentificationCard表的數據,更改Main方法的代碼如下,並執行程序:
static void Main(string[] args) { DeleteAllPersons(); AddPersonWithIdentificationCard(); //AddIdentificationCardWithPerson(); RemoveIdentificationCardFromPerson(); //RemovePersonFromIdentificationCard(); Console.WriteLine("Press any key to quit..."); Console.ReadKey(); }
執行后數據庫中Person表的數據如下:

IdentificationCard表的數據如下:

RemovePersonFromIdentificationCard
最后我們測試RemovePersonFromIdentificationCard方法,本來這個方法我是設計用來通過設置IdentificationCard實體的PersonCodeNavigation屬性為null,來刪除Person表的數據,但是測試后發現結果還是刪除的IdentificationCard表的數據,原因可以看下上面示例代碼中RemovePersonFromIdentificationCard方法中的注釋。更改Main方法的代碼如下,並執行程序:
static void Main(string[] args) { DeleteAllPersons(); AddPersonWithIdentificationCard(); //AddIdentificationCardWithPerson(); //RemoveIdentificationCardFromPerson(); RemovePersonFromIdentificationCard(); Console.WriteLine("Press any key to quit..."); Console.ReadKey(); }
執行后數據庫中Person表的數據如下:

IdentificationCard表的數據如下:

