跳到主要内容

Spring Cloud和远程关联

远程关联是Jimmer对微服务架构的首次思考。

通过垂直切分,实体类型可以被划分到不同的微服务,也可以说被划分到不同的数据库中。隶属于不同的微服务实体类型之间关联 (包括一对一、多对一、一对多和多对多) 就是远程关联。

提示

Jimmer自动实现远程关联数据的自动查询。

即,如果对象抓取器中包含任意深度的远程关联属性,就会跨越多个微服务查询数据的不同部分,最后将不同部分的查询结果合并成对象抓取器所期望的数据结构作为一个整体返回。无需开发人员自己实现远程查询和数据拼接。

数据库

在本文中,各实体如此划分

信息
  • BOOK_AUTHOR_MAPPING作为多对多关联中间表,既可以划分给book-service服务,也可以划分给author-service服务。本例将之划分给book-service服务,原因会在后问阐述。

  • 实线箭头表示同一个数据库中的外键引用,真伪可以由开发人员自由选择。

  • 虚线箭头表示跨越数据库边界的外键引用,只能是伪外键。

为了简化讨论,三个服务的数据库初始化脚本如下

语言例子服务(子项目)数据库初始化SQL
Java

jimmer-examples/java/jimmer-cloud

store-service

jimmer-cloud/store-service/src/main/resources/store.sql

book-service

jimmer-cloud/book-service/src/main/resources/book.sql

author-service

jimmer-cloud/author-service/src/main/resources/author.sql

Kotlin

jimmer-examples/kotlin/jimmer-cloud-kt

store-service

jimmer-cloud-kt/store-service/src/main/resources/store.sql

book-service

jimmer-cloud-kt/book-service/src/main/resources/book.sql

author-service

jimmer-cloud-kt/author-service/src/main/resources/author.sql

信息

附带的例子jimmer-examples/java/jimmer-cloudjimmer-examples/kotlin/jimmer-cloud-kt 使用H2,各服务启动后会自动初始化各自的数据库。

实体定义

在一个独立的子项目中 (附带例子为 jimmer-examples/java/jimmer-cloud/modeljimmer-examples/kotlin/jimmer-cloud-kt/model ),定义所有实体类型。

这些实体类型构成了全局模型,实体类型隶属于不同的微服务。隶属于不同微服务中的实体之间构成了远程关联,其实可以理解成定义在不同微服务之间的实体彼此交互契约。

BookStore.java
@Entity(microServiceName = "store-service")
public interface BookStore {

@OneToMany(mappedBy = "store")
List<Book> books();

...省略其他代码...
}
Book.java
@Entity(microServiceName = "book-service")
public interface Book {

@Nullable
@ManyToOne
BookStore store();

@ManyToMany
List<Author> authors();

...省略其他代码...
}
Author.java
@Entity(microServiceName = "author-service")
public interface Book {

@ManyToMany(mappedBy = "authors")
List<Book> books();

...省略其他代码...
}
  • 实体类的@Entity注解的microServiceName被指定,这样,说明每个实体属于那个微服务,即属于那个数据库。

    在本例中,实体类型BookStoreBookAuthor分别隶属于为服务store-servicebook-serviceauthor-service

    这里的微服务,就是注册中心中每个服务的名称,对于spring-cloud而言,就是全局配置spring.application.name

  • 隶属于不同微服务中的实体之间的关联,就是远程关联。

    • BookStore.booksBook.store: BookStore隶属于store-service,而Book隶属于book-service

    • Book.authorsAuthor.books: Book隶属于book-service,而Author隶属于author-service

  • 多对一关联Book.store必须可null,因为远程关联表示其外键必然引用另外一个数据库中的数据,也就是说必然为伪外键。而Jimmer中伪外键所对应的字段必须可null

    请参考真假外键

  • 对于书籍和作者之间的双向多对多关联而言,Book.authors是主动方,而Author.books是从动方 (@ManyToManymappedBy被配置)

    信息

    Jimmer规定远程关联的中间表必须隶属于主动方实体所属的微服务。

    这里回答了前文留下的问题,为何在本例中中间表BOOK_AUTHOR_MAPPING定义在微服务book-service的数据库中,而不定义在author-service的数据库中。

注册中心

微服务应用需要一个注册中心,这是微服务架构的基本特征,本文无需做任何阐述。

附带例子中,为jimmer-examples/java/jimmer-cloud/registry-centerjimmer-examples/kotlin/jimmer-cloud-kt/registry-center

实现各服务

由于微服务store-servicebook-serviceauthor-service的实现方式高度雷同,所以本文档仅讲解如何实现store-service

备注

虽然本文只讲解store-service,但要运行起来体验远程关联的效果,需要把三个服务都实现完。

完整的代码可参见附带的例子jimmer-examples/java/jimmer-cloudjimmer-examples/kotlin/jimmer-cloud-kt

让Jimmer支持微服务

任何一个微服务,都必须让Jimmer支持微服务。有两种做法:

  • 使用Spring Boot Starter

    修改spring配置文件application.yml (或application.properties)

    spring:
    application:
    name: store-service

    jimmer:
    micro-service-name: ${spring.application.name}
    ...省略其他配置...

    eureka:
    client:
    serviceUrl:
    defaultZone: http://localhost:7000/eureka/

    这里,"store-service"既作为当前服务在spring-cloud注册中心的名称,又作为Jimmer的微服务配置。

  • 使用底层API时

    JSqlClient sqlClient = JSqlClient
    .newBuilder()
    .setMicroServiceName("...")
    ...省略其他配置...
    .build();

一旦配置了jimmer.micro-service-name, Jimmer就明白了哪些实体类型隶属于当前服务 (这里是BookStore)

  • 使用Jimmer的ORM API操作当前微服务的实体类型,将会正确执行,如同之前所有文档所讲那样。

  • 使用Jimmer的ORM API操作其他微服务的实体类型,将会抛出异常。

提示

只要每个服务都保证spring.application.namejimmer.micro-service-name相同,Jimmer就会自动实现远程关联数据的自动查询,无需开发人员编写任何代码。

实现业务的查询

BookStoreService.java
@GetMapping("/store/{id}/detail")
public @FetchBy("COMPLEX_FETCHER") BookStore findStoreDetail(
@PathVariable("id") long id
) {
return storeRepository.findNullable(id, COMPLEX_FETCHER);
}

public static final Fetcher<BookStore> COMPLEX_FETCHER =
Fetchers.BOOK_STORE_FETCHER
.allScalarFields()
.books(
Fetchers.BOOK_FETCHER
.allScalarFields()
.authors(
Fetchers.AUTHOR_FETCHER
.allScalarFields()
)
);
  • ❶ 聚合根查询,从当前服务store-service所属数据库中直接查询

  • ❷ 通过远程关联属性BookStore.books查询关联对象

    通过HTTP协议访问远程微服务book-service间接获取数据

  • ❸ 通过远程关联属性Book.authors查询关联对象

    通过HTTP协议访问远程微服务author-service间接获取数据

提示

用户只需要实现聚合根对象的查询,关联属性 (包括远程关联) 的查询由Jimmer全自动化。

因此,即使不为另外两个微服务book-serviceauthor-service编写任何Java/Kotlin代码,只要修改它们的配置文件支持了微服务,本服务的此方法也可以正确运行。

运行效果

假如store-service在本机被启动且端口为7001,访问http://localhost:7001/store/1/detail,得到的结果为

{
"id":1,
"name":"O'REILLY",
"website":null,
"books":[
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":50,
"authors":[
{
"id":1,
"firstName":"Eve",
"lastName":"Procello",
"gender":"FEMALE"
},
{
"id":2,
"firstName":"Alex",
"lastName":"Banks",
"gender":"MALE"
}
]
},
{
"id":2,
...略...
},
{
"id":3,
...略...
},
{
"id":4,
"name":"Effective TypeScript",
"edition":1,
"price":73,
"authors":[
{
"id":3,
"firstName":"Dan",
"lastName":"Vanderkam",
"gender":"MALE"
}
]
},
{
"id":5,
...略...
},
{
"id":6,
...略...
},
{
"id":7,
"name":"Programming TypeScript",
"edition":1,
"price":47.5,
"authors":[
{
"id":4,
"firstName":"Boris",
"lastName":"Cherny",
"gender":"MALE"
}
]
},
{
"id":8,
...略...
},
{
"id":9,
...略...
}
]
}