EF Core中如何正確地設置兩張表之間的關聯關系


數據庫


假設現在我們在SQL Server數據庫中有下面兩張表:

Person表,代表的是一個人:

CREATE TABLE [dbo].[Person](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [PersonCode] [nvarchar](20) NULL,
    [Name] [nvarchar](50) NULL,
    [Age] [int] 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

其主鍵是ID,而且主鍵是自增列。Person表還有個PersonCode列是唯一鍵,然后Name和Age列用來描述一個人的名字和年齡。

 

Book表,代表的是一本書:

CREATE TABLE [dbo].[Book](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [BookCode] [nvarchar](20) NULL,
    [PersonCode] [nvarchar](20) NULL,
    [BookName] [nvarchar](50) NULL,
    [ISBN] [nvarchar](20) NULL,
 CONSTRAINT [PK_Book] 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_Book] UNIQUE NONCLUSTERED 
(
    [BookCode] 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

其主鍵是ID,而且主鍵也是自增列。Book表的BookCode列是唯一鍵,Book表的PersonCode列引用Person表的PersonCode列值,所以Book表的PersonCode列實際上是外鍵,但是我們並沒有在數據庫中設置兩張表之間的外鍵關系,我們將稍后在EF Core中的實體之間設置外鍵關系,來演示就算在數據庫中沒有設置外鍵,EF Core也可以設置實體之間的外鍵關系。 所以Person表和Book表實際上是一對多關系,通過兩張表的PersonCode列,一個Person對應多個Book,表示一個人可以擁有多本書。Book表還有BookName列和ISBN列,分別用來記錄一本書的書名和ISBN號碼。

 

 

實體


新建一個.NET Core控制台項目,現在我們在EF Core中建立Person表和Book表的實體:

 

Person實體,對應數據庫的Person表,其屬性Book是一個ICollection<Book>類型的Book實體集合,表示一個Person實體包含多個Book實體:

public partial class Person
{
    public int Id { get; set; }
    public string PersonCode { get; set; }
    public string Name { get; set; }
    public int? Age { get; set; }

    //通過Person實體的Book屬性,可以找到多個Book實體,說明Person表是一對多關系中的主表
    public virtual ICollection<Book> Book { get; set; }
}

 

Book實體,對應數據庫的Book表,其屬性Person是一個Person實體,表示一個Book實體只能找到一個Person實體:

public partial class Book
{
    public int Id { get; set; }
    public string BookCode { get; set; }
    public string PersonCode { get; set; }
    public string BookName { get; set; }
    public string Isbn { get; set; }

    //通過Book實體的Person屬性,可以找到一個Person實體,說明Book表是一對多關系中的從表
    public virtual Person Person { get; set; }
}

 

然后是繼承DbContext的TestDBContext類,其中最重要的地方是OnModelCreating方法中設置Person實體和Book實體一對多關系的Fluent API,每一行都寫明了注釋:

public partial class TestDBContext : DbContext
{
    public TestDBContext()
    {
    }

    public TestDBContext(DbContextOptions<TestDBContext> options)
        : base(options)
    {
    }

    public virtual DbSet<Book> Book { 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=Dtt!123456;Database=TestDB");
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>(entity =>
        {
            entity.HasKey(e => e.BookCode);//設置Book實體的BookCode屬性為EF Core實體的Key屬性

            entity.HasIndex(e => e.BookCode)
                .HasName("IX_Book")
                .IsUnique();

            entity.Property(e => e.Id).ValueGeneratedOnAdd();//設置Book實體的Id屬性為插入數據到數據庫Book表時自動生成,因為Book表的ID列為自增列
            entity.Property(e => e.Id).HasColumnName("ID");

            entity.Property(e => e.BookCode).HasMaxLength(20);

            entity.Property(e => e.BookName).HasMaxLength(50);

            entity.Property(e => e.Isbn)
                .HasColumnName("ISBN")
                .HasMaxLength(20);

            entity.Property(e => e.PersonCode).HasMaxLength(20);
        });

        modelBuilder.Entity<Person>(entity =>
        {
            entity.HasKey(e => e.PersonCode);//設置Person實體的PersonCode屬性為EF Core實體的Key屬性

            entity.HasIndex(e => e.PersonCode)
                .HasName("IX_Person")
                .IsUnique();

            entity.Property(e => e.Id).ValueGeneratedOnAdd();//設置Person實體的Id屬性為插入數據到數據庫Person表時自動生成,因為Person表的ID列為自增列
            entity.Property(e => e.Id).HasColumnName("ID");

            entity.Property(e => e.Name).HasMaxLength(50);

            entity.Property(e => e.PersonCode).HasMaxLength(20);

            //設置Person實體和Book實體之間的一對多關系,盡管我們並沒有在數據庫中建立Person表和Book表之間的一對多外鍵關系,但是我們可以用EF Core的Fluent API在實體層面設置外鍵關系
            entity.HasMany(p => p.Book)//設置Person實體通過屬性Book可以找到多個Book實體,表示Person表是一對多關系中的主表
            .WithOne(b => b.Person)//設置Book實體通過屬性Person可以找到一個Person實體,表示Book表是一對多關系中的從表
            .HasPrincipalKey(p => p.PersonCode)//設置Person表的PersonCode列為一對多關系中的主表鍵
            .HasForeignKey(b => b.PersonCode)//設置Book表的PersonCode列為一對多關系中的從表外鍵
            .OnDelete(DeleteBehavior.ClientSetNull);//設置一對多關系的級聯刪除效果為DeleteBehavior.ClientSetNull

        });
    }
}

 

 

示例代碼


現在我們來設想下面一個場景:

假設數據庫中的Person表有一行數據如下:

 

數據庫中的Book表有三行數據如下:

 

可以看到Book表三行數據的PersonCode列都為NULL,那么我們怎么在EF Core中更改Book表三行數據的PersonCode列為Person表的PersonCode列值呢?也就是說將Book表三行數據的PersonCode列都改為Person表的值P001,從而表示James這個人擁有三本書。

 

本例的示例代碼都寫在了.NET Core控制台項目的Program類中,這里先將代碼全部貼出來:

class Program
{
    /// <summary>
    /// 初始化Person表和Book表的數據,沒有設置Book表的外鍵列PersonCode的值
    /// </summary>
    static void InitData()
    {
        //初始化數據庫數據
        using (var dbContext = new TestDBContext())
        {
            var james = new Person() { PersonCode = "P001", Name = "James", Age = 30 };

            dbContext.Person.Add(james);

            var chineseBook = new Book() { BookCode = "B001", Isbn = "001", BookName = "Chinese" };//沒有設置Book表中外鍵列PersonCode的值
            var japaneseBook = new Book() { BookCode = "B002", Isbn = "001", BookName = "Japanese" };//沒有設置Book表中外鍵列PersonCode的值
            var englishBook = new Book() { BookCode = "B003", Isbn = "001", BookName = "English" };//沒有設置Book表中外鍵列PersonCode的值

            //插入三條數據到Book表
            dbContext.Book.Add(chineseBook);
            dbContext.Book.Add(japaneseBook);
            dbContext.Book.Add(englishBook);

            dbContext.SaveChanges();
        }
    }

    /// <summary>
    /// 刪除Person表和Book表的所有數據
    /// </summary>
    static void DeleteAllData()
    {
        using (var dbContext = new TestDBContext())
        {
            dbContext.Database.ExecuteSqlCommand("DELETE FROM [dbo].[Book]");
            dbContext.Database.ExecuteSqlCommand("DELETE FROM [dbo].[Person]");
        }
    }

    /// <summary>
    /// 不正確地設置Person表和Book表的關聯關系,這種方法會讓EF Core錯誤地生成INSERT語句,而不是UPDATE語句
    /// </summary>
    static void SetRelationshipIncorrectly()
    {
        using (var dbContext = new TestDBContext())
        {
            var james = dbContext.Person.First(e => e.Name == "James");//首先通過DbContext從數據庫中查詢出要建立關聯關系的Person表實體

            var chineseBook = new Book() { BookCode = "B001" };//只構造Book實體的Key屬性即可,根據BookCode值"B001"來構造Chinese Book
            var japaneseBook = new Book() { BookCode = "B002" };//只構造Book實體的Key屬性即可,根據BookCode值"B002"來構造Japanese Book
            var englishBook = new Book() { BookCode = "B003" };//只構造Book實體的Key屬性即可,根據BookCode值"B003"來構造English Book

            Console.WriteLine($"Before adding, chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//可以看到由於此時Book實體chineseBook沒有被DbContext跟蹤,所以狀態是Detached
            Console.WriteLine($"Before adding, japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//可以看到由於此時Book實體japaneseBook沒有被DbContext跟蹤,所以狀態是Detached
            Console.WriteLine($"Before adding, englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//可以看到由於此時Book實體englishBook沒有被DbContext跟蹤,所以狀態是Detached

            Console.WriteLine();
                
            james.Book = new List<Book>();//由於我們在上面調用dbContext.Person.First(e => e.Name == "James")時,沒有用EF Core中Eager Loading的Include方法來加載Book實體集合,所以這里要用List類來構造一個Book實體集合,否則james.Book為null

            james.Book.Add(chineseBook);//添加chineseBook到Person類的Book實體集合
            Console.WriteLine("chineseBook was added into Person.Book collection");

            james.Book.Add(japaneseBook);//添加japaneseBook到Person類的Book實體集合
            Console.WriteLine("japaneseBook was added into Person.Book collection");

            james.Book.Add(englishBook);//添加englishBook到Person類的Book實體集合
            Console.WriteLine("englishBook was added into Person.Book collection");

            Console.WriteLine();

            Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//調用DbContext.Entry()方法后,DbContext發現一個原本狀態是Detached的Book實體chineseBook被加入到Person.Book集合中了,所以此時chineseBook的實體狀態變為了Added
            Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//調用DbContext.Entry()方法后,DbContext發現一個原本狀態是Detached的Book實體japaneseBook被加入到Person.Book集合中了,所以此時japaneseBook的實體狀態變為了Added
            Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//調用DbContext.Entry()方法后,DbContext發現一個原本狀態是Detached的Book實體englishBook被加入到Person.Book集合中了,所以此時englishBook的實體狀態變為了Added

            dbContext.SaveChanges();//由於此時chineseBook、japaneseBook和englishBook的EntityState都是Added,所以此時DbContext.SaveChanges方法調用后,EF Core生成的是INSERT語句,將chineseBook、japaneseBook和englishBook插入數據庫表Book,導致插入了重復值到唯一鍵列BookCode,所以數據庫報錯
        }
    }

    /// <summary>
    /// 正確地設置Person表和Book表的關聯關系,這種方法會讓EF Core正確地生成UPDATE語句,在數據庫中設置Book表的PersonCode列數據
    /// </summary>
    static void SetRelationshipCorrectly()
    {
        using (var dbContext = new TestDBContext())
        {
            var james = dbContext.Person.First(e => e.Name == "James");//首先通過DbContext從數據庫中查詢出要建立關聯關系的Person表實體

            var chineseBook = new Book() { BookCode = "B001" };//只構造Book實體的Key屬性即可,根據BookCode值"B001"來構造Chinese Book
            var japaneseBook = new Book() { BookCode = "B002" };//只構造Book實體的Key屬性即可,根據BookCode值"B002"來構造Japanese Book
            var englishBook = new Book() { BookCode = "B003" };//只構造Book實體的Key屬性即可,根據BookCode值"B003"來構造English Book

            dbContext.Attach(chineseBook);//將chineseBook關聯到DbContext,開始跟蹤
            dbContext.Attach(japaneseBook);//將japaneseBook關聯到DbContext,開始跟蹤
            dbContext.Attach(englishBook);//將englishBook關聯到DbContext,開始跟蹤

            Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//由於上面chineseBook被Attach到DbContext開始跟蹤了,所以此時chineseBook的實體狀態是Unchanged
            Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//由於上面japaneseBook被Attach到DbContext開始跟蹤了,所以此時japaneseBook的實體狀態是Unchanged
            Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//由於上面englishBook被Attach到DbContext開始跟蹤了,所以此時englishBook的實體狀態是Unchanged

            Console.WriteLine();

            james.Book = new List<Book>();//由於我們在上面調用dbContext.Person.First(e => e.Name == "James")時,沒有用EF Core中Eager Loading的Include方法來加載Book實體集合,所以這里要用List類來構造一個Book實體集合,否則james.Book為null

            james.Book.Add(chineseBook);//添加chineseBook到Person類的Book實體集合
            Console.WriteLine("chineseBook was added into Person.Book collection");

            james.Book.Add(japaneseBook);//添加japaneseBook到Person類的Book實體集合
            Console.WriteLine("japaneseBook was added into Person.Book collection");

            james.Book.Add(englishBook);//添加englishBook到Person類的Book實體集合
            Console.WriteLine("englishBook was added into Person.Book collection");

            Console.WriteLine();

            Console.WriteLine($"Berfore querying DbContext.Entry(chineseBook), chineseBook.PersonCode is :{chineseBook.PersonCode ?? "null"}");//此時由於我們還沒有調用DbContext.Entry()方法,所以DbContext還無法察覺到chineseBook已經被添加到Person類的Book實體集合了,所以chineseBook.PersonCode為null
            Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//調用DbContext.Entry()方法后,DbContext發現一個原本狀態是Unchanged的Book實體chineseBook被加入到Person.Book集合中了,所以此時chineseBook的實體狀態變為了Modified
            Console.WriteLine($"After querying DbContext.Entry(chineseBook),  chineseBook.PersonCode is :{chineseBook.PersonCode}");//由於上面我們調用DbContext.Entry(chineseBook)使得DbContext得知了chineseBook被加入到Person.Book集合中了,所以DbContext還將Book實體的外鍵屬性PersonCode也進行了賦值,為P001

            Console.WriteLine();

            Console.WriteLine($"Berfore querying DbContext.Entry(japaneseBook),japaneseBook.PersonCode is :{japaneseBook.PersonCode ?? "null"}");//很有意思的是我們上面在chineseBook上調用DbContext.Entry()方法后,japaneseBook的PersonCode屬性也不為null了,變為了P001,說明調用一次DbContext.Entry()方法后,會引發DbContext重新檢查所有被跟蹤實體的狀態
            Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//在上面為chineseBook調用DbContext.Entry()方法時,DbContext同時發現了原本狀態是Unchanged的Book實體japaneseBook,也被加入到了Person.Book集合中,所以japaneseBook的實體狀態也變為了Modified
            Console.WriteLine($"After querying DbContext.Entry(japaneseBook),  japaneseBook.PersonCode is :{japaneseBook.PersonCode}");//在上面為chineseBook調用DbContext.Entry()方法時,DbContext得知了japaneseBook也被加入到了Person.Book集合中,所以DbContext將japaneseBook的PersonCode屬性也賦值為P001了

            Console.WriteLine();

            Console.WriteLine($"Berfore querying DbContext.Entry(englishBook),englishBook.PersonCode is :{englishBook.PersonCode ?? "null"}");//在上面為chineseBook調用DbContext.Entry()方法時,DbContext得知了englishBook也被加入到了Person.Book集合中,所以DbContext將englishBook的PersonCode屬性也賦值為P001了
            Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//在上面為chineseBook調用DbContext.Entry()方法時,DbContext同時發現了原本狀態是Unchanged的Book實體englishBook,也被加入到了Person.Book集合中,所以englishBook的實體狀態也變為了Modified
            Console.WriteLine($"After querying DbContext.Entry(englishBook),  englishBook.PersonCode is :{englishBook.PersonCode}");//在上面為chineseBook調用DbContext.Entry()方法時,DbContext得知了englishBook也被加入到了Person.Book集合中,所以DbContext將englishBook的PersonCode屬性也賦值為P001了

            dbContext.SaveChanges();//由於此時chineseBook、japaneseBook和englishBook的EntityState都是Modified,所以此時DbContext.SaveChanges方法調用后,EF Core生成的是UPDATE語句,通過更新數據庫Book表的PersonCode列,將Chinese、Japanese和English三行Book數據同Person表的數據成功關聯了起來
        }
    }


    static void Main(string[] args)
    {
        DeleteAllData();//調用DeleteAllData方法刪除Person表和Book表的所有數據,防止有臟數據
        InitData();//初始化Person表和Book表的數據

        SetRelationshipCorrectly();//正確地的設置Person表和Book表的關聯關系
        SetRelationshipIncorrectly();//不正確地的設置Person表和Book表的關聯關系,該方法會拋出異常錯誤

        Console.WriteLine("Press any key to quit...");
        Console.ReadKey();
    }
}

 

示例代碼中的DeleteAllData方法,是清表語句,用來刪除Person表和Book表的所有數據,防止有臟數據。

InitData方法用來初始化Person表和Book表的數據,Person表插入了一行數據,Book表插入了三行數據且PersonCode列都為NULL,調用InitData方法后數據庫Person表和Book表的數據就和上面示例代碼前的兩個截圖相同了。

 

 

測試SetRelationshipIncorrectly方法

先將示例代碼的Main方法改為如下:

static void Main(string[] args)
{
    DeleteAllData();//調用DeleteAllData方法刪除Person表和Book表的所有數據,防止有臟數據
    InitData();//初始化Person表和Book表的數據

    //SetRelationshipCorrectly();//正確地的設置Person表和Book表的關聯關系
    SetRelationshipIncorrectly();//不正確地的設置Person表和Book表的關聯關系,該方法會拋出異常錯誤

    Console.WriteLine("Press any key to quit...");
    Console.ReadKey();
}

 

SetRelationshipIncorrectly方法用來演示怎么錯誤地設置Person表和Book表的關聯關系,可以看到由於我們在其中新建的三個Book實體

var chineseBook = new Book() { BookCode = "B001" };//只構造Book實體的Key屬性即可,根據BookCode值"B001"來構造Chinese Book
var japaneseBook = new Book() { BookCode = "B002" };//只構造Book實體的Key屬性即可,根據BookCode值"B002"來構造Japanese Book
var englishBook = new Book() { BookCode = "B003" };//只構造Book實體的Key屬性即可,根據BookCode值"B003"來構造English Book

最終在調用DbContext.SaveChanges方法時其實體狀態都是Added,所以調用DbContext.SaveChanges方法時,EF Core在數據庫中生成的是INSERT語句,嘗試將這三個實體數據插入數據庫Book表,由於調用InitData方法后,數據庫Book表中已經有相同PersonCode列值的數據了,Book表的PersonCode列又是唯一鍵,所以DbContext.SaveChanges方法拋出異常。

我們可以從EF Core的后台日志中,查看到調用DbContext.SaveChanges方法時生成的是INSERT語句:

=============================== EF Core log started ===============================
SaveChanges starting for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
DetectChanges starting for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
DetectChanges completed for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Opening connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Opened connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Beginning transaction with isolation level 'ReadCommitted'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executing update commands individually as the number of batchable commands (3) is smaller than the minimum batch size (4).
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executing DbCommand [Parameters=[@p0='?' (Size = 20), @p1='?' (Size = 50), @p2='?' (Size = 20), @p3='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Book] ([BookCode], [BookName], [ISBN], [PersonCode])
VALUES (@p0, @p1, @p2, @p3);
SELECT [ID]
FROM [Book]
WHERE @@ROWCOUNT = 1 AND [BookCode] = @p0;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Failed executing DbCommand (8ms) [Parameters=[@p0='?' (Size = 20), @p1='?' (Size = 50), @p2='?' (Size = 20), @p3='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Book] ([BookCode], [BookName], [ISBN], [PersonCode])
VALUES (@p0, @p1, @p2, @p3);
SELECT [ID]
FROM [Book]
WHERE @@ROWCOUNT = 1 AND [BookCode] = @p0;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Disposing transaction.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Closing connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Closed connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================

在SetRelationshipIncorrectly方法中我們還輸出了chineseBook、japaneseBook和englishBook三個Book實體的EntityState,可以看到將chineseBook、japaneseBook和englishBook三個Book實體添加到Person類的Book實體集合后,EntityState發生了相應的變化。

 

 

測試SetRelationshipCorrectly方法

先將示例代碼的Main方法改為如下:

static void Main(string[] args)
{
    DeleteAllData();//調用DeleteAllData方法刪除Person表和Book表的所有數據,防止有臟數據
    InitData();//初始化Person表和Book表的數據

    SetRelationshipCorrectly();//正確地的設置Person表和Book表的關聯關系
    //SetRelationshipIncorrectly();//不正確地的設置Person表和Book表的關聯關系,該方法會拋出異常錯誤

    Console.WriteLine("Press any key to quit...");
    Console.ReadKey();
}

 

SetRelationshipCorrectly方法用來演示如何正確地設置Person表和Book表的關聯關系,首先我們將

var chineseBook = new Book() { BookCode = "B001" };//只構造Book實體的Key屬性即可,根據BookCode值"B001"來構造Chinese Book
var japaneseBook = new Book() { BookCode = "B002" };//只構造Book實體的Key屬性即可,根據BookCode值"B002"來構造Japanese Book
var englishBook = new Book() { BookCode = "B003" };//只構造Book實體的Key屬性即可,根據BookCode值"B003"來構造English Book

這三個新建的Book實體Attach到了DbContext,如下所示:

dbContext.Attach(chineseBook);//將chineseBook關聯到DbContext,開始跟蹤
dbContext.Attach(japaneseBook);//將japaneseBook關聯到DbContext,開始跟蹤
dbContext.Attach(englishBook);//將englishBook關聯到DbContext,開始跟蹤

所以這次最終在調用DbContext.SaveChanges方法時其實體狀態都是Modified,那么調用DbContext.SaveChanges方法時,EF Core在數據庫中生成的是UPDATE語句,正確地將chineseBook、japaneseBook和englishBook這三個Book實體的PersonCode列值更新到了數據庫Book表中,我們可以從EF Core的后台日志中,查看到調用DbContext.SaveChanges方法時生成的是UPDATE語句:

=============================== EF Core log started ===============================
SaveChanges starting for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
DetectChanges starting for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
DetectChanges completed for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Opening connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Opened connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Beginning transaction with isolation level 'ReadCommitted'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executing update commands individually as the number of batchable commands (3) is smaller than the minimum batch size (4).
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executing DbCommand [Parameters=[@p1='?' (Size = 20), @p0='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Book] SET [PersonCode] = @p0
WHERE [BookCode] = @p1;
SELECT @@ROWCOUNT;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executed DbCommand (4ms) [Parameters=[@p1='?' (Size = 20), @p0='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Book] SET [PersonCode] = @p0
WHERE [BookCode] = @p1;
SELECT @@ROWCOUNT;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
A data reader was disposed.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executing DbCommand [Parameters=[@p1='?' (Size = 20), @p0='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Book] SET [PersonCode] = @p0
WHERE [BookCode] = @p1;
SELECT @@ROWCOUNT;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executed DbCommand (4ms) [Parameters=[@p1='?' (Size = 20), @p0='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Book] SET [PersonCode] = @p0
WHERE [BookCode] = @p1;
SELECT @@ROWCOUNT;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
A data reader was disposed.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executing DbCommand [Parameters=[@p1='?' (Size = 20), @p0='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Book] SET [PersonCode] = @p0
WHERE [BookCode] = @p1;
SELECT @@ROWCOUNT;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executed DbCommand (4ms) [Parameters=[@p1='?' (Size = 20), @p0='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Book] SET [PersonCode] = @p0
WHERE [BookCode] = @p1;
SELECT @@ROWCOUNT;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
A data reader was disposed.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Committing transaction.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Closing connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Closed connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Disposing transaction.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
An 'Book' entity tracked by 'TestDBContext' changed from 'Modified' to 'Unchanged'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see key values.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
An 'Book' entity tracked by 'TestDBContext' changed from 'Modified' to 'Unchanged'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see key values.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
An 'Book' entity tracked by 'TestDBContext' changed from 'Modified' to 'Unchanged'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see key values.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
SaveChanges completed for 'TestDBContext' with 3 entities written to the database.
=============================== EF Core log finished ===============================

最終數據庫中Person表和Book表的數據如下所示:

Person表

Book表

在SetRelationshipCorrectly方法中我們也輸出了chineseBook、japaneseBook和englishBook三個Book實體的EntityState,可以看到這三個Book實體的EntityState發生的變化

 

 

DbContext何時會檢查被跟蹤實體的狀態?

從目前得知的情況,調用DbContext.Entry方法和DbContext.SaveChanges方法時,都會觸發DbContext對所有被跟蹤實體的重新檢查,從而更新被跟蹤實體的EntityState和相關實體屬性。在EF Core中應該還有其它方法也會觸發DbContext重新檢查被跟蹤實體的狀態,后面發現后再做補充。

 

 

更正

本文中示例代碼中的SetRelationshipIncorrectly和SetRelationshipCorrectly兩個方法,有一段描述有點問題,如下:

james.Book = new List<Book>();//由於我們在上面調用dbContext.Person.First(e => e.Name == "James")時,沒有用EF Core中Eager Loading的Include方法來加載Book實體集合,所以這里要用List類來構造一個Book實體集合,否則james.Book為null

其實我發現在EF Core中就算沒開啟Lazy Loading和沒使用Eager Loading的Include方法時,使用DbContext做查詢后(例如上面代碼中的james就來自查詢dbContext.Person.First(e => e.Name == "James")),也會給集合導航屬性賦值為System.Collections.Generic.HashSet<T>,只不過其長度(Count)為0。

所以上面這段代碼給james.Book賦值其實是沒有必要的,因為james.Book本來就不為null,后面直接調用james.Book的Add方法即可。只不過貌似給james.Book賦值為new List<Book>()也沒有什么問題,只是沒有必要而已。

 

 

源代碼下載

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM