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();
}