8 Star 63 Fork 15

Thoughtworks/aggregate-persistence

Create your Gitee Account
Explore and code with more than 12 million developers,Free private repositories !:)
Sign up
Clone or Download
contribute
Sync branch
Cancel
Notice: Creating folder will generate an empty file .keep, because not support in Git
Loading...
README
Apache-2.0

Aggregate Persistence

可参考:

1. 简介

领域驱动设计(DDD)已经被业界认为是行之有效的复杂问题解决之道。随着微服务的流行,DDD也被更多的团队采纳。然而在DDD落地时,聚合(Aggregate)的持久化一直缺少一种优雅的方式解决。

在DDD实践中,聚合应该作为一个完整的单元进行读取和持久化,以确保业务的不变性或者说业务规则不变破坏。例如,订单总金额应该与订单明细金额之和一致。

由于领域模型和数据库的数据模型可能不一致,并且聚合可能涉及多个实体,因此Hibernate, MyBatis和Spring Data等框架直接用于聚合持久化时,总是面临一些困难,而且代码也不够优雅。有人认为NoSQL是最适合聚合持久化的方案。确实如此,每个聚合实例就是一个文档,NoSQL天然为聚合持久化提供了很好的支持。然而并不是所有系统都适合用NoSQL。当遇到关系型数据库时,一种方式是将领域事件引入持久化过程。也就是在处理业务过程中,聚合抛出领域事件,Repository根据领域事件的不同,执行不同的SQL,完成数据库的修改。但这样的话,Repository层就要引入一些逻辑判断,代码冗余增加了维护成本。

本项目旨在提供一种轻量级聚合持久化方案,帮助开发者真正从业务出发设计领域模型,不需要考虑持久化的事情。在实现Repository持久化时,不需要考虑业务逻辑,只负责聚合的持久化,从而真正做到关注点分离。也就是说,不论有多少个业务场景对聚合进行了修改,对聚合的持久化只需要一个方法。

方案的核心是Aggregate<T>容器,T是聚合根的类型。Repository以Aggregate<T>为核心,当Repository查询或保存聚合时,返回的不是聚合本身,而是聚合容器Aggregate<T>。以订单付款为例,Application Service的代码如下:

@Transactional
public void checkout(String orderId, CheckoutRequest request) {
    Aggregate<Order> aggregate = orderRepository.findById(orderId);
    Order order = aggregate.getRoot();

    Payment payment = new Payment(PaymentType.from(request.getPaymentType()), request.getAmount());
    order.checkout(payment);

    orderRepository.save(aggregate);
}

Aggregate<T>保留了聚合的历史快照,因此在Repository保存聚合时,就可以与快照进行对比,找到需要修改的实体和字段,然后完成持久化工作。它提供以下功能:

  • public R getRoot():获取聚合根
  • public R getRootSnapshot(): 获取聚合根的历史快照
  • public boolean isChanged(): 聚合是否发生了变化
  • public boolean isNew():是否为新的聚合
  • public <T> Collection<T> findNewEntitiesById(Function<R, Collection<T>> getCollection, Function<T, ID> getId):在实体集合(例如订单的所有订单明细行中)找到新的实体
  • public <T, ID> Collection<T> findChangedEntities(Function<R, Collection<T>> getCollection, Function<T, ID> getId):在实体集合(例如所有订单明细行中)找到发生变更的实体
  • public <T, ID> Collection<T> findRemovedEntities(Function<R, Collection<T>> getCollection, Function<T, ID> getId):在实体集合(例如所有订单明细行中)找到已经删除的实体

工具类DataObjectUtils提供了对象的对比功能。它可以帮助你修改数据库时只update那些变化了的字段。以Person为例,DataObjectUtils.getChangedFields(personSnapshot, personCurrent)将返回哪些Field发生了变化。你可以据此按需修改数据库(请参考示例工程)。

与Hibernate的@Version类似,聚合根需要实现Versionable接口,以便Repository基于Version实现乐观锁。Repository对聚合的所有持久化操作,都要判断Version。示意SQL如下:

    insert into person (id, name, age, address, version )
    values (#{id}, #{name}, #{age}, #{address}, 1)

    update person set age = #{age}, address = #{address}, version = version + 1
    where id = #{id} and version = #{version}
    
    delete person
    where id = #{id} and version = #{version}

2. 使用Aggregate-Persistence

在项目中加入以下依赖,就可以使用Aggregate-persistence的功能了:

        <dependency>
            <groupId>com.github.meixuesong</groupId>
            <artifactId>aggregate-persistence</artifactId>
            <version>1.2.1</version>
        </dependency>

3. 使用示例

Aggregate-Persistence本身并不负责持久化工作,它是一个工具,用于识别聚合的变更,例如发现有新增、修改和删除的实体,真正的持久化工作由你的Repository实现。

接下来我们通过订单聚合持久化项目展示Repository如何利用Aggregate-Persistence的功能,实现订单聚合的持久化。该项目的技术栈使用Springboot, MyBatis。

订单聚合包括两个实体:订单(Order)和订单明细行(OrderItem),其中订单是聚合根:

public class Order implements Versionable {
    private String id;
    private Date createTime;
    private Customer customer;
    private List<OrderItem> items;
    private OrderStatus status;
    private BigDecimal totalPrice;
    private BigDecimal totalPayment;
    private int version;
}

public class OrderItem {
    private Long id;
    private Product product;
    private BigDecimal amount;
    private BigDecimal subTotal;
}

OrderRepository完成订单的持久化工作,主要方法如下:

public class OrderRepository {
    Aggregate<Order> findById(String orderId);
    void save(Aggregate<Order> orderAggregate);
    void remove(Aggregate<Order> orderAggregate);
}

在本例中,OrderRepository需要完成订单的新增、订单项的修改(如购买数量变化或者移除了某个商品)、订单的删除功能。由于领域模型与数据模型不一致,因此保存时,Repository将Domain model(Order)转换成Data object(OrderDO),然后使用MyBatis完成持久化。查询时,进行反向操作,将Data object转换成Domain model.

3.1 查询订单

下面的代码用于查询订单,并返回Aggregate<Order>。当查询数据库并创建Order聚合后,调用AggregateFactory.createAggregate创建Aggregate<T>对象,在Aggregate<T>内部,它将自动保存Order的快照,以供后续对比。

public Aggregate<Order> findById(String id) {
    OrderDO orderDO = orderMapper.selectByPrimaryKey(id);
    if (orderDO == null) {
        throw new EntityNotFoundException("Order(" + id + ") not found");
    }

    Order order = orderDO.toOrder();
    order.setCustomer(customerRepository.findById(orderDO.getCustomerId()));
    order.setItems(getOrderItems(id));

    return AggregateFactory.createAggregate(order);
}

3.2 保存新增订单、修改订单

使用save接口方法完成订单及订单明细行的新增、修改和删除操作。示例代码如下:

void save(Aggregate<Order> orderAggregate) {
    if (orderAggregate.isNew()) {
        //insert order
        Order order = orderAggregate.getRoot();
        orderMapper.insert(new OrderDO(order));
        //insert order items
        List<OrderItemDO> itemDOs = order.getItems().stream()
            .map(item -> new OrderItemDO(order.getId(), item))
            .collect(Collectors.toList());
        orderItemMapper.insertAll(itemDOs);
    } else if (orderAggregate.isChanged()) {
        //update order 
        updateAggregateRoot(orderAggregate);
        //delete the removed order items from DB
        removeOrderItems(orderAggregate);
        //update the changed order items
        updateOrderItems(orderAggregate);
        //insert the new order items into DB
        insertOrderItems(orderAggregate);
    }
}

上例代码中,当orderAggregate.isNew()为true时,调用MyBatis Mapper插入数据。否则如果聚合已经被修改,则需要更新数据。

首先更新聚合根。领域对象(Order)首先被转换成数据对象(OrderDO),然后DataObjectUtils对比OrderDO的历史版本,得到Delta值,最终调用MyBatis的update selective方法更新到数据库中。代码如下:

private void updateAggregateRoot(Aggregate<Order> orderAggregate) {
    //only update changed fields, avoid update all fields
    OrderDO newOrderDO = new OrderDO(orderAggregate.getRoot());
    Set<String> changedFields = DataObjectUtils.getChangedFields(orderAggregate.getRootSnapshot(), orderAggregate.getRoot());
    if (orderMapper.updateByPrimaryKeySelective(newOrderDO, changedFields) != 1) {
        throw new OptimisticLockException(String.format("Update order (%s) error, it's not found or changed by another user",
                orderAggregate.getRoot().getId()));
    }
}

对于订单明细行的增删改,都是通过Aggregate找到新增、删除和修改的实体,然后完成数据库操作。代码示例如下:

private void removeOrderItems(Aggregate<Order> orderAggregate) {
    Collection<OrderItem> removedEntities = orderAggregate.findRemovedEntities(Order::getItems, OrderItem::getId);
    removedEntities.stream().forEach((item) -> {
        if (orderItemMapper.deleteByPrimaryKey(item.getId()) != 1) {
            throw new OptimisticLockException(String.format("Delete order item (%d) error, it's not found", item.getId()));
        }
    });
}

private void updateOrderItems(Aggregate<Order> orderAggregate) {
    Collection<ChangedEntity<OrderItem>> entityPairs = orderAggregate.findChangedEntitiesWithOldValues(Order::getItems, OrderItem::getId);
    for (ChangedEntity<OrderItem> pair : entityPairs) {
        Set<String> changedFields = DataObjectUtils.getChangedFields(pair.getOldEntity(), pair.getNewEntity());
        OrderItemDO orderItemDO = new OrderItemDO(orderAggregate.getRoot().getId(), pair.getNewEntity());
        if (orderItemMapper.updateByPrimaryKeySelective(orderItemDO, changedFields) != 1) {
            throw new OptimisticLockException(String.format("Update order item (%d) error, it's not found", orderItemDO.getId()));
        }
    }
}

private void insertOrderItems(Aggregate<Order> orderAggregate) {
    Collection<OrderItem> newEntities = orderAggregate.findNewEntities(Order::getItems, (item) -> item.getId() == null);
    if (newEntities.size() > 0) {
        List<OrderItemDO> itemDOs = newEntities.stream().map(item -> new OrderItemDO(orderAggregate.getRoot().getId(), item)).collect(Collectors.toList());
        orderItemMapper.insertAll(itemDOs);
    }
}

Aggregate<T>提供的findXXXEntities系列方法,都是针对订单明细行这样的实体集合。例如订单明细中,可能增加了商品A,修改了商品B的数量,删除了商品C。findXXXEntities方法用于找出这些变更。第1个参数是函数式接口,用于获取实体集合,以便在此集合中识别新增、修改和删除的实体。第2个参数也是函数式接口,获得实体主键值。

需要提醒的是,当聚合发生变化时,不论聚合根是否发生变化,都应该修改聚合根的版本号,以确保聚合作为一个整体被修改,避免并发修改时产生的数据不一致现象。

3.3 删除订单

删除订单的同时,需要删除所有订单明细行。

public void remove(Aggregate<Order> aggregate) {
    Order order = aggregate.getRoot();
    if (orderMapper.delete(new OrderDO(order)) != 1) {
        throw new OptimisticLockException(
            String.format("Delete order (%s) error, it's not found or changed by another user", order.getId())
        );
    }
    orderItemMapper.deleteByOrderId(order.getId());
}

完整的示例代码见订单聚合持久化项目,该示例演示了如何运用Mybatis实现聚合的持久化,并且只持久化那些修改的数据。例如一个表有20个字段,只有1个字段修改了,采用此方案时,只会修改数据库的一个字段,而非所有字段。

4. 总结

总的来说,本项目提供了一种轻量级聚合持久化方案,能够帮助开发者设计干净的领域模型的同时,很好地支持Repository做持久化工作。通过持有聚合根的快照,Aggregate<T>可以识别聚合发生了哪些变化,然后Repository使用基于Version的乐观锁和DataObjectUtils在字段属性级别的比较功能,实现按需更新数据库。

5. Changelog

1.2 修改了之前采用对比字段值,如果为null时判断为未修改的方式。新方式改为使用DataObjectUtils.getChangedFields获取变更的字段名。

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

About

本项目旨在提供一种轻量级聚合持久化方案,帮助开发者真正从业务出发设计领域模型,不需要考虑持久化的事情。在实现Repository持久化时,不需要考虑业务逻辑,只负责聚合的持久化,从而真正做到关注点分离。也就是说,不论有多少个业务场景对聚合进行了修改,对聚合的持久化只需要一个方法。 expand collapse
Java
Apache-2.0
Cancel

Releases

No release

Contributors

All

Activities

Load More
can not load any more
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/thoughtworks/aggregate-persistence.git
git@gitee.com:thoughtworks/aggregate-persistence.git
thoughtworks
aggregate-persistence
aggregate-persistence
master

Search