分享我们项目中基于EF事务机制的架构

写在前面:

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

posted @ 2013-07-10 13:09  Leo C.W  Views(11146)  Comments(25Edit  收藏  举报