工作单元(工作单位)

介绍

连接和事务管理是使用数据库的应用程序中最重要的概念之一。您需要知道何时打开连接,何时启动事务以及如何处置连接等等... ASP.NET Boilerplate使用其工作单元系统管理数据库连接和事务

ASP.NET Boilerplate中的连接和事务管理

ASP.NET样板打开一个数据库连接(也可能不会第一个数据库使用过程中立即打开,但打开的基础上,ORM提供者实现)并开始交易的时候进入 一个工作方法的单位您可以在此方法中安全地使用连接。在该方法结束时,事务被提交并且连接被设置如果方法抛出异常,则回滚事务,并释放连接。通过这种方式,工作单元的方法是原子的工作单元)。ASP.NET Boilerplate自动完成所有这些操作。

如果工作单元方法调用另一个工作单元方法,则两者都使用相同的连接和事务。第一个输入的方法管理连接和事务,然后其他方法重用它。

传统的工作单位方法

默认情况下,某些方法是工作单元方法:

假设我们有一个如下所示应用程序服务方法:

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方法中,我们使用人员存储库插入一个人,并使用统计信息库增加总人数。这两个存储库共享相同的连接和事务,因为默认情况下应用程序服务方法是一个工作单元。ASP.NET Boilerplate打开数据库连接并在输入CreatePerson方法时启动事务,如果没有抛出异常,它会在事务结束时提交事务。如果抛出异常,它会回滚所有内容。这样,CreatePerson方法中的所有数据库操作都变为原子工作单元)。 

除了默认的常规工作类单元之外,您还可以在模块的PreInitialize方法中添加自己的约定, 如下所示:

Configuration.UnitOfWork.ConventionalUowSelectors.Add(type => ...);

如果类型必须是传统的工作单元,则应检查类型并返回true。

控制工作单位

工作单元隐含地适用于上面定义的方法。在大多数情况下,您无需手动控制Web应用程序的工作单元。如果要控制其他地方的工作单元,可以明确使用它。它有两种方法。 

UnitOfWork属性

第一种和首选方法是使用UnitOfWork属性。例:

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

这样,CreatePerson方法成为工作单元并管理数据库连接和事务。两个存储库都使用相同的工作单元。请注意,如果这是一个应用程序服务方法,则不需要UnitOfWork属性。请参阅“ 工作单元方法限制 ”部分。

UnitOfWork属性有选项。请参阅“详细工作单元”部分。UnitOfWork属性也可以在类上使用,以配置它们的所有方法。如果class属性存在,则method属性会覆盖它。

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,如下所示(默认情况下,某些基类已经注入了UnitOfWorkManager:MVC控制器,应用程序服务域服务 ......)。这样,您就可以创建有限范围的工作单元。使用此方法,您必须 手动调用Complete方法。如果不调用它,则回滚事务并且不保存更改。

开始的方法有设置过载的工作选项单位如果你没有充分的理由,使用UnitOfWork属性会更简单(并推荐)

工作单位详细

禁用工作单位

您可能希望禁用传统工作单元方法的工作单元为此,请使用UnitOfWorkAttribute的IsDisabled 属性。用法示例:

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

通常,您不想这样做,但在某些情况下您可能想要禁用工作单元:

请注意,如果工作单元方法调用此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)]。

请注意,NHibernate和EntityFramework等ORM框架在内部保存单个命令中的更改。假设您更新了非事务性UOW中的一些实体。即使在这种情况下,所有更新都在单元数据库命令的工作单元结束时执行。如果直接执行SQL查询,则会立即执行,如果您的UOW不是事务性的,则不会回滚。

非交易UOW存在限制。如果您已经处于事务性工作范围单元中,则忽略将事务处理设置为false(使用事务范围选项在事务工作单元中创建非事务性工作单元)。

小心使用非事务性工作单元,因为大多数时候事情应该是事务性的,以确保数据的完整性。如果您的方法只读取数据而不更改数据,则可以安全地进行非事务性处理。

工作单位方法召唤另一个方法

工作单位是环境。如果工作单元方法调用另一个工作单元方法,则它们共享相同的连接和事务。第一种方法管理连接,然后其他方法重用它。

工作单位范围

您可以在另一个事务中创建不同的独立事务,也可以在事务中创建非事务性范围。.NET 为此定义了TransactionScopeOption您可以设置工作单元的“范围”选项来控制它。

自动保存更改

如果方法是一个工作单元,ASP.NET Boilerplate会自动保存方法末尾的所有更改。假设我们需要一种方法来更新人名:

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

这就是你要做的一切!名称已更新。我们甚至不必调用_personRepository.Update方法。ORM框架跟踪工作单元中实体的所有更改,并将这些更改反映到数据库中。

请注意,我们不需要为传统的工作单元方法声明UnitOfWork属性。

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()时,数据库连接也必须打开。

在大多数情况下,您将在Web应用程序中安全地使用GetAll方法,因为默认情况下所有控制器操作都是一个工作单元。这样,数据库连接在整个请求期间可用。

UnitOfWork属性限制

您可以使用UnitOfWork属性:

我们建议您始终将方法设为虚拟不能将该属性用于私有方法,因为ASP.NET Boilerplate对此使用动态代理,并且因为派生类无法看到私有方法。如果您不使用依赖项注入 并自己实例化该类,则UnitOfWork属性(以及任何代理)不起作用

选项

有一些选项可用于更改工作单元的行为。

首先,我们可以在启动配置中更改所有工作单元的默认值这通常在我们模块的PreInitialize方法中完成 

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

    //...other module methods
}

作为第二种选择,我们可以覆盖特定工作单元的默认值。的UnitOfWork属性构造和IUnitOfWorkManager。Begin方法有重载来设置这些选项。

作为最后一个选项,您可以使用启动配置来配置ASP.NET MVC,Web API和ASP.NET Core MVC控制器的默认工作单元属性(有关详细信息,请参阅其文档)。

方法

UnitOfWork系统无缝且无形地工作,但在某些特殊情况下,您可能需要调用其方法。

您可以通过以下两种方式之一访问当前工作单元:

保存更改

ASP.NET Boilerplate在工作单元结束时保存所有更改。您不必执行任何操作,但有时您可能希望在工作单元操作中保存对数据库的更改。例如,在保存一些更改后,我们可能希望使用 EntityFramework获取新插入的实体的Id 

您可以使用当前工作单元SaveChangesSaveChangesAsync方法。

请注意,如果当前工作单元是事务性的,则在发生异常时将回滚事务中的所有更改。即使是保存的变化!

活动

工作单元具有已完成失败和已处置的事件。您可以注册这些事件并执行所需的任何操作。例如,您可能希望在当前工作单元成功完成时运行一些代码。例:

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);
}

nidie.com.cn - 用心与你沟通