2

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

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

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

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

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

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

福勒描述UoF 说:

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

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

问题的原始措辞:

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

首先,我允许我的模型在 UnitOfWork 的 Commit() 阶段之前处于无效状态。
为什么这是有益的最简单的例子是移动和重命名文件语义。
在此处输入图像描述

如果您需要移动file1Dir2并将其重命名为,如果您在事务上具有独立的移动重命名操作file2,则没有简单的方法可以做到这一点。在或 中存在命名冲突。
Dir1Dir2

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

现在我的问题的本质:

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

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

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

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

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

4

2 回答 2

1

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

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

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

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

于 2013-10-29T09:05:04.153 回答
0

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

考虑这段代码:

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 的存储库序列图
唉,有一些警告。如果过滤表达式使用导航属性并且延迟加载已关闭,则很可能会出现 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 实现良好且一致的存储库需要付出努力,但它并不像我最初认为的那么可怕。

于 2013-11-07T16:17:34.750 回答