【问题标题】:How to implement UnitOfWork with Entity Framework (and other ORMs). Some data in DB, some in memory如何使用实体框架(和其他 ORM)实现 UnitOfWork。一些数据在数据库中,一些在内存中
【发布时间】:2013-11-07 06:41:03
【问题描述】:

更改自: “使用实体框架(和其他 ORM)进行域模型验证。一些数据在数据库中,一些在内存中。”

最初我认为用 EF 实现 UoW 是一件容易的事。但事实证明这要复杂得多。

当业务逻辑(无论它位于何处)向存储库请求实体时,它不仅应该在数据库中搜索,还应该在本地缓存中搜索。存储库不应返回实体,以防它在当前 UoW 中被标记为删除,但它仍然存在于存储中。
带来复杂性的是部分图物化。当我们忘记持久性并假装一切都存在于内存中时,毫无疑问:加载一切,根据需要更改它,然后将结果更改写入 db。尽可能简单。

添加更多细节:
身份验证消息处理程序设置 Principal。
在我的 BL 深处,我需要检查当前经过身份验证的用户是否可以执行该操作。
每个角色都有与之相关的动作。所以我需要找到所有角色的动作并检查用户是否在角色中。但是,有可能在处理请求期间用户已从角色中删除。有关删除(或添加)的信息保存在 UoW 内部,但尚未保存。因此,如果我的检查逻辑只询问 DB,它会得到与工作流程不一致的答案。

可能是我错误地设置了 UoW 的边界,它不应该跨越单独的 BL 操作:
- 从角色中删除用户 - 更改到 DB
- 检查用户角色 - 从数据库中获取状态

虽然 BL 可以在每次写入 DB 时进行嵌套调用。所以我觉得让客户定义UoW的“大小”是可以的。

Fowler describing UoF 说:

您可以在每次更改对象模型时更改数据库, 但这会导致大量非常小的数据库调用,最终导致 很慢。此外,它要求您进行交易 对整个交互开放,如果你有一个 跨越多个请求的业务事务。情况是 更糟糕的是,如果你需要跟踪你读过的对象,所以你 可以避免不一致的读取。

但行为可能需要一系列对象更改。并且每次更改都必须知道当前状态 - 先前更改后尚未存储在 DB 中的状态。

问题的原始措辞:

我对使用实体框架实现的存储库和工作单元模式有些困惑。

首先,我允许我的模型在 UnitOfWork 的 Commit() 阶段之前处于无效状态。
最简单的例子就是移动和重命名文件语义。

如果您需要将file1 移动到Dir2 并将其重命名为file2,如果您将移动重命名 操作。
Dir1Dir2 中存在命名冲突。

所以我认为允许模型暂时具有无效状态是合理的。

现在我的问题的本质:

我的业务模型有一个规则,要求分析所有相关实体。
我将验证逻辑放在逻辑上包含所有这些项目的父实体中,因此负责施加和检查约束。而且我还想检查唯一性约束。

这听起来可能有点偏执,但在这种情况下,即使不使用数据库,我也可以通过单元测试轻松检查我的逻辑。

然而,在 EF 开始验证过程时,“未来”系统的状态由当前上下文(添加、删除、修改项)和未具体化到内存中并保留在数据库中的部分(更不用说并发)定义。 (绝对不能将所有实体加载到内存中)。

这需要我的存储库非常详细,以考虑分布式状态的事实。我发现这不是一项简单的任务。

我想知道其他人是如何解决这个问题的。可能有更好的方法来解决这个问题。

【问题讨论】:

  • 您所描述的内容被称为“最终一致性”或“基础”。这是关于一致性的另一种观点,它没有“事务”或“ACID”那么严格。谷歌搜索其中一些术语应该可以帮助您找到答案,因为解决方案对您当前的方法非常主观(即您是否在做 CQRS?)。
  • 好吧'我更多的是研究 DDD 部分而不是积极的实施。一旦我接近持久性无知,存储库、UoW 和类似 DDD 的东西就会变得非常模糊。

标签: .net entity-framework validation domain-driven-design ddd-repositories


【解决方案1】:

其实 DDD 很简单。它只是意味着:在代码中对领域概念、行为和用例进行建模。 ORM 或 ANY 持久性 (db) 细节在这里没有位置。当您执行 DDD 时,数据库不存在。当您想要存储某些内容时,您可以将其发送到存储库。当您需要存储中的某些内容时,您可以请求存储库来获取它。

DDD 是一种注重业务而非技术位的思维方式。从域的角度来看,存储库是“持久性”。当然,Domain 只定义 repo 接口(抽象),而 repository 实现驻留在其他地方,通常在 Peristence 层。但是您可以有多个实现。当测试或简单地开发时,拥有一个足以充当“临时”数据库但不涉及实际数据库的内存存储库(编写速度快)要容易得多。当然,Doamin 并不真正关心 Repo 是如何实现的,也不应该关心,因为 Repository 的目的是将域与持久性细节解耦

所以 EF、Nhibernate、NoSql 等所有这些都无关紧要,Domain 只知道存储库接口。 repo 实现将使用所有这些工具,但域不会知道。

还有一件事,域模型验证是由域完成的。存储库不进行验证,它只是从数据库中保存/加载内容。如果您指的是用户输入验证,则应在数据进入域之前完成,因为它与数据格式有关。但是,与业务规则相关的所有内容都仅由域强制执行

【讨论】:

  • 问题是要执行业务规则,您可能需要向存储库询问相关实体。但是这些实体存在于数据库和内存中,因此 repo 应该以某种方式合并它们以返回一致的结果。当然,如果您只有内存中的集合,那完全没有问题。可能我会将问题措辞更改为“如何使用 EF 实现 repo 以考虑内存状态。
  • 没有问题。业务规则仅在域中强制执行,您无需向存储库询问相关实体,因为没有任何实体。将 rdbms 思维模式应用于 DDD 是非常危险的,并且会导致非常糟糕的域建模。业务规则依赖于用例,通常是使用不同实体的服务的一部分,但这与 FK 类型关系无关。你正在使用回购,因为您不关心内存或数据库存储。无论回购实现如何,回购接口和域使用都不会改变。DDD 以 Domain 开头,以 db 结尾
  • 如何在 Customer.AddNewOrder() 方法中为客户强加 MaxOrdersCount 规则?从哪里获得客户的订单数量?有些订单在 DB 中,有些在 UnitOfWork 中并标记为已添加。最好的办法是问回购不是吗?您可以在 Customer.OrdersCount 属性中跟踪这一点,在 Customer Materailization 期间设置其初始值,然后在需要时对其进行递增和递减。然而我不是你永远不需要询问客户订单的回购来检查一些断言。
  • 为什么 Customer 有一个 AddNewOrder 方法?在我看来,它是客户的用例,而不是行为。 OrdersCount 似乎是 Customer 定义之外的东西,是 OrderStats 上下文的更多部分。 DDD 是相当棘手的,如果你没有正确理解上下文,那将是一个名为 DDD 的烂摊子,与 DDD 几乎没有共同之处。
  • 那你会把这种行为放在哪里?
【解决方案2】:

现在我相信没有通用的方法来实现存储库。
每个案例都需要自己考虑。

考虑这段代码:

public TEntity FirstOrDefault(Expression<Func<TEntity, bool>> filter)
{
    var compiledFilter = filter.Compile();

    var entity = DbSet.Local.SingleOrDefault(compiledFilter); // first, look in local storage
    if (entity != null)
    {
        return entity;
    }

    // Now we ask the database.
    entity = DbSet.SingleOrDefault(filter);

    // Entity from database can be already 'deleted' or changed locally. 
    // Modified local entity may not pass the filter so we should check this explicitly.
    if (entity != null && !MatchesLocal(entity, compiledFilter))
    {
        entity = null;
    }

    return entity;
}

private bool MatchesLocal(TEntity entity, Func<TEntity, bool> filter)
{
    Contract.Requires<ArgumentNullException>(entity != null);
    Contract.Requires<ArgumentNullException>(filter != null);

    var entry = Context.Entry(entity);
    var deleted = entry.State == EntityState.Deleted;

    var result = !deleted && filter(entity);

    return result;
}

这有点对应于 Fowler 的存储库 sequence diagram
唉,有一些警告。如果过滤表达式使用导航属性并且延迟加载已关闭,则很可能会出现 NullReferenceException。如果针对 DB 中不存在的属性执行搜索,则会出现另一个异常。
也许,特殊访问者的表达式转换可以解决这个问题,但这并不是一件容易的事。

下一点是性能。当我们只需要检查一些东西时,获取一堆对象是没有意义的。
示例如下:

        public bool VendorExistsWithName(string vendorName)
        {
            vendorName = vendorName.ToLower();

            // look among local objects
            var matchedLocalVendors = DbSet.Local
                                           .Where(
                                               vendor =>
                                               vendor.Name.ToLower() == vendorName)
                                           .ToList();

            if (matchedLocalVendors.Count >= 0)
            {
                return true;
            }

            var deletedVendorIds = new HashSet<int>(GetDeleted().Select(vendor => vendor.VendorId));
            var modifiedVendors = GetModified().ToDictionary(vendor => vendor.VendorId);

// ReSharper disable ReplaceWithSingleCallToAny 
            var exists = DbSet.Where(vendor => vendor.Name.ToLower() == vendorName)
                                  .Select(vendor => vendor.VendorId)
                                  .AsEnumerable()
                                  .Where(id => !deletedVendorIds.Contains(id)) // not deleted
                                  .Where(
                                      id =>
                                      !modifiedVendors.ContainsKey(id) ||
                                      modifiedVendors[id].Name.ToLower() == vendorName)
                                  .Any(); // matches even if modified
// ReSharper restore ReplaceWithSingleCallToAny

            return exists;
        }

这里我们只得到 id 而不是大对象。该方法可以推广,但这需要努力通过键(简单或复合)来获取和分组实体。
所有提到的功能都可以由实体框架提供开箱即用。不幸的是,事实并非如此。

最后但并非最不重要的痛苦是应该使用但不能使用聚合函数的情况。如果没有将内存状态与 DB 状态适当“合并”,这几乎是不可能的。另一方面,我不记得我想在我的业务逻辑中使用聚合函数的情况。

总而言之,我想说的是,虽然使用 EF 实现良好且一致的存储库需要付出努力,但它并不像我最初认为的那么可怕。

【讨论】:

    猜你喜欢
    • 2014-04-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-11
    相关资源
    最近更新 更多