Spring在2018年9月发布了Spring-Data-JDBC子项目的1.0.0.RELEASE版本(目前版本为1.0.6-RELEASE),Spring-Data-JDBC设计借鉴了DDD,提供了对DDD的支持,包括:html
在前面领域设计:聚合与聚合根一文中,经过列子介绍了聚合与聚合根;而在领域设计:领域事件一文中,经过例子介绍了领域事件。spring
本文结合Spring-Data-JDBC来重写这两个例子,来看一下Spring-Data-JDBC如何对DDD进行支持。sql
Spring-Data-JDBC项目还较新,文档并不齐全(Spring-Data-JDBC的文档仍是以Spring-Data-JPA为基础编写的,依赖仍是Spring-Data-JPA,实际不须要Spring-Data-JPA依赖),因此这里给出搭建过程当中的注意点。数据库
新建一个maven项目,pom.xml中配置markdown
<!--这里须要引入spring-boot 2.1.0以上,2.0的boot尚未spring-data-jdbc--><parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version></parent><dependencies>
<!--引入spring-data-jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
</dependencies>
复制代码
开启jdbc支持app
@SpringBootApplication
@EnableAutoConfiguration
@EnableJdbcRepositories // 主要是这个注解,来启动spring-data-jdbc支持@EnableTransactionManagement
public class TestConfig {
}
复制代码
领域设计:聚合与聚合根中举了两个列子:dom
咱们先看Order与OrderDetail。数据库设计
Order与OrderDetail组成了一个聚合,其中Order是聚合根,聚合中的操做都是经过聚合根来完成的。maven
在Spring-Data-JDBC中如何表示这一层关系呢?spring-boot
@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;
}
}
复制代码
根据上面的说明,咱们的sql结构以下:
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 '订单主键,由spring-data-jdbc自动维护',
`prod_name` varchar(11) NOT NULL COMMENT '产品名称',
PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
复制代码
对聚合的操做,Spring-Data-JDBC提供了Repository接口,直接实现便可,提供了相似RubyOnRails那样的动态查询方法,不过须要经过Query注解自行编写sql,详见下文。
@Repository
public interface OrderRepository extends CrudRepository<Order, Long> {
}
复制代码
这就搞定了,咱们编写一个测试,来测试一下:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestConfig.class)
public class OrderTest {
@Autowired
private OrderRepository orderRepository;
@Test
public void testInit() {
Order order = new Order("测试订单");
order.addDetail("产品1");
order.addDetail("产品2");
Order info = orderRepository.save(order); // 1
Optional<Order> orderInfoOptional = orderRepository.findById(info.getRecId()); // 2
assertEquals(2, orderInfoOptional.get().getOrderDetailList().size()); // 3
}
}
复制代码
产品与产品评论的关系以下:
代码表示就是简单的经过id进行关联。代码以下:
@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;
}
}
复制代码
对应的sql以下:
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`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 product_comment;
CREATE TABLE `product_comment` (
`rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`product_id` BIGINT(11) NOT NULL COMMENT '产品主键,手动赋值',
`content` varchar(11) NOT NULL COMMENT '评论内容',
PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
复制代码
产品和评论都是聚合根,因此都有各自的仓储类:
@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
}
复制代码
熟悉Mybatis的朋友对这段代码应该很眼熟吧!
测试以下:
@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 info = productRepository.save(prod);
ProductComment comment1 = new ProductComment(info.getRecId(), "评论1"); // 1
ProductComment comment2 = new ProductComment(info.getRecId(), "评论2");
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);
}
}
复制代码
从上面的两个例子能够看出:
对于同一个聚合中的多个实体,能够经过在聚合根中引用对应的实体对象,来实现聚合操做。Spring-Data-JDBC会自动处理这层关系
对于不一样的聚合,经过id的方式进行引用,手动处理二者的关系。这也是领域设计里推荐的作法
若是实体中须要引用其余实体,可是并不想保持一致的操做,那么使用Transient注解
被聚合根引用的实体对象,对应的数据库表中须要一个与聚合根同名的字段,用于保存聚合根的id。这就能够用来区分数据表之间是聚合根与实体的关系,仍是聚合根与聚合根之间的关系
若是表中有一个字段,字段名与另外一张数据表的表名相同,其中保存的是对应的id,那么这张表是对应字段表的实体,对应字段表是聚合根
若是表中的字段是「表名+id」形式,那么两张表都是聚合根,分属于不一样的聚合
若是两个实体之间是多对多的关系,则能够引入一个「关系值对象」,引用方持有这个「关系值对象」来维护关系。对应数据库设计,就是引入一个mapping表,代码以下:
// 来自spring示例 class Book { ...... private Set authors = new HashSet<>(); }
@Table("book_author") class AuthorRef { Long authorId; }
class Author { ...... String name; }
在领域设计:领域事件一文中使用Spring提供的ApplicationEvent演示了领域事件,这里经过对Order聚合根的扩展,来看看Spring-Data-JDBC对领域事件的支持。
假设上面的Order建立后,须要发送一个领域事件,该如何处理呢?
Spring-Data-JDBC默认提供了5个事件:
那么对于上面的需求,咱们不须要建立什么事件,只须要建立一个监听器,来监听AfterSaveEvent事件就能够了。
@Bean
public ApplicationListener<AfterSaveEvent> afterSaveEventListener() {
return event -> {
Object entity = event.getEntity();
if (entity instanceof Order) {
Order order = (Order) entity;
System.out.println("订单[" + order.getName() + "]保存成功");
}
};
}
复制代码
从新执行上面的OrderTest的测试方法,会获得以下输出:
订单[测试订单]保存成功
复制代码
若是咱们须要自定义事件,该如何处理呢?Spring-Data-JDBC提供了DomainEvents和AfterDomainEventPublication注解:
被DomainEvents注解的无参方法,能够返回一个或多个事件
被AfterDomainEventPublication注解的方法,能够用于事件发布后的后续处理工做
这两个方法在repository.save方法执行时被调用
@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("订单[" + event.getName() + "]保存成功"); }; } }
再次执行上面的OrderTest的测试方法,会获得以下输出:
订单[测试订单]保存成功 // 这是AfterSaveEvent事件触发的
订单[测试订单]保存成功 // 这是自定义事件触发的
Event published
复制代码
Spring-Data-JDBC在原来Spring事件的基础上进行了加强:
Spring-Data-JDBC的设计借鉴了DDD。本文演示了Spring-Data-JDBC如何对DDD进行支持:
Spring-Data-JDBC还提供了以下功能:
有兴趣可自行参考文档。