【问题标题】:DDD: Repository, unit of work, ORMs and dependency injectionDDD:存储库、工作单元、ORM 和依赖注入
【发布时间】:2014-10-28 05:04:15
【问题描述】:

编辑

如果你把 10 位领域专家放在一个房间里会发生什么?对,你有 11 条意见。 (其中10个被声明为反模式

感谢大家的详细解答。我会研究它们并考虑它们如何帮助我解决特定问题。


当与 ORM 和依赖注入一起使用时,我很难理解存储库和工作单元。考虑以下非常标准的接口:

public interface IRepository<TAggregateRoot> : ITransientDependency
{

    void Add(TAggregateRoot aggregateRoot);
    void Delete(TAggregateRoot aggregateRoot);
    IEnumerable<TAggregateRoot> GetAll();
}

public interface IUnitOfWork : ITransientDependency
{
    void Commit();
    void Rollback();
}

我想用这些方法来介绍一些场景。

  • 将单个实体插入到存储库中
  • 删除链接实体也应删除的聚合
  • 对超过 2 个存储库执行事务

使用 NHibernate 的默认实现可能如下所示:

public abstract class NHibernateRepository<TAggregateRoot> : IRepository<TAggregate>
{
    protected NHibernateRepository(ISession session) {}
}

public sealed NHibernateUnitOfWork : IUnitOfWork
{
    public NHibernateUnitOfWork(ISession session)
    {}

    public void Commit() {
        _session.Flush();
    }
}

1.场景:将单个实体插入存储库

// ASP.NET MVC controller, but valid for any
// other arbitary application service
public class MyController : Controller {

    private readonly IPeopleRepository _repository;

    // di -> declaring IPeopleRepository dependency
    public MyController(IPeopleRepository repository) {
        _repository = repository;
    }

    public void AddPerson(Person person) {
        _repository.Add(person);
    }

}

现在,在我将人添加到存储库后会发生什么?对,没什么。即使单个插入不完全是一个工作单元(一个事务,技术),像 EF 和 NHibernate 这样的 ORM 框架仍然需要提交对数据库的更改,因为他们花哨的会话DBContexts在技术上是工作单元存储库

如何克服第一个问题?为我所做的每一件事开始一个工作单元

2.Scenario:删除聚合,链接实体也应该被删除

查看以下汇总:

public class Person : IAggregateRoot {

    private readonly List<Cat> _cats = new List<Cat>();

    public IEnumerable<Cat> Cats {
        get { return _cats; }
    }

    public void AddCat(Cat cat) {
    //

}

让我们通过存储库使用它的根来删除聚合:

IPersonRepository.Remove(person);

现在,Person 聚合的所有实体都在技术上被删除。由于代码中不再有对它们的引用,因此垃圾收集器充当数据库管理器并从内存中删除 Cats

但这在 ORM 存储库实现中看起来如何呢? 工作单元在哪里发挥作用?

3.场景:在超过 2 个存储库上进行事务

好的,我得到了我喜欢的 SomethingService。他必须在多个存储库上做一些事情,因此显然需要一个需要工作单元的事务。

public SomethingService : ISomethingService {

    public ISomethingService(IFirstRepo repo1, ISecondRepo repo2, IUnitOfWork uow)
    {
    ...
    }

    public void DoSomething() {
        repo1.AddThis();
        repo2.GetThisOne();
        repo2.BecauseOfTheOneAboveDeleteThis();
        uow.Commit();
    }

}

对我来说看起来不错,但考虑到上面的 NHibernate 存储库和 工作单元 实现,这将不起作用,因为每个(工作单元、 2 个存储库)具有不同的 NHibernate 会话实例!

我考虑过使用 Interceptors 进行面向方面的编程,但这只是部分起作用,因为当 IoC 拦截服务方法时,已经使用自己的会话创建了存储库,因此无法共享工作单元的会话。

如何克服这个问题?是否有任何完整的工作示例可以在没有任何肮脏黑客的情况下运行? (例如,单件作品单元)

只是说:是的,我想将存储库与 ORM 一起使用。它们是一种很好的方式来抽象框架,让我按照我(或我的客户)想要的方式设计我的域,而不是像框架想要的那样。

非常感谢您阅读这堵文字墙。

【问题讨论】:

  • 将实体添加到存储库应该向 ORM 的更改跟踪器注册它。使用 EF,您可以通过将 repo 包装在 DbSet 周围来做到这一点。然后在进行任意数量的实体操作之后,您可以简单地调用Commit,它映射到拥有 DbSet 的上下文的SaveChanges。不过,我认为您不能“将框架抽象掉”。

标签: c# entity-framework nhibernate dependency-injection domain-driven-design


【解决方案1】:

1.工作单元的处理方式可以稍有不同,


根据您的建议,将会话注入存储库,我不会走那条路,而是使用 sessionFactory

public interface IUnitOfWork : IDisposable
{
    ISession CurrentSession { get; }
    void Commit();
    void Rollback();
}

public class NHibernateUnitOfWork : IUnitOfWork
{
    private readonly ISessionFactory _sessionFactory;

    [ThreadStatic]
    private ISession _session;

    [ThreadStatic]
    private ITransaction _transaction;

    public NHibernateUnitOfWork(ISessionFactory sessionFactory)
    {
        _sessionFactory = sessionFactory;
        _session = _sessionFactory.OpenSession();
        _transaction = _session.BeginTransaction();
    }

    public static ISession CurrentSession { get { return _session; } }

    public void Dispose()
    {
        _transaction = null;
        _session.Close();
        _session = null;
    }

    public void Commit()
    {
        _transaction.Commit();
    }

    public void Rollback()
    {
        if (_transaction.IsActive) _transaction.Rollback();
    }
}

public class Repository : IRepository
{
    public void Add(IObj obj)
    {
        if (NHibernateUnitOfWork.CurrentSession == null)
            throw new Exception("No unit of work present");

        NHibernateUnitOfWork.CurrentSession.Save(obj);         
    }
}

// ASP.NET MVC controller, but valid for any
// other arbitary application service
public class MyController : Controller 
{
    private readonly IPeopleRepository _repository;

    // di -> declaring IPeopleRepository dependency
    public MyController(IPeopleRepository repository) {
        _repository = repository;
    }

    public void AddPerson(Person person) 
    {
        using (IUnitOfWork uow = new NHibernateUnitOfWork())
        {
            try
            { 
                _repository.Add(person);
                uow.Commit();
            }
            catch(Exception ex)
            {
               uow.RollBack();
            }
        }
    }
 }

虽然这是解决问题的一种方法,但也有一些更聪明的方法,一种是使用 ActionFilter,它在操作之前启动事务并在全部成功时提交,或者您可以使用 HttpModule 来处理事务处理..

或者

您可以采用完全不同的路径并实现命令模式,其中每个操作都是命令,无论它有多复杂,并且处理程序应该启动并提交事务看看https://fnhmvc.codeplex.com/

2。如果使用正确的映射和正确的 UOW 用于删除父实体子实体将被自动删除

3.如果使用上面提到的工作单元模式,这将不是问题

公共的SomethingService:ISomethingService { 公共ISomethingService(IFirstRepo repo1,ISecondRepo repo2,IUnitOfWork uow) { ... }

public void DoSomething() 
{
    using (IUnitOfWork uow = new NHibernateUnitOfWork())
    {
        try
        { 
            repo1.AddThis();
            repo2.GetThisOne();
            repo2.BecauseOfTheOneAboveDeleteThis();
            uow.Commit();
        }
        catch(Exception ex)
        {
           uow.RollBack();
        }
    }
}

【讨论】:

    【解决方案2】:

    场景 1:

    假设您的 Controller 托管在 Web 应用程序中,并且单个 Web 请求应导致将实体插入到存储库中。诀窍是 然后对齐您的IUnitOfWork,以便在您开始处理 请求并在您完成处理请求时提交。

    我不确定您使用的是哪个 DI 框架,但 Unity 有一个优雅的解决方案 PerRequestLifetimeManager 形式的 ASP.NET MVC。使用注册的所有类型 此生命周期管理器仅在单个 Web 请求的范围内缓存,并且 当 Web 请求结束时会自动处理。因此,如果你使用这一生 经理并让您的工作单元实施IDisposable,以便它在一个 丢弃,你已经处理好了。

    当错误发生时,您可能需要做一些小技巧才能不提交, 虽然 - 可能在NHibernateUnitOfWork(见下文)。

    场景 2:

    我会说IPersonRepository.Remove(person) 应该知道明确删除所有 猫也一样。这意味着您不能使用通用存储库,这是完美的 很好,因为它被认为(至少在某些人看来)有点像anti-pattern

    场景 3:

    同样,解决方案是使用正确的生命周期管理器。每个原因 存储库现在拥有自己的 ISession 版本是因为 DI 容器认为 ISession 瞬态类型。如果你使用PerResolve 之类的东西,或者更好的是 PerRequest 生命周期管理器,它将为每个重复使用相同的 ISession 实例 您的存储库。

    顺便说一句,我注意到您的 NHibernateRepository 直接依赖于 ISession 而不是 比NHibernateUnitOfWork - 这有什么具体原因吗?我想我会 让NHibernateUnitOfWork 公开一个ISession 属性(或者甚至重新公开其所有成员),并让所有存储库都依赖于NHibernateUnitOfWork。 一方面,因为存储库不只是“通过会话做某事”,它们是 实际上是你正在做的工作单元的部分。另一方面,它使事情变得更容易 使用 NHibernateUnitOfWork 作为外观,您可以在其中防止 NHibernate 会话实际上 如果出现错误或未进行任何修改,则提交。

    在这种情况下,我会让NHibernateUnitOfWork 使用PerRequest 生命周期而不是 比ISession

    【讨论】:

    • +1 提到通用存储库模式是一种惰性反模式。
    • “通用存储库模式是一种惰性反模式”是一个非常有争议的说法,尤其是在 DDD 的上下文中。由于评论长度的限制,我表达了它的主要问题。一旦您定义了自定义(非通用)存储库,您就会很想将越来越复杂的评估添加到存储库中。它导致 1) 域逻辑泄漏,2) 聚合封装违规。 DDD Repository 应该是通用的,它不需要有“有意义的契约”,添加、删除、编辑、获取、列出 - 都是存储库必须做的。在存储库接口中使用 IQueryable 只是另一个大错误。
    • 如果您需要“在 DB 端”做一些复杂的事情,规范模式(另一种 DDD 模式)是更好的方法。它特别适合通用(或通用)存储库。
    【解决方案3】:

    一般来说,您的问题可以通过将工作单元的控制权交给知道应用程序当前执行上下文的对象(在您的情况下为控制器)来解决。

    工作单元是您的业务/应用事务,较低级别的持久性对象(例如存储库)不知道总体上下文,也不应该决定事务何时完成。实际上,Repos 应该只是对 UoW 的引用,以便能够从中添加/删除内容,而不是结束它。

    blog post 对 DBContext 和工作单元的微妙之处进行了非常好的解释。它是关于 Entity Framework 的 DBContext,但您可以轻松地将其转换为 NHibernate 的 Session。

    您的服务必须是应用程序中的唯一组件 负责最后调用 DbContext.SaveChanges() 方法 的商业交易。应用程序的其他部分是否应该调用 SaveChanges() 方法(例如存储库方法),你最终会 部分提交的更改,使您的数据不一致 状态。

    对于场景 3,您应该尽可能避免跨越多个聚合的业务事务。将您的聚合精确地设计为transactional consistency boundaries 可能是个好主意。

    如果您仍然需要因为另一个聚合的变化而影响一个聚合,最终一致性可以派上用场。例如,可以通过让第一个聚合发出域事件,然后事件处理程序(同步或异步)获取它并在单独的 UoW 中调用第二个聚合来实现。

    【讨论】:

    • +1 获取有关事务一致性边界的 DDD 详细信息。
    • @guillaume31,在这种情况下,我认为所有最终一致性、领域事件、“聚合事务一致性边界”都是过度规范。这是人们经常谈到的关于 NoSQL 的内容。如果问题只是接受所有更改,则不需要单独的 UoW。只需在域服务或控制器代码中使用 Repository.SaveChanges()。如果我们在这里需要 ACID 来进行复杂的交易,那么就需要创建交易层逻辑。 UoW 可以实现它,是的,它必须使用 ORM\DataAccess 功能。
    【解决方案4】:

    我会尽量回答。

    1.场景:将单个实体插入存储库

    你可以将void SaveChanges()方法添加到你的仓库接口,没关系。

    2.删除链接实体应删除到的聚合

    有很多方法可以做到这一点。首先,您应该决定是负责删除关联对象的聚合根还是存储库?

    如果是聚合根责任,那么

    1. 您需要使用void OnDelete() 填充IAggregateRoot
    2. 删除前在存储库中调用它。
    3. OnDelete 的实现应该手动删除所有关联的对象,因此您需要
    4. 定义IDeleteStrategy,它将像Only-Delete-Repository 一样工作。区别在于 IDeleteStrategy 方法的参数限制 - 它必须处理实体(!),而不仅仅是聚合根。并且存储库可以通过将删除操作委托给它来重用它。
    5. 最后,您应该将IDeleteStrategy 合并到要删除的每个实体类型的聚合根中。是的,它是聚合根中的依赖注入。

    这个解决方案很好,因为:

    1. 聚合根管理聚合内部,因此不会违反聚合封装。
    2. 如果满足某些内部聚合条件,聚合根会抛出异常,因此删除不会完成。
    3. 或许,您可以更简单、更通用地保存您的存储库(对我而言,根据 DDD,这非常好)

    如果它是存储库的责任,那么您应该创建非通用存储库并为每个聚合实现自定义删除逻辑。

    最后一个。有一个原则告诉我们对象的创建者对其生命周期负责。因此,工厂可能是清除内部骨料的好地方。

    3.场景:对超过 2 个存储库进行事务

    有一个技术限制,需要使用单个 DbConnection 来执行事务/UoW。解决方案很大程度上取决于您使用的技术。因此,我可以快速建议的唯一方法是强制您的环境使用单个上下文 (EF?)/connection/session 每个请求/用户会话。

    另一种可能性是实现某种事务级别(在域和 DAL 之间),但这也取决于您使用的技术。

    【讨论】:

      猜你喜欢
      • 2023-03-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多