In September 2018, Spring released the 1.0.0.RELEASE of the Spring-data-JDBC subproject (currently 1.0.6-release). The spring-data-JDBC design references DDD and provides DDD support, including:

  • Polymerization and polymerization roots
  • warehousing
  • Field events

In the previous domain design: aggregations and aggregations roots article, we introduced aggregations and aggregations roots by examples. In domain Design: Domain events, domain events are introduced by examples.

This article rewrites these two examples in conjunction with Spring-data-JDBC to see how SPRing-data-JDBC supports DDD.

Environment set up

The spring-data-JDBC project is still relatively new, and the documentation is not complete (the spring-data-JDBC documentation is still written on the basis of spring-data-JPA and relies on spring-data-JPA, which does not actually require the spring-data-JPA dependency). So here are the points to pay attention to in the construction process.

Create a new Maven project and configure it in POM.xml

<! Spring-boot 2.1.0 or above <parent> <groupId>org.springframework.boot</groupId> The < artifactId > spring - the boot - starter - parent < / artifactId > < version > 2.1.4. RELEASE < / version > < / parent > < dependencies > <! <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> </dependencies>Copy the code

Enabling JDBC Support

@springBootApplication @enableAutoConfiguration@EnableJdbcRepositories // Mainly this annotation, To start the spring - data - JDBC support @ EnableTransactionManagement public class TestConfig {}Copy the code

Polymerization and polymerization roots

Domain design: aggregations and root aggregations are two examples:

  • The relationship between Order and OrderDetail
  • Relationship between Product and ProductComment
  • Let’s implement these two examples with Spring-data-JDBC, and take a look at spring-data-JDBC support for aggregation and aggregation roots.

So let’s look at Order and Order detail.

Order and Details

Order and OrderDetail form an aggregation, where Order is the aggregation root through which all operations in the aggregation are performed.

How do you represent this layer of relationship in spring-data-JDBC?

@Getter // 1 @Table("order_info") // 2 public class Order { @Id // 3 private Long recId; private String name; private Set<OrderDetail> orderDetailList = new HashSet<>(); // 4 public Order(String name) { // 5 this.name = name; Public void addDetail(String prodName) {// 6 orderdetaillist. add(new OrderDetail(prodName)); } } @Getter // 1 public class OrderDetail { @Id // 3 private Long recId; private String prodName; OrderDetail(String prodName) {// 7 this. ProdName = prodName; }}Copy the code
  1. Lombok annotations, which only provide get methods, encapsulate operations
  2. By default, the mapping between class names and table names is
  • The first letter of the class name is lowercase
  • Hump underline
  • Here order is the keyword in the database, so use the Table annotation to map to the ORDER_INFO Table
  1. The @ID annotation indicates that the class is an entity
  2. A Set that holds an OrderDetail in Order, indicating that the Order and OrderDetail form an aggregation and that Order is the root of the aggregation
  • Aggregation relationships are maintained by default by Spring-data-JDBC
  • If it is a Set, then the order_detail table needs to have an order_INFO field holding the order primary key
  • If it is a List collection, the order_detail table needs to have two fields :order_info to hold the primary key of the order, and order_info_key to hold the order
  1. Neither class provides a set method, which is assigned by a constructor
  2. Order is the aggregation root through which all operations are performed, and the addDetail method is provided to add Order details
  3. Because all operations on OrderDetail are done through Order, making the OrderDetail constructor package-level visible limits the external build of OrderDetail

According to the above instructions, our SQL structure is as follows:

DROP TABLE IF EXISTS `order_info`; CREATE TABLE 'order_info' (' rec_id 'BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键', 'name' varchar(11) NOT NULL COMMENT '表 名 ', PRIMARY KEY (' rec_id')) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS order_detail; CREATE TABLE 'order_detail' (' rec_id 'BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键', 'order_info' BIGINT(11) NOT NULL COMMENT ' ', 'prod_name' varchar(11) NOT NULL COMMENT 'product name ', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;Copy the code

Spring-data-jdbc provides a Repository interface for aggregation, which provides dynamic Query methods like RubyOnRails, but requires you to write your own SQL through Query annotations, as described below.

@Repository
public interface OrderRepository extends CrudRepository<Order, Long> {
}
Copy the code
  • An interface that inherits the CrudRepository interface provides basic queries that can be used directly

That’s it. Let’s write a test to test it:

@RunWith(SpringRunner.class) @SpringBootTest(classes = TestConfig.class) public class OrderTest { @Autowired private OrderRepository orderRepository; @test public void testInit() {Order Order = new Order(" Test Order "); Order. AddDetail (" 1 "); Order. AddDetail (" products "); Order info = orderRepository.save(order); // 1 Optional<Order> orderInfoOptional = orderRepository.findById(info.getRecId()); // 2 assertEquals(2, orderInfoOptional.get().getOrderDetailList().size()); / / 3}}Copy the code
  1. The save operation is performed directly using the provided save method, which automatically handles the aggregation relationship, that is, the order and its two order_details are saved automatically
  2. Order is found using the provided findById, which returns an Optional type
  3. Order_detail is automatically assembled in the Order returned. The corresponding delete operation also automatically deletes its associated order_detail

Products and Reviews

The relationship between product and product review is as follows:

  • There is no business consistency requirement for products and product reviews, so two “aggregations”
  • Product reviews are associated with product aggregation via productId

The code representation is simply associated by ID. The code is as follows:

@Getter public class Product { // 1 @Id private Long recId; private String name; public Product(String name) { this.name = name; @getter public class ProductComment {@id private Long recId; private Long productId; // 2 private String content; Public ProductComment(Long productId, String content) {this.productId = productId; this.content = content; }}Copy the code
  1. The corresponding collection is no longer held in Product
  2. Accordingly, ProductComment holds the product primary key field

The SQL is as follows:

DROP TABLE IF EXISTS `product`; CREATE TABLE 'product' (' rec_id 'BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键', 'name' varchar(11) NOT NULL COMMENT 'InnoDB ', PRIMARY KEY (' rec_id')) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS product_comment; CREATE TABLE 'product_COMMENT' (' rec_id 'BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键', 'product_id' BIGINT(11) NOT NULL COMMENT ' Varchar (11) NOT NULL COMMENT 'COMMENT ', PRIMARY KEY (' rec_id')) ENGINE=InnoDB DEFAULT CHARSET=utf8;Copy the code

Products and reviews are aggregate roots, so each has its own warehousing category:

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
}

@Repository
public interface ProductCommentRepository extends CrudRepository<ProductComment, Long> {

 @Query("select count(1) from product_comment where product_id = :productId") // 1
 int countByProductId(@Param("productId") Long productId); // 2

}
Copy the code
  1. Bind the relationship between SQL and methods using the Query annotation, which begins with:. (Spring-data-JDBC does not currently support automatic SQL binding)
  2. Param annotation to indicate the parameter name, or use jdK8’s -parameters compiler to automatically bind to the parameter name

Familiar with Mybatis friends to this code should be very familiar!

The tests are as follows:

@RunWith(SpringRunner.class) @SpringBootTest(classes = TestConfig.class) public class ProductTest { @Autowired private ProductRepository productRepository; @Autowired private ProductCommentRepository productCommentRepository; @test public void testInit() {Product prod = new Product(" Product name "); Product info = productRepository.save(prod); ProductComment comment1 = new ProductComment(info.getrecid (), "comment1 "); // 1 ProductComment comment2 = new ProductComment(info.getrecid (), "comment2 "); // 1 ProductComment comment2 = new ProductComment(info.getrecid ()," comment2 "); productCommentRepository.save(comment1); int num = productCommentRepository.countByProductId(info.getRecId()); assertEquals(1, num); productCommentRepository.save(comment2); num = productCommentRepository.countByProductId(info.getRecId()); assertEquals(2, num); productRepository.delete(info); // 2 num = productCommentRepository.countByProductId(info.getRecId()); assertEquals(2, num); }}Copy the code
  1. Products and reviews are saved separately
  2. When a product is deleted, reviews are not deleted. If you need to delete them, you need to manually delete them.

Polymerization section

As can be seen from the two examples above:

  • For multiple entities in the same aggregation, you can implement aggregation by referring to the corresponding entity object in the root of the aggregation. Spring-data-jdbc handles this relationship automatically

  • For different aggregations, reference them by ID, and handle the relationship between them manually. This is also recommended in domain design

  • Use Transient annotations if you need to reference other entities in an entity but do not want to maintain consistent operations

  • The entity object referenced by the aggregate root needs a field in the database table with the same name as the aggregate root to store the ID of the aggregate root. This can be used to distinguish between tables that are aggregate root to entity or aggregate root to aggregate root

  • If there is a field in a table with the same name as another table with the corresponding ID stored in it, then the table is the entity of the corresponding field table and the corresponding field table is the aggregation root

  • If the fields in a table are of the form “table name + ID”, then both tables are aggregation roots and belong to different aggregates

  • If there is a many-to-many relationship between two entities, you can introduce a “relational value object” that the referrer holds to maintain the relationship. The corresponding database design is to introduce a mapping table, the code is as follows:

    // from the Spring example class Book {…… private Set authors = new HashSet<>(); }

    @Table(“book_author”) class AuthorRef { Long authorId; }

    class Author { …… String name; }

Field events

Domain events are demonstrated using spring-provided ApplicationEvents in Domain Design: Domain Events. Here we look at spring-data-JDBC support for domain events by extending the Order aggregation root.

Suppose that after the Order above is created, a domain event needs to be sent. What should be done?

Spring-data-jdbc provides five events by default:

  • BeforeDeleteEvent: The aggregate root fires before it is deleted
  • AfterDeleteEvent: The aggregate root fires after it has been deleted
  • BeforeSaveEvent: The aggregate root fires before it is saved
  • AfterSaveEvent: The aggregate root fires after it has been saved
  • AfterLoadEvent: The aggregate root fires after being recovered from the warehouse

For the above requirements, we do not need to create an event, just create a listener to listen for AfterSaveEvent events.

@Bean public ApplicationListener<AfterSaveEvent> afterSaveEventListener() { return event -> { Object entity = event.getEntity(); if (entity instanceof Order) { Order order = (Order) entity; System.out.println(" order [" + order.getName() + "] saved successfully "); }}; }Copy the code

Re-executing the OrderTest test method above yields the following output:

Order [Test order] saved successfullyCopy the code

What if we need to customize events? Spring – Data – JDBC provides DomainEvents and AfterDomainEventPublication comments:

  • A method with no arguments annotated by DomainEvents that can return one or more events

  • Be AfterDomainEventPublication annotation methods, which can be used for event after the release of the follow-up work

  • Both methods are called when the repository.save method is executed

    @Getter public class OrderCreateEvent extends ApplicationEvent { // 1 private String name; public OrderCreateEvent(Object source, String name) { super(source); this.name = name; }}

    @Getter @Table(“order_info”) public class Order { …… @DomainEvents public ApplicationEvent domainEvent() { // 2 return new OrderCreateEvent(this, this.name); } @AfterDomainEventPublication public void postPublish() { // 3 System.out.println(“Event published”); }}

    public class TestConfig { …… @bean public ApplicationListener orderCreateEventListener() {// 4 return event -> {system.out.println (” order [” +) Event.getname () + “] Saved successfully “); }; }}

  1. Customize an event, specific to the visible domain design: domain event
  2. The DomainEvents annotation method creates an OrderCreateEvent event when the repository.save method is called, passing in the order name as a parameter
  3. AfterDomainEventPublication annotation method after the event publishing, for correction, can handle events after the release of some processing, simply print here
  4. OrderCreateEvent Listens for the event to process

Executing the OrderTest test method above again yields the following output:

Order [test order] saved successfully // This is the order triggered by the AfterSaveEvent saved successfully // This is the custom Event triggered by the Event publishedCopy the code

Events section

Spring-data-jdbc has been enhanced from the original Spring events:

  • Five events related to aggregate root operations have been added
  • Simplified event publishing with the DomainEvents annotation (only triggered when repository.save)
  • Through AfterDomainEventPublication annotation processing event after the release of the callback (only in the repository. Save the trigger)
  • The AbstractAggregateRoot abstract class is provided to further simplify event handling

conclusion

The design of Spring-data-JDBC borrows from DDD. This article demonstrates how spring-data-JDBC supports DDD:

  • Automatically handles the relationship between aggregate roots and entities
  • Default warehouse interface, simplified aggregate storage
  • Annotations to simplify the publication of domain events

Spring-data-jdbc also provides the following functions:

  • MyBatis support
  • Id generation
  • Auditing
  • CustomConversions

If you are interested, please refer to the documentation.

The resources

  • Spring-data-jdbc documentation: docs.spring. IO /spring-data…
  • Application Code: Code code: Code code: Code code: Code code: Code code: Code code: Code code