Remote Associations
Remote associations are Jimmer's first thoughts on microservice architecture.
Through vertical splitting, entity types can be divided into different microservices, or in other words, divided into different databases. The associations (including one-to-one, many-to-one, one-to-many and many-to-many) between entity types belonging to different microservices are remote associations.
Jimmer automatically implements automatic query of remote associated data.
That is, if the object fetcher contains remote association properties of any depth, it will query different parts of the data across multiple microservices, and finally merge the query results of different parts into the data structure expected by the object fetcher as a whole return. No need for developers to implement remote queries and data splicing themselves.
Databases
In this article, each entity is divided as follows:
-
BOOK_AUTHOR_MAPPINGas the many-to-many association intermediate table, can be divided into either thebook-serviceservice or theauthor-serviceservice. In this example, it is divided into thebook-serviceservice, the reason will be explained later. -
Solid arrows indicate foreign key references within the same database, whose validity can be freely chosen by developers.
-
Dashed arrows indicate foreign key references across database boundaries, which must be pseudo foreign keys.
To simplify the discussion, the database initialization scripts for the three services are as follows:
| Language | Example | Service (Subproject) | Database Initialization 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 |
The accompanying examples jimmer-examples/java/jimmer-cloud or jimmer-examples/kotlin/jimmer-cloud-kt use H2, each service will automatically initialize its own database after startup.
Entity Definition
In a separate subproject (the accompanying examples are jimmer-examples/java/jimmer-cloud/model or jimmer-examples/kotlin/jimmer-cloud-kt/model), define all entity types.
These entity types constitute the global model. Entity types belong to different microservices. Entities between different microservices constitute remote associations, which can actually be understood as the interaction contracts between entities belonging to different microservices.
- Java
- Kotlin
@Entity(microServiceName = "store-service")
public interface BookStore {
@OneToMany(mappedBy = "store")
List<Book> books();
...other code omitted...
}
@Entity(microServiceName = "store-service")
interface BookStore {
@OneToMany(mappedBy = "store")
val books: List<Book>
...other code omitted...
}
- Java
- Kotlin
@Entity(microServiceName = "book-service")
public interface Book {
@Nullable
@ManyToOne
BookStore store();
@ManyToMany
List<Author> authors();
...other code omitted...
}
@Entity(microServiceName = "book-service")
interface Book {
@ManyToOne
val store: BookStore?
@ManyToMany
val authors: List<Author>
...other code omitted...
}
- Java
- Kotlin
@Entity(microServiceName = "author-service")
public interface Book {
@ManyToMany(mappedBy = "authors")
List<Book> books();
...other code omitted...
}
@Entity(microServiceName = "author-service")
interface Author {
@ManyToMany(mappedBy = "authors")
val books: List<Book>
...other code omitted...
}
-
The
microServiceNameof the@Entityannotation of the entity class is specified, so that it is clear which microservice each entity belongs to, i.e. which database.In this example, the entity types
BookStore,BookandAuthorbelong to the servicesstore-service,book-serviceandauthor-servicerespectively.Here the microservice is the name of each service in the registry center, for spring-cloud it is the global configuration
spring.application.name. -
The associations between entities belonging to different microservices are remote associations.
-
BookStore.booksandBook.store:BookStorebelongs tostore-service, whileBookbelongs tobook-service -
Book.authorsandAuthor.books:Bookbelongs tobook-service, whileAuthorbelongs toauthor-service
-
-
The many-to-one association
Book.storemust be nullable, because the remote association means its foreign key must reference data in another database, that is, it must be a pseudo foreign key. And in Jimmer, the field corresponding to a pseudo foreign key must be nullable.Please refer to Real and Pseudo Foreign Keys.
-
For the bidirectional many-to-many association between books and authors,
Book.authorsis the owning side, whileAuthor.booksis the inverse side (mappedByof@ManyToManyis configured).infoJimmer stipulates that the intermediate table of remote associations must belong to the microservice where the owning side entity belongs.
Here the question left in the previous section is answered, why in this example the intermediate table
BOOK_AUTHOR_MAPPINGis defined in thebook-servicedatabase instead of theauthor-servicedatabase.
Registry Center
Microservice applications need a registry center, which is a basic feature of microservice architecture, no explanation is needed in this article.
In the accompanying examples, it is jimmer-examples/java/jimmer-cloud/registry-center or jimmer-examples/kotlin/jimmer-cloud-kt/registry-center
Implementing Services
Since the implementations of the microservices store-service, book-service and author-service are highly similar, this document only explains how to implement store-service.
Although this document only explains store-service, to experience the effect of remote associations, all three services need to be fully implemented.
Complete code can be found in the accompanying examples jimmer-examples/java/jimmer-cloud or jimmer-examples/kotlin/jimmer-cloud-kt.
Enable Microservice Support in Jimmer
Any microservice must enable Jimmer's microservice support. There are two ways:
-
Use Spring Boot Starter
Modify spring configuration file
application.yml(orapplication.properties)spring:
application:
name: store-service
jimmer:
micro-service-name: ${spring.application.name}
...other configurations omitted...
eureka:
client:
serviceUrl:
defaultZone: http://localhost:7000/eureka/Here, "store-service" is used both as the name of the current service in spring-cloud registry center and as the microservice configuration in Jimmer.
-
When using low-level APIs
- Java
- Kotlin
JSqlClient sqlClient = JSqlClient
.newBuilder()
.setMicroServiceName("...")
...other configurations omitted...
.build();val sqlClient = newKSqlClient {
setMicroServiceName("...")
...other configurations omitted...
}
Once jimmer.micro-service-name is configured, Jimmer knows which entity types belong to the current service (here is BookStore).
-
Using Jimmer's ORM APIs to operate on the entity types belonging to the current microservice will execute correctly, just like everything explained in previous documents.
-
Using Jimmer's ORM APIs to operate on entity types belonging to other microservices will throw exceptions.
As long as each service ensures spring.application.name and jimmer.micro-service-name are the same, Jimmer will automatically implement automatic query of remote associated data without requiring developers to write any code.
Implement Business Query Logic
- Java
- Kotlin
@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()
)
);
@GetMapping("/store/{id}/detail")
fun findStoreDetail(
@PathVariable("id") id: Long
): @FetchBy("COMPLEX_FETCHER") BookStore? =
storeRepository.findNullable(id, COMPLEX_FETCHER)
companion object {
val COMPLEX_FETCHER =
newFetcher(BookStore::class).by { ❶
allScalarFields()
books { ❷
allScalarFields()
authors { ❸
allScalarFields()
}
}
}
}
-
❶ Query root aggregate, queried directly from the database belonging to the current service
store-service -
❷ Query associated objects through remote association property
BookStore.booksAccess remote microservice
book-serviceindirectly to get data via HTTP protocol -
❸ Query associated objects through remote association property
Book.authorsAccess remote microservice
author-serviceindirectly to get data via HTTP protocol
Users only need to implement queries for root aggregate objects, queries for associated properties (including remote associations) are fully automated by Jimmer.
Therefore, even without writing any Java/Kotlin code for the other two microservices book-service and author-service, as long as their configuration files are modified to support microservices, this method can still run correctly.
Run Effects
Assuming store-service is started on localhost with port 7001, accessing http://localhost:7001/store/1/detail will get the result:
{
"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,
...omitted...
},
{
"id":3,
...omitted...
},
{
"id":4,
"name":"Effective TypeScript",
"edition":1,
"price":73,
"authors":[
{
"id":3,
"firstName":"Dan",
"lastName":"Vanderkam",
"gender":"MALE"
}
]
},
{
"id":5,
...omitted...
},
{
"id":6,
...omitted...
},
{
"id":7,
"name":"Programming TypeScript",
"edition":1,
"price":47.5,
"authors":[
{
"id":4,
"firstName":"Boris",
"lastName":"Cherny",
"gender":"MALE"
}
]
},
{
"id":8,
...omitted...
},
{
"id":9,
...omitted...
}
]
}