寫在前面:
1. 本文中單元測試用到的數據庫,在執行測試之前,會被清空,即使用空數據庫。
2. 本文中的單元測試都是正確通過的。
要理解EF的事務機制,首先要理解這2個類:TransactionScope和DbContext。
DbContext是我們的數據庫,通常我們會建一個類MyProjectDbContext繼承自DbContext,里面包含所有的數據庫表。這個類相當於定義了一個完整的數據庫。
下面通過一些單元測試來看看這2個類是如何工作的。
1 [Test] 2 public void Can_Rollback_On_Errors_In_Different_Context() 3 { 4 var user1 = Mock.Users.Random(); 5 var user2 = Mock.Users.Random(); 6 user2.FirstName = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 7 var userCount = 0; 8 try 9 { 10 using (var scope = new TransactionScope()) 11 { 12 using (var db = new MyProjectDbContext()) 13 { 14 db.Users.Add(user1); 15 db.SaveChanges(); 16 userCount = db.Users.Count(); 17 } 18 using (var db = new MyProjectDbContext()) 19 { 20 db.Users.Add(user2); 21 db.SaveChanges();//will throw exception 22 } 23 scope.Complete(); 24 } 25 } 26 catch(Exception) 27 { 28 29 } 30 Assert.AreEqual(1, userCount); 31 using (var db = new MyProjectDbContext()) 32 { 33 Assert.AreEqual(0, db.Users.Count()); 34 } 35 }
注意第一個assert,userCount是等於1的,也就是說第一個db.SaveChanges()是順利執行了的。但是看看第二個assert,數據庫里面卻沒有user記錄。這就是使用TransactionScope得到的真正的事務機制。
再看一個測試:
1 [Test] 2 public void Cannot_Rollback_Without_Scope() 3 { 4 var user1 = Mock.Users.Random(); 5 var user2 = Mock.Users.Random(); 6 user2.FirstName = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 7 var userCount = 0; 8 try 9 { 10 using (var db = new MyProjectDbContext()) 11 { 12 db.Users.Add(user1); 13 db.SaveChanges(); 14 userCount = db.Users.Count(); 15 } 16 using (var db = new MyProjectDbContext()) 17 { 18 db.Users.Add(user2); 19 db.SaveChanges();//will throw exception 20 } 21 } 22 catch (Exception) 23 { 24 25 } 26 Assert.AreEqual(1, userCount); 27 using (var db = new MyProjectDbContext()) 28 { 29 Assert.AreEqual(1, db.Users.Count()); 30 } 31 }
這個測試跟上面的測試差不多,唯一的區別就是沒有使用TransactionScope把兩個DbContext包起來。於是每個DbContext成為獨立的事務。
再來看一個測試:
1 [Test] 2 public void Shouldnot_SaveToDB_As_ScopeNotComitted() 3 { 4 var user1 = Mock.Users.Random(); 5 var userCount = 0; 6 try 7 { 8 using (var scope = new TransactionScope()) 9 { 10 using (var db = new MyProjectDbContext()) 11 { 12 db.Users.Add(user1); 13 db.SaveChanges(); 14 userCount = db.Users.Count(); 15 } 16 //scope.Complete(); 17 } 18 } 19 catch (Exception) 20 { 21 22 } 23 Assert.AreEqual(1, userCount); 24 using (var db = new MyProjectDbContext()) 25 { 26 Assert.AreEqual(0, db.Users.Count()); 27 } 28 }
}
這個測試表明,一旦DbContext被TransactionScope包起來之后,那么scope必須要調用scope.Complete()才能將數據更新到數據庫。
基於上面的這些知識,我們可以很容易為EF搭建支持真正事務的框架。下面我簡單介紹下我們的項目架構(EF CodeFirst, MVC)。
基於EF事務機制的架構
Domain層:
定義數據實體類,即數據庫中的表。定義繼承自DbContext的MyProjectDbContext。
Service層:
主要用於封裝所有對數據庫的訪問。例子代碼如下:
1 public List<User> GetAllUsers() 2 { 3 using (var db = new MyProjectDbContext()) 4 { 5 return db.Users.ToList(); 6 } 7 }
上面這段代碼中注意要使用using,否則DbContext的延遲加載功能會在controller層被調用。加了using之后,可以避免在controller層對數據庫的直接訪問。
Controller層:
調用service層的代碼從數據庫中得到數據,返回給UI。例子:
1 public ActionResult GetAllUsers() 2 { 3 var users = IoC.GetService<IUserService>().GetAll(); 4 return View(users); 5 }
同時將UI傳回來的數據更新到數據庫,這時如果需要調用多個service來更新數據庫,那么就需要用到事務。例子:
1 public ActionResult DeleteUser(int userId) 2 { 3 try 4 { 5 using (var scope = new TransactionScope()) 6 { 7 IoC.GetService<IUserService>().DeleteLogs(userId); 8 IoC.GetService<IUserService>().DeleteUser(userId); 9 scope.Complete(); 10 return View(); 11 } 12 } 13 catch(Exception) 14 { 15 16 } 17 return View(); 18 }
通常情況下,我們會在MyControllerBase里面加一個 ActionResult TryScope(Action action)的方法,這樣在子類里面就可以不用寫try-catch了。
對於EF更深層的機制,我了解的也不多。歡迎大家討論!