class="p0">1. 本文中单元测试用到的数据库,在执行测试之前,会被清空,即使用空数据库。
2. 本文中的单元测试都是正确通过的。
要理解EF的事务机制,首先要理解这2个类:TransactionScope和DbContext。
DbContext是我们的数据库,通常我们会建一个类MyProjectDbContext继承自DbContext,里面包含所有的数据库表。这个类相当于定义了一个完整的数据库。
下面通过一些单元测试来看看这2个类是如何工作的。
1 [Test] 2 public void Can_Rollback_On_Errors_In_Different_Context()
{
var user1 = Mock.Users.Random();
var user2 = Mock.Users.Random();
user2.FirstName = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
var userCount = 0;
try {
using (var scope = new TransactionScope())
{
using (var db = new MyProjectDbContext())
{
db.Users.Add(user1);
db.SaveChanges();
userCount = db.Users.Count();
}
using (var db = new MyProjectDbContext())
{
db.Users.Add(user2);
db.SaveChanges();//will throw exception22 }23 scope.Complete();
}
}
catch(Exception)
{ }
Assert.AreEqual(1, userCount);
using (var db = new MyProjectDbContext())
{
Assert.AreEqual(0, db.Users.Count());
}
}
注意第一个assert,userCount是等于1的,也就是说第一个db.SaveChanges()是顺利执行了的。但是看看第二个assert,数据库里面却没有user记录。这就是使用TransactionScope得到的真正的事务机制。
public void Cannot_Rollback_Without_Scope() {
var user1 = Mock.Users.Random();
var user2 = Mock.Users.Random();
user2.FirstName = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
var userCount = 0;
try
{
using (var db = new MyProjectDbContext())
{
db.Users.Add(user1);
db.SaveChanges();
userCount = db.Users.Count();
}
using (var db = new MyProjectDbContext()) {
db.Users.Add(user2);
db.SaveChanges();//will throw exception
}
}
catch (Exception)
{ }
Assert.AreEqual(1, userCount);
using (var db = new MyProjectDbContext()) {
Assert.AreEqual(1, db.Users.Count());
}
}
这个测试跟上面的测试差不多,唯一的区别就是没有使用TransactionScope把两个DbContext包起来。于是每个DbContext成为独立的事务。
再来看一个测试:
[Test]
public void Shouldnot_SaveToDB_As_ScopeNotComitted() {
var user1 = Mock.Users.Random();
var userCount = 0;
try
{
using (var scope = new TransactionScope())
{
using(var db = new MyProjectDbContext())
{
db.Users.Add(user1);
db.SaveChanges();
userCount = db.Users.Count();
}
//scope.Complete();
}
}
catch (Exception)
{ }
Assert.AreEqual(1, userCount);
using (var db = new MyProjectDbContext())
{
Assert.AreEqual(0, db.Users.Count());
}
}
}
这个测试表明,一旦DbContext被TransactionScope包起来之后,那么scope必须要调用scope.Complete()才能将数据更新到数据库。
基于上面的这些知识,我们可以很容易为EF搭建支持真正事务的框架。下面我简单介绍下我们的项目架构(EF CodeFirst, MVC)。
基于EF事务机制的架构
Domain层:
定义数据实体类,即数据库中的表。定义继承自DbContext的MyProjectDbContext。
Service层:
主要用于封装所有对数据库的访问。例子代码如下:
public List<User> GetAllUsers() {
using (var db = new MyProjectDbContext())
{ return db.Users.ToList();
}
}
上面这段代码中注意要使用using,否则DbContext的延迟加载功能会在controller层被调用。加了using之后,可以避免在controller层对数据库的直接访问。
Controller层:
调用service层的代码从数据库中得到数据,返回给UI。例子:
public ActionResult GetAllUsers()
{
var users = IoC.GetService<IUserService>().GetAll();
return View(users);
}
同时将UI传回来的数据更新到数据库,这时如果需要调用多个service来更新数据库,那么就需要用到事务。例子:
public ActionResult DeleteUser(int userId)
{
try
{
using(var scope = new TransactionScope())
{
IoC.GetService<IUserService>().DeleteLogs(userId);
IoC.GetService<IUserService>().DeleteUser(userId);
scope.Complete();
return View();
}
}
catch(Exception)
{
}
return View();
}
通常情况下,我们会在MyControllerBase里面加一个 ActionResult TryScope(Action action)的方法,这样在子类里面就可以不用写try-catch了。