前两天在选择Demo工程的框架的时候,选了从未用过的Spring Data JDBC。在官方给出的Examples里,发现有个单独的模块jmolecules-example

我感觉挺惊讶的,Spring官方竟然给一个在Github上star不超过400(截止到2021-4-22)的项目给了单独的使用示例。

但是,我看到这个项目的第一眼就star上了。用注解和接口表达领域驱动设计中的概念,实在是太coooool了。

所以,我就决定将这个示例jmolecules-example,clone下来研究一下。

为啥测试用例通过不了?

所有Spring Data示例在一个repository里简直太折磨人了,clone下来下载jar包都得需要好长时间。

下载完之后,运行测试用例就报错了。

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ’example.springdata.jdbc.jmolecules.customer.Customers’ available

一脸懵逼,官方示例不应该有问题。

但是,这个错误很明显,没找到Customers这个Repository

让我们来看看Customers的代码。

1
2
3
public interface Customers extends Repository<Customer, CustomerId>, AssociationResolver<Customer, CustomerId> {
	Customer save(Customer customer);
}

用过Spring Data JPA的都知道,我们自定义一个接口然后继承org.springframework.data.repository.Repository就行了。但是这里,继承的是org.jmolecules.ddd.types.Repository。所以上面的错误,肯定跟这个有关系。所以,就必须了解这个jMolecules了。

jMolecules

A set of libraries to help developers work with architectural concepts in Java. Goals:

  • Express that a piece of code (package, class, method…) implements an architectural concept.
  • Make it easy for the human reader to determine what kind of architectural concepts a given piece of code is.
  • Allow tool integration (to do interesting stuff like generating persistence or static architecture analysis to check for validations of the architectural rules.)

从Github上的介绍以及源码可以看出来,jMolecules只是提供了一些标志性接口注解而已。

比如: 使用@ValueObject来标志领域驱动设计中的值对象

如果这个jMolecules只有标识作用,是不是显得没有那么实用?因此,还有另一个叫jmolecules-integrations的项目。

jMolecules — Technology integrations

正是这个项目,可以将jMolecules转换成Spring Data中的接口及注解。

从示例的pom.xml中可以看到就有这些依赖。

而重点就是下面这个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<plugin>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-maven-plugin</artifactId>
    <version>${byte-buddy.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>transform</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.jmolecules.integrations</groupId>
            <artifactId>jmolecules-bytebuddy</artifactId>
            <version>${jmolecules-integration.version}</version>
        </dependency>
    </dependencies>
</plugin>

使用这个插件在target目录下生成class文件,测试用例就可以通过了。

生成的Customers代码如下:

1
2
3
public interface Customers extends Repository<Customer, CustomerId>, AssociationResolver<Customer, CustomerId>, org.springframework.data.repository.Repository<Customer, CustomerId> {
    Customer save(Customer customer);
}

可以,看到生成的class文件里多了org.springframework.data.repository.Repository接口。

Spring Data JDBC示例分析

示例里,更多的还是关于Spring Data JDBC的内容,而jMolecules相当于起了一个辅助的作用。

下面,分析一下这个示例的代码。

1
2
3
4
5
6
7
8
--customer
  |--Address
  |--Customer
  |--Customers
--order
  |--LineItem
  |--Order
  |--Orders

CustomersOrders很简单,分别是聚合根CustomerOrderRepository

所以重点还是CustomerOrder

一对一

Customer是一个典型的一对一关系示例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Getter
@ToString
@AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = @__(@PersistenceConstructor))
public class Customer implements AggregateRoot<Customer, CustomerId> {

	private final CustomerId id;
	private @Setter String firstname, lastname;
	private List<Address> addresses;

	public Customer(String firstname, String lastname, Address address) {

		Assert.notNull(address, "Address must not be null!");

		this.id = CustomerId.of(UUID.randomUUID().toString());

		this.firstname = firstname;
		this.lastname = lastname;

		this.addresses = new ArrayList<>();
		this.addresses.add(address);
	}

	@Value(staticConstructor = "of")
	public static class CustomerId implements Identifier {
		private final String id;
	}
}
1
2
3
4
5
@Value
@ValueObject
public class Address {
	private final String street, city, zipCode;
}

这里有Lombok的注解中的@PersistenceConstructor,其实意思就是在构造方法上加上这个@PersistenceConstructor注解,它的作用可以在Spring Data JDBC文档上了解到。

1
@AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = @__(@PersistenceConstructor))

虽然,代码里使用的是List,但是这里CustomerAddress确实是一对一的。这里jMolecules通过AggregateRoot接口,让我们不再需要加上@Id注解(org.springframework.data.annotation.@Id)。

而且,对于Spring Data JDBC只需要@Id注解就够了,甚至不需要setter和getter方法。Spring Data不愧是贯彻领域驱动设计概念的框架。有时候,有些框架强制使用setter和getter方法就让我觉得很别扭。

CustomerAddress的建表语句可就有点意思了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
CREATE TABLE IF NOT EXISTS customer (
	id VARCHAR(100) PRIMARY KEY,
	firstname VARCHAR(100),
	lastname VARCHAR(100)
);

CREATE TABLE IF NOT EXISTS address (
	street VARCHAR(100),
	city VARCHAR(100),
	zip_code VARCHAR(100),
	customer VARCHAR(100),
	customer_key VARCHAR(100)
);

Address是一个值对象,它随customer产生也随之消亡。

我们不需要做任何操作,在customer保存的时候,会将customerid保存到addresscustomer字段。

customer_key,以我的理解对应的是customer中List的index。

customer保存后的数据如下:

IDFIRSTNAMELASTNAME
52eb4e05-4371-4009-a03e-2fbdc3a5963dCarterMatthews

address的数据如下:

STREETCITYZIP_CODECUSTOMERCUSTPMER_KEY
41 GreystreetDreaming Tree273152eb4e05-4371-4009-a03e-2fbdc3a5963d0

一对多

OrderLineItem就是一对多的关系了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Table("MY_ORDER")
@Getter
@ToString
@RequiredArgsConstructor
public class Order implements AggregateRoot<Order, Order.OrderId> {

	private final OrderId id;
	private @Column("FOO") List<LineItem> lineItems;
	private Association<Customer, CustomerId> customer;

	public Order(Customer customer) {

		this.id = OrderId.of(UUID.randomUUID());
		this.customer = Association.forAggregate(customer);
		this.lineItems = new ArrayList<>();
	}

	public Order addLineItem(String description) {

		LineItem item = new LineItem(description);

		this.lineItems.add(item);

		return this;
	}

	@Value(staticConstructor = "of")
	public static class OrderId implements Identifier {

		private final UUID orderId;

		public static OrderId create() {
			return OrderId.of(UUID.randomUUID());
		}
	}
}

这其实跟一对一是非常类似的。唯一的点就是使用了@Table指定了表名和@Column指定了列名,而且是line_item表的列名。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
CREATE TABLE IF NOT EXISTS my_order (
	id VARCHAR(100),
	customer VARCHAR(100)
);

CREATE TABLE IF NOT EXISTS line_item (
	my_order_key VARCHAR(100),
	description VARCHAR(100),
	foo VARCHAR(100)
);

示例执行后my_order表的数据如下:

IDCUSTOMER
1a02ef53-db71-47ef-8985-88f907800dc852eb4e05-4371-4009-a03e-2fbdc3a5963d

示例执行后line_item表的数据如下:

MY_ORDER_KEYDESCRIPTIONFOO
0Foo1a02ef53-db71-47ef-8985-88f907800dc8
1Bar1a02ef53-db71-47ef-8985-88f907800dc8

多对一

CustomerOrder就是多对一

上面的一对一,一对多。都是聚合根被删除了,其包含的值对象也应该被删除,这是合理的。但是CustomerOrder都是聚合根。一个顾客可以有多个订单,订单删除了,顾客不能删除。

这种情况在Spring Data JDBC该怎么办呢。

在Spring官网的博客中有篇文章中讲到过处理方式。Spring Data JDBC, References, and Aggregates

对应于,这个示例,就是让Order持有CustomerID,而非对象。

所以,这里jMolecules就发挥作用了。使用Association来封装这个ID

如果是这样,Spring Data JDBC怎么知道jMolecules的封装,又该怎么解析Association呢?

那就是,JMolecules Spring integration的自定义转换器PrimitivesToIdentifierConverterPrimitivesToAssociationConverter。在Spring Data JDBCCustom Conversions有自定义转换器的介绍。

H2

示例中使用的H2数据库。所以除了只需要一个schema.sql外,不需要什么配置信息。

如果需要,查看H2数据库的数据。需要添加application.properties配置

1
spring.h2.console.enabled=true

另外,需要加入web的依赖。

然后在项目启动的时候,可以看到访问路径为/h2-console

类似下面的这种连接的url也可以看到

jdbc:h2:mem:0dbb7bce-452c-4d12-b9e3-1ca04479f6f5;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false

总结

不管是Spring Data JDBC, 还是jMolecules都是非常优秀的项目了。就连官方的给示例项目的很经典,可谓是以最少的代码表达出了你想知道的内容。

之前在还在网上看到有博客的作者说,Spring Data JDBC很鸡肋,我不清楚当时的作者为什么会有这样的言论,是不是真的了解过Spring Data JDBC

我只想说,Spring Data JDBC, yyds(永远滴神)