DDD / CQRS:结合用户界面要求的阅读模型

时间:2015-11-04 11:47:53

标签: domain-driven-design cqrs

让我们使用博客上下文的经典示例。在我们的域中,我们有以下方案:Users可以写Posts。必须至少在一个Posts内对Category进行编目。可以使用Posts来描述TagsUsers可以对Posts发表评论。

四个实体(PostCategoryTagComment)被实现为不同的聚合,因为我没有检测到实体数据的任何规则应该干涉另一个。因此,对于每个聚合,我将有一个代表它的存储库。太,每个聚合通过他的id引用其他人。

在CQRS之后,我从这个场景中扣除了导致WriteNewPostCommandPublishPostCommandDeletePostCommand等命令的典型用例以及各自的查询以获取数据来自存储库。 FindPostByIdQueryFindTagByTagNameQueryFindPostsByAuthorIdQuery等......

根据应用程序的哪个站点(后端或前端),我们会有或多或少复杂的查询。所以,如果我们在首页上,也许我们需要构建一些小部件来获取最后的评论,类别的最新帖子等等...查询涉及一个简单的Query对象(少数搜索标准)和一个{{ 1}}非常简单(单个存储库作为处理程序类的依赖)

但在其他地方,这些查询可能会更复杂。在管理面板中,我们需要在表格中显示满足复杂搜索条件的关系。可能是有趣的搜索帖子:作者姓名(无ID),类别名称,标签名称,发布日期......属于不同聚合和不同存储库的标准。

此外,在我们的帖子表中,我们不想显示帖子以及作者ID或类别ID。我们需要显示所有信息(姓名用户,头像,类别名称,类别图标等)。

我的问题是:

  • 在基础架构层,当我们设计存储库时,搜索方法(findAll,findById,findByCriterias ...)应该返回引用所有关联id的相应实体?我的意思是,如果有一个方法QueryHandlerfindPostById(uuid),应该返回一个帖子实例,其中引用了它拥有的所有类别ID,所有标签ID和它具有的作者ID?或者我的repo应该使用某种方法来填充给定的帖子实例以及我想要的关联吗?

  • 如果我想搜索由约翰撰写的2014年12月12日创建的帖子,并将其分类为"新闻"和"视频"类别和标签"科幻"和"冒险",并获取每个汇总的完整详细信息,如何创建我的findPostByCustomFilter(filter)Query

a)使用我的所有参数创建QueryHandler(authorName,categoriesNames,TagsNames,如果想要回溯QueryUserCategory关联完整详细信息,然后他的Tag只用一个来描述不同的阅读模型。还是......

b)创建不同的QueryHandler(FindCategoryByName,FindTagByName,FindUserByName),然后我的Web控制器将其调用以供日后使用 调用FindPostQuery,但现在传递给他的authorid,categoryid,tagid从其他查询返回?

b)解决方案看起来更干净但看起来我更贵。

4 个答案:

答案 0 :(得分:3)

  • 在查询方面,没有实体。您可以以任何最适合您需求的方式自由填充您的阅读模型。无论您需要在屏幕(部分)上显示什么数据,都可以将其放入读取模型中。返回这些读取模型的命令端存储库不是专用的查询端数据访问对象。

  • 您提到了“复杂的搜索条件” - 我建议您使用相应的class MovieRepository extends EntityRepository { public function getActoryByGender($gender) { /.../ } } 对象对其进行建模。此对象与技术无关,但它将传递给您的Query端数据访问对象,该对象将知道如何组合条件以针对其所针对的特定数据存储构建较低级别的查询。

答案 1 :(得分:1)

从CQRS,您将拥有一个用于查询和命令的separeted Stack。您的查询堆栈应代表项目中的diferente模块,命名空间,dll或包。

a)您将创建一个QueryModel,此查询模型将返回您需要的任何内容。如果您熟悉Entity Framework或NHibernate,您将创建一个Façade来保存此查询togheter,DbContext或Session。

b)你可以创建这个separeted查询,但是再说一遍,如果你熟悉任何ORM,你应该返回代表模型的集合,将每个集合返回为IQueryable并使用LET(Linq Expression Trees)来进行查询堆叠更有活力。

使用Entity Framework和C#例如:

public class QueryModelDatabase : DbContext, IQueryModelDatabase
{
    public QueryModelDatabase() : base("dbname")
    {
        _products = base.Set<Product>();
        _orders = base.Set<Order>();
    }

    private readonly DbSet<Order> _orders = null;
    private readonly DbSet<Product> _products = null;

    public IQueryable<Order> Orders
    {
        get { return this._orders.Include("Items").Include("Items.Product"); }
    }

    public IQueryable<Product> Products
    {
        get { return _products; }
    }
}

然后你应该按照你需要的方式进行查询并返回任何内容:

using (var db = new QueryModelDatabase())
{
    var queryable = from o in db.Orders.Include(p => p.Items).Include("Details.Product")
                    where o.OrderId == orderId
                    select new OrderFoundViewModel
                    {
                        Id = o.OrderId,
                        State = o.State.ToString(),
                        Total = o.Total,
                        OrderDate = o.Date,
                        Details = o.Items
                    };
    try
    {
        var o = queryable.First();
        return o;
    }
    catch (InvalidOperationException)
    {
        return new OrderFoundViewModel();
    }
}

答案 2 :(得分:1)

使用像这样的简单应用程序,更容易避免被聚合分散注意力。执行事件来源,通过一组易于查询所需方式的表来订阅事件。

换句话说,听起来您的主要目标是能够轻松查询所描述的场景。从最终目标开始。现在,编写事件处理程序以相应地调整表。

从事件和UI开始。然后,其他所有内容都将很容易适应。 Google的“事件建模”功能可以帮助您提出构想,以何种方式构建应用程序。

答案 3 :(得分:1)

我可以看到您的方法中存在三个问题,需要分别解决:

  1. 在CQRS中,查询与命令完全分开。因此,请勿尝试使用Commands管道存储库来解决您的查询。 CQRS的目的恰恰是允许您以非常不同的方式来解决命令和查询,因为它们有非常不同的要求。

  2. 您在问题标题中提到了DDD,但在问题本身中没有提及边界上下文。如果遵循DDD,则很可能会有不止一个BC。例如,在您的问题中,可能是CategoryName和AuthorName属于两个不同的BC,这也与博客帖子所在的BC不同。如果是这种情况,并且每个BC都适当拥有自己的数据,那么您要搜索并显示在UI中的数据将可能存储在不同的数据库中,因此,甚至无法通过联接在数据库中实现查询。

  3. 搜索和读取数据是两个不同的问题,可以/应该以不同的方式解决。搜索时,您会得到一些搜索条件(包括排序和分页),并且结果基本上是ID列表(authorId,postId,commentId)。读取数据时,您将获得一个或多个ID,结果是一个或多个具有所有必需数据属性的DTO。通常需要从多个BC读取数据以填充单个页面,这就是UI组合。

因此,如果我们同意这3点,特别是关注点3,我将提出以下建议:

找出所有要执行的搜索,并查看是否可以将它们分解为BC的简单搜索。例如,按作者名称搜索博客文章是一个问题,因为作者信息可能与博客文章位于不同的BC中。因此,为什么不在Authors BC中实现SearchAuthorByName,然后在Posts BC中实现SearchPostsByAuthorId。您可以从客户端本身或从API执行此操作。在客户端执行此操作可为客户端提供很大的灵活性,因为客户端可以通过多种方式来获取authorId(从MyFavourites列表,分页列表或按名称搜索),然后通过authorId获取帖子是单独的操作。您可以通过标签,类别和其他方式执行相同的操作。该帖子将具有ID,但没有有关这些ID的额外详细信息。

可能,您可能需要更复杂的搜索。只要搜索条件(包括排序字段)包含来自单个BC的字段,您就可以轻松创建一个读取模型并在那里执行搜索。请注意,这仅适用于搜索条件。如果搜索结果需要来自多个BC的数据,则可以使用UI组合来解决。但是,如果搜索条件包含来自多个BC的字段,那么您将需要某种能够对来自多个源的数据进行索引的搜索引擎。如果要进行全文本搜索,按类别,标签等对大量数据进行搜索,这一点尤其明显。您将需要使用一些特殊的服务,例如Elastic Search,它不会属于您现有的任何BC,它将像一项支持服务。