02-EF Core筆記之保存數據

  • 2019 年 10 月 6 日
  • 筆記

EF Core通過ChangeTracker跟蹤需要寫入資料庫的更改,當需要保存數據時,調用DbContext的SaveChanges方法完成保存。

基本的添加、更新、刪除操作示例如下:

using (var context = new BloggingContext())  {      // seeding database      context.Blogs.Add(new Blog { Url = "http://sample.com/blog" });      context.Blogs.Add(new Blog { Url = "http://sample.com/another_blog" });      context.SaveChanges();  }    using (var context = new BloggingContext())  {      // add      context.Blogs.Add(new Blog { Url = "http://sample.com/blog_one" });      context.Blogs.Add(new Blog { Url = "http://sample.com/blog_two" });        // update      var firstBlog = context.Blogs.First();      firstBlog.Url = "";        // remove      var lastBlog = context.Blogs.Last();      context.Blogs.Remove(lastBlog);        context.SaveChanges();  }

關聯數據

在EF Core中,除了獨立的模型外,還有與模型關聯的數據,這部分數據通過獨立模型添加到模型中,在SaveChanges時將會持久化到資料庫中。例如:

using (var context = new BloggingContext())  {      var blog = new Blog      {          Url = "http://blogs.msdn.com/dotnet",          Posts = new List<Post>          {              new Post { Title = "Intro to C#" },              new Post { Title = "Intro to VB.NET" },              new Post { Title = "Intro to F#" }          }      };        context.Blogs.Add(blog);      context.SaveChanges();  }

在這段程式碼中,Blog對象和三個Post對象將會被持久化。

如果要更改關係的引用,可將Post對象中的Blog引用設置為其它Blog對象即可:

using (var context = new BloggingContext())  {      var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" };      var post = context.Posts.First();        post.Blog = blog;      context.SaveChanges();  }

如果要刪除關係,只需將Post對象中的Blog引用設置為null即可,此時EF Core將判斷是否為必須關係,如果為必須關係,則從資料庫中刪除Post對象,如果為非必須關係,則將資料庫中對應的外鍵設置為null。

級聯刪除

級聯刪除是資料庫的概念,意思是當主體被刪除時,所有依賴該主體的項(通過外鍵關聯)也會被自動刪除。

EF Core對於提供了更細粒度的管理,它允許我們定義刪除行為,來控制依賴關係被移除時,如何處理關係的子實體。需要注意的是,EF Core的刪除行為僅對已載入的數據生效,如果關係未載入到記憶體中,則超出了EF Core的管控範圍。

事務

事務允許以原子方式處理多個資料庫操作。 如果已提交事務,則所有操作都會成功應用到資料庫。 如果已回滾事務,則所有操作都不會應用到資料庫。

默認情況下,每次SaveChanges方法的所保存的所有更改都將在一個事務中,要麼全部保存成功,要麼全部保存失敗。此種情況已能滿足大多數應用的需要。

共享事務(通過共享連接實現)

共享事務僅對關係型資料庫有效,因為此機制用到了DbConnection和DbTransaction。要實現該機制,首先要在多個DbContext之間共享資料庫連接。

以下程式碼演示了如何共享資料庫連接:

public class BloggingContext : DbContext  {      private DbConnection _connection;        public BloggingContext(DbConnection connection)      {        _connection = connection;      }        public DbSet<Blog> Blogs { get; set; }        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)      {          optionsBuilder.UseSqlServer(_connection);      }  }

對於上面程式碼中的BloggingContext,可以先創建DbConnection來進行實例化,也可以通過DbTransaction獲取DbConnection來實例化。隨後即可在同一個DbConnection上共享事務了。

使用 System.Transactions(環境事物)

如果需要跨較大作用域進行協調,則可以使用環境事務。例如:

using (var scope = new TransactionScope(      TransactionScopeOption.Required,      new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))  {      using (var connection = new SqlConnection(connectionString))      {          connection.Open();            try          {              // Run raw ADO.NET command in the transaction              var command = connection.CreateCommand();              command.CommandText = "DELETE FROM dbo.Blogs";              command.ExecuteNonQuery();                // Run an EF Core command in the transaction              var options = new DbContextOptionsBuilder<BloggingContext>()                  .UseSqlServer(connection)                  .Options;                using (var context = new BloggingContext(options))              {                  context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });                  context.SaveChanges();              }                // Commit transaction if all commands succeed, transaction will auto-rollback              // when disposed if either commands fails              scope.Complete();          }          catch (System.Exception)          {              // TODO: Handle failure          }      }  }

顯示登記到環境事務中:

context.Database.EnlistTransaction(transaction);

使用環境事務前,需驗證使用的提供程式是否支援環境事務。

並發控制

資料庫並發指多個進程或用戶同時訪問或更改資料庫中的相同數據的情況。 並發控制指的是用於在發生並發更改時確保數據一致性的特定機制。

EF Core採用樂觀並發控制來解決並發衝突問題。工作原理:每當在 SaveChanges 期間執行更新或刪除操作時,會將資料庫上的並發令牌值與通過 EF Core 讀取的原始值進行比較。如果一致則可以完成操作,如果不一致,則終止事務。

在關係資料庫上,EF Core 會對任何 UPDATE 或 DELETE 語句的 WHERE 子句中的並發令牌值進行檢查。 執行這些語句後,EF Core 會讀取受影響的行數。如果未影響任何行,將檢測到並發衝突,並且 EF Core 會引發 DbUpdateConcurrencyException。

在檢測到並發衝突後,EF Core會引發DbUpdateConcurrencyException異常,該異常中提供了一些有用的參數來幫助我們解決衝突:

  • 「當前值」是應用程式嘗試寫入資料庫的值。
  • 「原始值」是在進行任何編輯之前最初從資料庫中檢索的值。
  • 「資料庫值」是當前存儲在資料庫中的值。

此處可進行數據合併或用戶選擇等方式決策如何解決衝突。

狀態斷開對象的處理

EF Core判斷更新或添加數據是通過ChangeTrancker來進行的,這個操作需要在同一個DbContext中進行,而web應用通常先查詢到數據,然後將數據發送到客戶端進行相應的操作,隨後再由客戶端提交到伺服器端,此時實體所在的DbContext已發生變化,如何判斷對實體進行更新或添加就成了一個問題。

解決這個問題最簡單的方法是,更新和添加使用不同的web路徑,伺服器端通過提供Add方法和Update方法來區分操作。

除此之外,如果實體使用自動生成的主鍵,EF Core則可以通過判斷主鍵是否為默認值(null、0)來判斷是新增或更新。並且,對於這種情況,可直接使用DbContext的Update操作進行,在Update操作內部會完成該判斷。

如果實體的主鍵不是自動生成的,則需要手工判斷實體是否存在。下面的程式碼提供了一種添加或更新的思路:

public static void InsertOrUpdate(BloggingContext context, Blog blog)  {      var existingBlog = context.Blogs.Find(blog.BlogId);      if (existingBlog == null)      {          context.Add(blog);      }      else      {          context.Entry(existingBlog).CurrentValues.SetValues(blog);      }        context.SaveChanges();  }

SetValues方法將比較兩個實體的值,並對發生改變的屬性進行重新賦值,未發生改變的值保持不變,生成更新資料庫語句時也僅更新改變的欄位。

對於依賴關係的操作,同樣遵循以上幾種方式。

刪除操作

對於刪除操作,如果是刪除一個對象,則可以明確該對象的主鍵,並從資料庫中移除,此種情況不進行探討。

這裡需要探討的是,當對依賴關係中的列表進行部分刪除,如何進行更新的問題。例如Blog對象中有多個Post對象,如果從Blog中刪除部分Post,則意味著直接移除了Post對象,此時如果是斷開連接的情況,則EF Core無法跟蹤到Post實體列表的變更,從而導致無法正確的處理刪除。

一種可用的方案是採用軟刪除,將數據標記為已刪除,此時的操作與更新相同。然後在查詢數據時,使用查詢篩選器,將標記為已刪除的數據過濾掉,從而達到刪除的效果。

對於物理刪除,一種可用的方案是對Post列表進行對比,相應的程式碼如下:

public static void InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)  {      var existingBlog = context.Blogs          .Include(b => b.Posts)          .FirstOrDefault(b => b.BlogId == blog.BlogId);        if (existingBlog == null)      {          context.Add(blog);      }      else      {          context.Entry(existingBlog).CurrentValues.SetValues(blog);          foreach (var post in blog.Posts)          {              var existingPost = existingBlog.Posts                  .FirstOrDefault(p => p.PostId == post.PostId);                if (existingPost == null)              {                  existingBlog.Posts.Add(post);              }              else              {                  context.Entry(existingPost).CurrentValues.SetValues(post);              }          }            foreach (var post in existingBlog.Posts)          {              if (!blog.Posts.Any(p => p.PostId == post.PostId))              {                  context.Remove(post);              }          }      }        context.SaveChanges();  }