写在前面:
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)。
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更深层的机制,我了解的也不多。欢迎大家讨论!