解析ABP框架中的事务处理和工作单元

通用连接和事务管理方法
连接和事务管理是使用数据库的应用程序最重要的概念之一。当你开启一个数据库连接,什么时候开始事务,如何释放连接...诸如此类的。

正如大家都知道的,.Net使用连接池(connection pooling)。因此,创建一个连接实际上是从连接池中取得一个连接,会这么做是因为创建新连接会有成本。如果没有任何连接存在于连接池中,一个新的连接对象会被创建并且添加到连接池中。当你释放连接,它实际上是将这个连接对象送回到连接池。这并不是实际意义上的释放。这个机制是由.Net所提供的。因此,我们应该在使用完之后释放掉连接对象。这就是最佳实践。

在应用程序中,有两个通用的方来创建/释放一个数据库连接:

第一个方法:在Web请求到达的时候,创建一个连接对象。(Application_BeginRequest这个位于global.asax中的事件),使用同一个连接对象来处理所有的数据库操作,并且在请求结束的时候关闭/释放这个连接 (Application_EndRequest事件)。

这是个简易但却没效率的方法,原因:

  • 或许这个Web请求不需要操作数据库,但是连接却会开启。这对于连接池来说是个毫无效率的使用方式。
  • 这可能会让Web请求的运行时间变长,并且数据库操作还会需要一些执行。这也是一种没效率的连接池使用方式。
  • 这对于Web应用来说是可行的。如果你的应用程序是Widnows Service,这可能就无法被实现了。
  • 同样的这是一个使用事务式的数据库操作最佳场景。如果有一个操作发生失败,所有的操作都会回滚。因为事务会锁住数据库中的一些数据列(事件数据表),它必定要是短暂的。

第二个方法: 创建一个连接当需要的时候(只要在使用它之前)并且释放它在使用它之后。这是相当高效的,但是就得乏味而且反复的去进行(创建/释放连接)。

ABP的连接和事务管理
ABP综合上述两个连接管理的方法,并且提供一个简单而且高效的模型。

1.仓储类(Repository classes)

仓储是主要的数据库操作的类。ABP开启了一个数据库连接并且在进入到仓储方法时会启用一个事务。因此,你可以安全地使用连接于仓储方法中。在仓储方法结束后,事务会被提交并且会释放掉连接。假如仓储方法抛出任何异常,事务会被回滚并且释放掉连接。在这个模式中,仓储方法是单元性的(一个工作单元unit of work)。ABP在处理上述那些动作都是全自动的。在这里,有一个简单的仓储:

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框架。如上所示,不需要撰写任何数据库连接操作(NHibernate中的Session)的程序代码。

假如仓储方法调用另一个仓储方法(一般来说,若工作单元方法调用另一个工作单元的方法),都使用同一个连接和事务。第一个被调用到的仓储方法负责管理连接和事务,而其余被它调用的仓储方法则只单纯使用不管理。

2.应用服务(Application service classes)

一个应用服务的方法也被考虑使用工作单元。如果我们拥有一个应用服务方法如下:

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仓储并且使用statistics仓储增加总people数量。两个仓储共享同一个连接和事务于这个例子中,因为这是一个应用服务的方法。ABP开启一个数据库连接并且开启一个事务于进入到CreationPerson这个方法,若没有任何异常抛出,接着提交这个事务于方法结尾时,若有异常被抛出,则会回滚这个事务。在这种机制下,所有数据库的操作在CreatePerson中,都成了单元性的了(工作单元)。

3.工作单元(Unit of work)

工作单元在后台替仓储和应用服务的方法工作。假如你想要控制数据库的连接和事务,你就需要直接操作工作单元。下面有两个直接使用的示例:

首要且最好的使用UnitOfWorkAttribute的方式如下:

[UnitOfWork]
public void CreatePerson(CreatePersonInput input)
{
  var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
  _personRepository.Insert(person);
  _statisticsRepository.IncrementPeopleCount();
}

因此,CreatePerson方法转变成工作单元并且管理数据库连接和事务,两个仓储对象都使用相同的工作单元。要注意,假如这是应用服务的方法则不需要添加UnitOfWork属性,见工作单元方法:第三章,3.3.5。

第二个示例是使用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,如上所示。因此,你可以创建更多的有限范围 (limited scope)的工作单元。在这个机制中,你通常可以手动调用Complete方法。如果你不调用,事务会回滚并且所有的异常都不会被储存。Begin方法被重写从而设置工作单元的选项。

这很棒,不过除非你有很好的理由,否则还是少用UnitOfWork属性。

工作单元
1.禁用工作单元(Disabling unit of work)

你或许会想要禁用应用服务方法的工作单元(因为它默认是启用的)。要想做到这个,使用UnitOfWorkAttribute的IsDisabled属性。示例如下:

[UnitOfWork(IsDisabled = true)]
public virtual void RemoveFriendship(RemoveFriendshipInput input)
{
  _friendshipRepository.Delete(input.Id);
}

平常时, 你不会需要这么做,这是因为应用服务的方法都应该是单元性且通常是使用数据库。在有些情况下,你或许会想要禁用应用服务的工作单元:

(1)你的方法不需要任何数据库操作且你不想要开启那些不需要的数据库连接
(2)你想要使用工作单元于UnitOfWorkScope类的有限范围内,如上所述
注意,如果工作单元方法调用这个RemoveFriendship方法,禁用被忽略且它和调用它的方法使用同一个工作单元。因此,使用禁用这个功能要很小心。同样地,上述程序代码工作的很好,因为仓储方法默认即为工作单元。

2.无事务的工作单元(Non-transactional unit of work)

工作单元默认上是具事务性的(这是它的天性)。因此,ABP启动/提交/回滚一个显性的数据库等级的事务。在有些特殊案例中,事务可能会导致问题,因为它可能会锁住有些数据列或是数据表于数据库中。在此这些情境下, 你或许会想要禁用数据库等级的事务。UnitOfWork属性可以从它的建构子中取得一个布尔值来让它如非事务型工作单元般工作着。示例为:

 [UnitOfWork(false)]
public GetTasksOutput GetTasks(GetTasksInput input)
{
  var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
  return new GetTasksOutput
      {
        Tasks = Mapper.Map<List<TaskDto>>(tasks)
      };
}

建议可以这么做[UnitOfWork(isTransaction:false)]。(具有可读性并且明确)。

注意,ORM框架(像是NHibernate和EntityFramework)会在单一命令中于内部进行数据储存。假设你更新了一些的实体于非事务的UoW。即便于这个情境下所有的更新都会于单一数据库命令的工作单元尾部完成。但是,如果你直接执行SQL查询,它会立即被执行。

这里有一个非事务性UoW的限制。如果你已经位于事务性UoW区域内,设定isTransactional为false这个动作会被忽略。

使用非事务性UoW要小心,因为在大多数的情况下,数据整合应该是具事务性的。如果你的方法只是读取数据,不改变数据,那么当然可以采用非事务性。

3.工作单元调用其它工作单元(A unit of work method calls another)

若工作单元方法(一个贴上UnitOfWork属性标签的方法)调用另一个工作单元方法,他们共享同一个连接和事务。第一个方法管理连接,其它的方法只是使用它。这在所有方法都执行在同一个线程下是可行的(或是在同一个Web请求内)。实际上,当工作单元区域开始,所有的程序代码都会在同一个线程中执行并共享同一个连接事务,直到工作单元区域终止。这对于使用UnitOfWork属性和UnitOfWorkScope类来说都是一样的。如果你创建了一个不同的线程/任务,它使用自己所属的工作单元。

自动化的saving changes (Automatically saving changes)

当我们使用工作单元到方法上,ABP自动的储存所有变化于方法的末端。假设我们需要一个可更新person名称的方法:

 [UnitOfWork]
  public void UpdateName(UpdateNameInput input) {
   var person = _personRepository.Get(input.PersonId);
   person.Name = input.NewName;
  }

就这样,名称就被修改了!我们甚至没有调用_personRepository.Update方法。ORM框架会持续追踪实体所有的变化于工作单元内,且反映所有变化到数据库中。

注意,这不需要在应用服务声明UnitOfWork,因为它们默认就是采用工作单元。

4.仓储接口的GetAll()方法(IRepository.GetAll())

当你在仓储方法外调用GetAll方法, 这必定得有一个开启状态的数据库连接,因为它返回IQueryable类型的对象。这是需要的,因为IQueryable延迟执行。它并不会马上执行数据库查询,直到你调用ToList()方法或在foreach循环中使用IQueryable(或是存取被查询结果集的情况下)。因此,当你调用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方法必需是工作单元,因为IQueryable在被调用ToList()方法于方法本体内,并且数据库连接必须于IQueryable.ToList()被执行时开启。

一如GetAll()方法,如果需要数据库连接且没有仓储的情况下,你就必须要使用工作单元。注意,应用服务方法默认就是工作单元。

5.工作单元属性的限制(UnitOfWork attribute restrictions)

在下面情境下你可以使用UnitOfWork属性标签:

(1)类所有public或public virtual这些基于界面的方法(像是应用服务是基于服务界面)
(2)自我注入类的public virtual方法(像是MVC Controller和Web API Controller)
(3)所有protected virtual方法。
建议将方法标示为virtual。你无法应用在private方法上。因为,ABP使用dynamic proxy来实现,而私有方法就无法使用继承的方法来实现。当你不使用依赖注入且自行初始化类,那么UnitOfWork属性(以及任何代理)就无法正常运作。

选项
有许多可以用来控制工作单元的选项。

首先,我们可以在startup configuration中改变所有工作单元的所有默认值。这通常是用了我们模块中的PreInitialize方法来实现。

public class SimpleTaskSystemCoreModule : AbpModule
{
  public override void PreInitialize()
  {
    Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
    Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
  }

  //...other module methods
}

方法
工作单元系统运作是无缝且不可视的。但是,在有些特例下,你需要调用它的方法。

SaveChanges:

ABP储存所有的变化于工作单元的尾端,你不需要做任何事情。但是,有些时候,你或许会想要在工作单元的过程中就储存所有变化。在这个案例中,你可以注入IUnitOfWorkManager并且调用IUnitOfWorkManager.Current.SaveChanges()方法。示例中以Entity Framework在储存变化时取得新增实体的Id。注意,当前工作单元是具事务性的,所有在事务中的变化会在异常发生时都被回滚,即便是已调用SaveChange。

事件
工作单元具有Completed/Failed/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);
}
时间: 2016-06-21

解析ABP框架中的数据传输对象与应用服务

数据传输对象(DTOs) 数据传输对象(Data Transfer Objects)用于应用层和展现层的数据传输. 展现层传入数据传输对象(DTO)调用一个应用服务方法,接着应用服务通过领域对象执行一些特定的业务逻辑并且返回DTO给展现层.这样展现层和领域层被完全分离开了.在具有良好分层的应用程序中,展现层不会直接使用领域对象(仓库,实体). 1.数据传输对象的作用: 为每个应用服务方法创建DTO看起来是一项乏味耗时的工作.但如果你正确使用它们,这将会解救你的项目.为啥呢? (1)抽象领域层 (

ABP框架中的日志功能完全解析

ASP.NET Boilerplate使用Castle Windsor's logging facility日志记录工具,并且可以使用不同的日志类库,比如:Log4Net, NLog, Serilog... 等等.对于所有的日志类库,Castle提供了一个通用的接口来实现,我们可以很方便的处理各种特殊的日志库,而且当业务需要的时候,很容易替换日志组件. 译者注释:Castle是什么:Castle是针对.NET平台的一个开源项目,从数据访问框架ORM到IOC容器,再到WEB层的MVC框架.AOP,

基于ASP.NET MVC的ABP框架入门学习教程

为什么使用ABP 我们近几年陆续开发了一些Web应用和桌面应用,需求或简单或复杂,实现或优雅或丑陋.一个基本的事实是:我们只是积累了一些经验或提高了对,NET的熟悉程度. 随着软件开发经验的不断增加,我们发现其实很多工作都是重复机械的,而且随着软件复杂度的不断提升,以往依靠经验来完成一些简单的增删改查的做法已经行不通了.特别是用户的要求越来越高,希望添加的功能越来多,目前这种开发模式,已经捉襟见肘.我很难想象如何在现有的模式下进行多系统的持续集成并添加一些新的特性. 开发一个系统时,我们不可避免

ABP框架中导航菜单的使用及JavaScript API获取菜单的方法

每一个WEB应用程序都有导航菜单,Abp也为用户提供了通用的创建和显示菜单方式. 创建菜单 一个应用程序可能包含不同的模块,而每个模块都可能有它自己的菜单项.在Abp中,需要创建一个派生自NavigationProvider的类来定义一个菜单项. 假设我们有一个这样的主菜单: Tasks Reports Administration 1 User Management 2 Role Management 由上可知,Administration菜单项有两个子菜单项.对应的生成方法如下: publi

解析ABP框架领域层中的实体类与仓储类

领域层 实体是DDD(领域驱动设计)的核心概念之一.Eric Evans是这样描述的"很多对象不是通过它们的属性定义的,而是通过一连串的连续性事件和标识定义的"(引用领域驱动设计一书). 译者注:对象不是通过它们的属性来下根本性的定义,而应该是通过它的线性连续性和标识性定义的..所以,实体是具有唯一标识的ID且存储在数据库中.实体通常被映射成数据库中的一个表. 实体类(Entity classes) 在ABP中,实体继承自Entity类,请看下面示例: public class Per

ASP.NET样板项目ABP框架的特性总结

ABP是"ASP.NET Boilerplate Project (ASP.NET样板项目)"的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板. ASP.NET Boilerplate 基于DDD的经典分层架构思想,实现了众多DDD的概念(但没有实现所有DDD的概念). ABP的官方网站:http://www.aspnetboilerplate.com ABP在Github上的开源

详解ABP框架中Session功能的使用方法

如果一个应用程序需要登录,则它必须知道当前用户执行了什么操作.因此ASP.NET在展示层提供了一套自己的SESSION会话对象,而ABP则提供了一个可以在任何地方 获取当前用户和租户的IAbpSession接口. 关于IAbpSession 需要获取会话信息则必须实现IAbpSession接口.虽然你可以用自己的方式去实现它(IAbpSession),但是它在module-zero项目中已经有了完整的实现. 注入Session IAbpSession通常是以属性注入的方式存在于需要它的类中,不需

详解ABP框架的参数有效性验证和权限验证

参数有效性验证 应用程序的输入数据首先应该被检验是否有效.输入的数据能被用户或其他应用程序提交.在Web应用中,通常进行2次数据有效性检验:包括客户端检验和服务端检验.客户端的检验主要是使用户有一个好的用户体验. 首先最好是在客户端检验其表单输入的有效性并且展示给客户端的那些字段输入是无效的.但是,服务器端的校验是更关键和不可缺失的(不要只做客户端检验而不做服务器端检验). 服务器端的检验通常是被应用服务(层)执行,应用服务(层)中的方法首先检验数据的有效性,然后才使用这些通过验证的数据.ABP

ABP框架的基础配置及依赖注入讲解

配置ABP 配置是通过在自己模块的PreInitialize方法中来实现的 代码示例如下: public class SimpleTaskSystemModule : AbpModule { public override void PreInitialize() { //在你的应用中添加语言包,这个是英语和作者的土耳其语. Configuration.Localization.Languages.Add(new LanguageInfo("en", "English&quo

详解ABP框架中的数据过滤器与数据传输对象的使用

数据过滤器(Data filters) 在数据库开发中,我们一般会运用软删除(soft-delete)模式,即不直接从数据库删除数据,而是标记这笔数据为已删除.因此,如果实体被软删除了,那么它就应该不会在应用程序中被检索到.要达到这种效果,我们需要在每次检索实体的查询语句上添加SQL的Where条件IsDeleted = false.这是个乏味的工作,但它是个容易被忘掉的事情.因此,我们应该要有个自动的机制来处理这些问题. ABP提供数据过滤器(Data filters),它使用自动化的,基于规

详解ABP框架中的日志管理和设置管理的基本配置

日志管理 Server side(服务器端) ASP.NET Boilerplate使用Castle Windsor's logging facility日志记录工具,并且可以使用不同的日志类库,比如:Log4Net, NLog, Serilog... 等等.对于所有的日志类库,Castle提供了一个通用的接口来实现,我们可以很方便的处理各种特殊的日志库,而且当业务需要的时候,很容易替换日志组件. 译者注释:Castle是什么:Castle是针对.NET平台的一个开源项目,从数据访问框架ORM到

ABP框架的体系结构及模块系统讲解

DDD分层 为了减少复杂性和提高代码的可重用性,采用分层架构是一种被广泛接受的技术. 为了实现分层的体系结构,ABP遵循DDD(领域驱动设计)的原则,将分为四个层次: 展现层(Presentation):提供一个用户界面,实现用户交互操作. 应用层(Application):进行展现层与领域层之间的协调,协调业务对象来执行特定的应用程序的任务.它不包含业务逻辑. 领域层(Domain):包括业务对象和业务规则,这是应用程序的核心层. 基础设施层(Infrastructure):提供通用技术来支持

详解ABP框架中领域层的领域事件Domain events

在C#中,一个类可以定义其专属的事件并且其它类可以注册该事件并监听,当事件被触发时可以获得事件通知.这对于对于桌面应用程序或独立的Windows Service来说非常有用.但是, 对于Web应用程序来说会有点问题,因为对象是根据请求(request)被创建并且它们的生命周期都很短暂.我们很难注册其它类别的事件.同样地,直接注册其它类别的事件也造成了类之间的耦合性. 在应用系统中,领域事件被用于解耦并且重用(re-use)商业逻辑. 事件总线 事件总线为一个单体(singleton)的对象,它由

详解Django框架中用户的登录和退出的实现

Django 提供内置的视图(view)函数用于处理登录和退出 (以及其他奇技淫巧),但在开始前,我们来看看如何手工登录和退出. Django提供两个函数来执行django.contrib.auth\中的动作 : authenticate()和login(). 认证给出的用户名和密码,使用 authenticate() 函数.它接受两个参数,用户名 username 和 密码 password ,并在密码对给出的用户名合法的情况下返回一个 User 对象. 如果密码不合法,authenticat

详解Django框架中的视图级缓存

更加颗粒级的缓存框架使用方法是对单个视图的输出进行缓存. django.views.decorators.cache定义了一个自动缓存视图响应的cache_page装饰器. 他是很容易使用的: from django.views.decorators.cache import cache_page def my_view(request): # ... my_view = cache_page(my_view, 60 * 15) 也可以使用Python2.4的装饰器语法: @cache_page

详解Spring 框架中切入点 pointcut 表达式的常用写法

自从使用 AspectJ 风格切面配置,使得 spring 的切面配置大大简化,但是 AspectJ 是另外一个开源项目,其规则表达式的语法也稍稍有些怪异. 下面给出一些常见示例的写法,例如,下面是一个对 Service 包上所有方法的切面配置: <aop:config> <aop:pointcut id="serviceOperation" expression="execution(* *..service*..*(..))"/> <

详解.vue文件中监听input输入事件(oninput)

.vue文件其实是一个组件,关于它的说明我之前也写过一篇文章,地址:.vue文件,今天这篇文章要讲的是.vue文件中监听input的输入值变化事件.需求是这页面中,改变input的值,就调用一个事件,第一想到的是oninput. oninput 事件在用户输入时触发,菜鸟教程中的用法是: 但是在.vue中这样写是没有作用的: <input type="text" id="cardsNum2" value="1" @oninput =&quo

详解Spring框架入门

一.什么是Spring Spring框架是由于软件开发的复杂性而创建的.Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情.然而,Spring的用途不仅仅限于服务器端的开发.从简单性.可测试性和松耦合性角度而言,绝大部分Java应用都可以从Spring中受益.Spring是一个轻量级控制反转(IoC)和面向切面(AOP)的容器框架. ◆目的:解决企业应用开发的复杂性 ◆功能:使用基本的JavaBean代替EJB,并提供了更多的企业应用功能 ◆范围:任何Java应用 二.