Conection 和事务管理在使用数据库的应用中是一个最重要的概念。当你打开一个连接,开始一个事务,如何来处理这些连接等等。
您也许知道,.NET使用了连接池。所以,创建一个连接实际上是从连接池里得到一个连接,因为创建一个新的连接需要花费一段时间。
如果在池中没有空闲的连接,则会创建一个新的连接并添加到连接池中。当你释放一个连接,实际上是将该连接放回到连接池中。并没有完全释放
这种机制是.NET提供的即插即用的功能。所以在使用完连接后,应该立即释放掉,除非你在需要的时候才创建一个连接
在应用中创建/释放一个数据库连接有两个通用方法。
第一个方法:当一个web请求并开始创建一个新的连接(通常在global.aspx中的Application_BeginRequest方法中执行),在所有数据库操作使用相同的连接,并且在请求结束时关闭/释放
该连接(Application_EndRequest event).这中方式是简单的但并不是很好,为什么?
1、也许在一个请求中并没有数据库操作,但是这个连接会一直处于打开状态。这是使用连接池非常低效的一种方式。
2、在一次请求中,可能请求耗时很长时间而数据库操作只花了很短的时间,这也是使用连接池非常低效的一种方式。
3、在一个WEB应用中是可行的,但是在Windows服务时,这将是不可行的。
所以,以事务方式来处理数据库操作被认为是一种最佳实践。如果其中一个操作失败,其他所有操作就会回滚。因为事务可以锁定数据库中一些行(甚至表),它必须是短暂存在的。
第二种方法:当需要的时候创建一个连接(在使用它之前),这是最高效方式,不需要在每个地方乏味并重复的创建/释放连接。
在 ASP.NET Boilerplate中使用连接和事务管理
仓储类
执行数据库操作仓储主要的类。ASP.NET Boilerplate打开一个数据库连接(也许没有立即打开,但是在第一次使用数据库会打开,这要基于ORM实现方式,当进入到仓储方法会开启一个事务。所以,在仓储方法中,你可以使用一个安全的连接。在方法结束时,事务被提交同时连接被释放。如果仓储方法抛出任何异常,事务会回滚并且释放连接。这样的话,一个仓储方法是一个原子性质的(一个工作单元)。ASP.NET Boilerplate会自动处理这些。这里,有一个简单的仓储:
public class ContentRepository : NhRepositoryBase<Content>, IContentRepository
{
public List<Content> GetActiveContents(string searchCondition)
{
var query = from content in Session.Query<Content>()
where content.IsActive && !content.IsDeleted
select content;
if (!string.IsNullOrEmpty(searchCondition))
{
query = query.Where(content => content.Text.Contains(searchCondition));
}
return query.ToList();
}
}
这个列子使用了NHibernate作为ORM工具,从上面显示来看,没有看到数据库连接(Session in Nihernate)打开/关闭的代码。
如果一个仓储方法调用了另一个仓储方法(一般,如果一个工作单元方法调用另一个工作单元的方法),它们使用相同的连接和事务。
应用服务(Application Services)
一个应用服务方法也可以被认为一个工作单元,假设我们一个应用服务方法,如下:
public class PersonAppService : IPersonAppService
{
private readonly IPersonRepository _personRepository;
private readonly IStatisticsRepository _statisticsRepository;
public PersonAppService(IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
{
_personRepository = personRepository;
_statisticsRepository = statisticsRepository;
}
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
}
}
在CreatePerson方法中,我们使用person仓储插入一个Person同时使用一个统计仓储新增总人数。在这个列子中所有的仓储共享相同连接和事务,因为这是一个应用服务方法。ASP.NET Boilerplate打开一个数据库连接并且当进入到CreatePerson方法会开启一个事务,如果方法结束时没有任何异常抛出则会提交这个事务。正是因为这种方式,所有在CreatePerson方法里数据库操作将变成原子性的(一个工作单元)。
工作单元(Unit Of Work)
工作单元对于仓储和应用服务方法隐式有效(你不需要使用UnitOfWork属性显示调用),如果你想在其他地放操作数据库连接和事务,你必须显示的使用它们。这里有两个方法:
UnitOfWork 属性
首选的方式是使用UnitOfWorkAttribute属性,例如:
[UnitOfWork]
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
}
这样,CreatePerson方法变成一个管理数据库连接和事务的工作单元。同时仓储使用相同的工作单元,注意在这是一个数据服务方法并不需要UnitOfWork属性、
IUnitOfWorkManager
第二种方式是用IUnitOfWorkManager.Begin(...)方法,如下所示:
public class MyService
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IPersonRepository _personRepository;
private readonly IStatisticsRepository _statisticsRepository;
public MyService(IUnitOfWorkManager unitOfWorkManager, IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
{
_unitOfWorkManager = unitOfWorkManager;
_personRepository = personRepository;
_statisticsRepository = statisticsRepository;
}
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
using (var unitOfWork = _unitOfWorkManager.Begin())
{
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
unitOfWork.Complete();
}
}
}
你可以注入并使用IUnitOfWorkManager就想上面那样(如果你从ApplicationService类派生自的应用服务(ApplicationService)类,你可以直接使用CurrentUnitOfWork,如果没有,你应该注入IUnitOfWorkManager)。正因为如此,你可以创建更多限制作用域的工作单元,通常你应该调用Compete()方法,如果你没有调用,事务将会回滚,改变的数据将不会保存。
如果你没有很好的方式,你最好使用UnitOfWork属性。
工作单元详解。
关闭工作单元
如果你想关闭应用服务方法的工作单元(因为默认是开启的),为了实现这样,UnitOfWork的关闭属性(IsDisabled),例子如下:
[UnitOfWork(IsDisabled = true)]
public virtual void RemoveFriendship(RemoveFriendshipInput input)
{
_friendshipRepository.Delete(input.Id);
}
一般情况,你不要这样做,因为一个操作数据库应用服务方法必须保持原子性,
有些情况你可能想关闭工作单元:
1、你的方法没有执行任何数据库操作,你不想打开不必要的数据库连接。
2、如上面描述的那样,你想要在一个UnitOfWorkScope类的有限作用域内使用工作单元
注意上面,如果一个工作单元方法调用这个RemoveFriendship方法,关闭属性将被忽略,它使用调用方法相同的工作单元,所以,请小心使用关闭属性,
禁用事务的工作单元
工作单元默认是事务的,ASP.NET Boilerplate开启/提交/回滚一个显示的数据库级别的事务。在一些特殊情况中,事务可能会导致一些问题,因为在数据库中它也许会锁定一些行或者表。在这样的情况下,你想关闭数据库级别的事务,UnitOfWork属性可以在构造函数中设置一个布尔值,像下面这样:
[UnitOfWork(isTransactional: false)]
public GetTasksOutput GetTasks(GetTasksInput input)
{
var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
return new GetTasksOutput
{
Tasks = Mapper.Map<List<TaskDto>>(tasks)
};
}
我建议使用[UnitOfWork(isTransactional: false)] ,我认为这样更具有可读性和明确性,你也可以使用[UnitOfWork(false)].
注意ORM框架(像NHibernate和EntityFramework)内部使用了一个单一的命令来保存更改。假设你在非事务工作更新了一些实体,虽然在这种情况下所有的更新都是在工作单元结束时使用单一数据库命令来执行,但是你直接执行一个SQL查询,它会立马执行。
对于非事务工作单元这是一个限制,如果你已经在一个事务单元的工作域中,设置 isTransactional为False将被忽略。
自动保存变化
当我们为一个方法使用工作单元,ASP.NET Boilerplate会在方法结束时自动的保存所有变化,假定我们需要更新一个Person的姓名:
[UnitOfWork]
public void UpdateName(UpdateNameInput input)
{
var person = _personRepository.Get(input.PersonId);
person.Name = input.NewName;
}
就这样,姓名被改变!我们不需要调用_personRepository.Update()方法,ORM框架会跟踪工作单元中所有实体的变化并将数据变化反馈到数据库中。
注意:不要为应用服务方法定义UnitWork属性,因为他们默认是一个工作单元。
IRepository.GetAll() 方法
当你在仓储方法外调用GetAll(),这里必须打开一个数据库连接因为它返回IQueryable。这是必须的,因为IQueryable是延迟执行的。它没有执行任何数据库查询,除非你调用ToList()方法,或者在foreach语句中循环使用。所以,当你调用ToList()方式时,数据库连接被激活。像下面的列子:
[UnitOfWork]
public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
{
//Get IQueryable<Person>
var query = _personRepository.GetAll();
//Add some filters if selected
if (!string.IsNullOrEmpty(input.SearchedName))
{
query = query.Where(person => person.Name.StartsWith(input.SearchedName));
}
if (input.IsActive.HasValue)
{
query = query.Where(person => person.IsActive == input.IsActive.Value);
}
//Get paged result list
var people = query.Skip(input.SkipCount).Take(input.MaxResultCount).ToList();
return new SearchPeopleOutput { People = Mapper.Map<List<PersonDto>>(people) };
}
这里SearchPeople()方法必须设置工作单元,因为IQueryabled的ToList()方法体内调用了,当执行IQueryabled. ToList()时,数据库连接必须要打开。
像GetAll()这样的方法,在仓储外部数据库连接是需要的,你必须使用工作单元。
注意服务方法默认设置成工作单元的。
UnitOfWork 属性的限制
SaveChanges
ASP.NET Boilerplate会在工作单元结束保存所有的变化,你不需要做任何事情,但是,有时候,你也许想在工作单元中途将数据库改变。如果这样,你可以注入IUnitOfWorkManager并且调用IUnitOfWorkManager.Current.SaveChanges()方法,一个例子你要先保存改变来获取一个新插入的实体的ID。注意:如果当前的工作单元是事务的,如果遇到一个异常,在事务中所有的变化将会回滚,虽然保存变化了、
Events
一个工作单元有 Completed, Failed and Disposed事件,你可以注册这样事件来执行需要的操作,注入IUnitOfWorkManager,使用IUnitOfWorkManager.Current属性来激活工作单元,并注册它的事件。
当当前的工作单元成功执行,你也许想运行这些代码。例如:
public void CreateTask(CreateTaskInput input)
{
var task = new Task { Description = input.Description };
if (input.AssignedPersonId.HasValue)
{
task.AssignedPersonId = input.AssignedPersonId.Value;
_unitOfWorkManager.Current.Completed += (sender, args) => { /* TODO: Send email to assigned person */ };
}
_taskRepository.Insert(task);
}