Save Mode of Associated Objects
Basic Concepts
In the previous article, we introduced how to control the save mode of aggregate root objects.
This article will discuss how to control the save mode of associated objects. Associated objects have three save modes:
-
REPLACE (default strategy)
This strategy includes two aspects of capabilities:
-
Save the associated objects specified by the user.
First, determine whether the associated object already exists in the database.
-
If the id property of the associated object is specified, determine whether the same object exists in the existing data based on the id.
-
Otherwise, determine based on the key properties of the associated object.
Different actions are taken based on the different determination results:
-
If the associated object already exists, perform an UPDATE operation.
-
Otherwise, perform an INSERT operation.
-
-
Dissociate the other associated objects that the user did not specify.
If some associated objects exist in the database but do not exist in the data structure being saved, the unneeded associated objects will be dissociated.
The dissociation operation is affected by other configurations. Depending on the different configurations, the operation that may ultimately be executed is raising an error, clearing the foreign key of the associated objects, or even deleting the associated objects.
The dissociation operation is not the focus of this article. Please refer to the relevant chapter.
-
-
MERGE
Compared with
REPLACE
, the behavior of saving the associated objects specified by the user is exactly the same; however, theMERGE
operation will not trigger the dissociation operation. -
APPEND
This mode differs significantly from the previous two modes. Apart from unconditionally performing INSERT on the associated objects, there are no additional operations. Therefore, naturally, the key configuration of the associated object is not required.
For ease of explanation, this article has adjusted the order of discussion based on the difficulty level: APPEND
, MERGE
, REPLACE
.
Two Configuration Methods
Jimmer provides some shortcut methods that can quickly configure the associated save mode, such as sqlClient.merge
, sqlClient.append
. These APIs are straightforward and self-explanatory, so this article will not discuss them.
This article only discusses the most basic configuration method, of which there are two approaches.
-
Configure a specific associated property
- Java
- Kotlin
BookStore store = ...any data structure, omitted...
sqlClient
.getEntities()
.saveCommand(store)
.setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.MERGE
)
.execute();val store: BookStore = ...any data structure, omitted...
sqlClient.save(store) {
setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.MERGE
)
}That is, for the associated property
books
ofBookStore
, the save mode of its associated objects (of typeBook
) isMERGE
.Other associated properties are not affected.
-
Configure all associated properties of the data structure being saved
- Java
- Kotlin
BookStore store = ...any data structure, omitted...
sqlClient
.getEntities()
.saveCommand(store)
.setAssociatedModeAll(AssociatedSaveMode.MERGE)
.execute();val store: BookStore = ...any data structure, omitted...
sqlClient.save(store) {
setAssociatedModeAll(AssociatedSaveMode.MERGE)
}For any associated property in the current data structure being saved, the save mode of the associated objects is uniformly
MERGE
.
The configuration for a specific associated property takes precedence over the configuration for all associated properties.
The only difference between these two configuration methods is the granularity of control; there is no functional difference. Therefore, this article will consistently use the first configuration method.
1. APPEND
APPEND is the simplest mode. It performs an unconditional insert of the associated objects without any judgments.
- Java
- Kotlin
BookStore store = BookStoreDraft.$.produce(draft -> {
draft.setId(2L);
draft.addIntoBooks(book -> {
book.setName("SQL in Action");
book.setEdition(2);
book.setPrice(new BigDecimal("59.9"));
});
draft.addIntoBooks(book -> {
book.setName("Redis in Action");
book.setEdition(2);
book.setPrice(new BigDecimal("49.9"));
});
});
sqlClient
.getEntities()
.saveCommand(store)
.setMode(SaveMode.UPDATE_ONLY)
.setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.APPEND
)
.execute();
val store = BookStore {
id = 2L
books().addBy {
name = "SQL in Action"
edition = 2
price = BigDecimal("59.9")
}
books().addBy {
name = "Redis in Action"
edition = 2
price = BigDecimal("49.9")
}
}
sqlClient.save(store) {
setMode(SaveMode.UPDATE_ONLY)
setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.APPEND
)
}
Finally, two SQL statements are generated:
-
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* SQL in Action */, ? /* 2 */, ? /* 59.9 */, ? /* 2 */
) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
)
2. MERGE
Unlike APPEND
, MERGE
does not unconditionally insert associated objects; it determines whether the associated object exists, and then decides whether to perform an update or insert operation.
-
If the id property of the associated object is specified, it determines whether the same object exists in the existing data based on the id.
-
Otherwise, it determines based on the key properties of the associated object.
In the following examples, we will have the associated collection BookStore.books
contain both object with id and object without id to demonstrate these two scenarios.
- Java
- Kotlin
BookStore store = BookStoreDraft.$.produce(draft -> {
draft.setId(2L);
draft.addIntoBooks(book -> { // With id
book.setId(10L);
book.setName("GraphQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("59.9"));
});
draft.addIntoBooks(book -> { // Without id
book.setName("Redis in Action");
book.setEdition(2);
book.setPrice(new BigDecimal("49.9"));
});
});
sqlClient
.getEntities()
.saveCommand(store)
.setMode(SaveMode.UPDATE_ONLY)
.setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.MERGE
)
.execute();
val store = BookStore {
id = 2L
books().addBy { // With id
id = 10L
name = "GraphQL in Action"
edition = 1
price = BigDecimal("59.9")
}
books().addBy { // Without id
name = "Redis in Action"
edition = 2
price = BigDecimal("49.9")
}
}
sqlClient.save(store) {
setMode(SaveMode.UPDATE_ONLY)
setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.MERGE
)
}
The following SQL statements are generated:
-
Based on the
id
property, determine if the first associated object existsselect
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.ID = ? /* 10 */ -
If the result from the previous step indicates the object exists, update it
update BOOK
set
NAME = ? /* GraphQL in Action */,
EDITION = ? /* 1 */,
PRICE = ? /* 59.9 */,
STORE_ID = ? /* 2 */
where
ID = ? /* 10 */ -
Based on the
key
properties (for associated objects of typeBook
, it isname
andedition
), determine if the second associated object existsselect
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.NAME = ? /* Redis in Action */
and
tb_1_.EDITION = ? /* 2 */ -
If the result from the previous step indicates the object does not exist, insert it
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
)
values(
? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
)
Here is the translation of the document to English, preserving the indentation of code blocks:
3. REPLACE
In terms of saving the associated objects specified by the user, REPLACE
has no difference from MERGE
.
However, REPLACE
has an additional capability compared to MERGE
: the dissociation operation.
If some associated objects exist in the database but do not exist in the data structure being saved, these unneeded associated objects will undergo the dissociation operation.
The only difference between the following example and the previous one is that the save mode of the associated objects has not been changed, and the default behavior REPLACE
is adopted.
- Java
- Kotlin
BookStore store = BookStoreDraft.$.produce(draft -> {
draft.setId(2L);
draft.addIntoBooks(book -> { // With id
book.setId(10L);
book.setName("GraphQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("59.9"));
});
draft.addIntoBooks(book -> { // Without id
book.setName("Redis in Action");
book.setEdition(2);
book.setPrice(new BigDecimal("49.9"));
});
});
sqlClient
.getEntities()
.saveCommand(store)
.setMode(SaveMode.UPDATE_ONLY)
// There is no need to call `setAssociatedSaveMode`,
// because `REPLACE` is the default behavior
.execute();
val store = BookStore {
id = 2L
books().addBy { // With id
id = 10L
name = "GraphQL in Action"
edition = 1
price = BigDecimal("59.9")
}
books().addBy { // Without id
name = "Redis in Action"
edition = 2
price = BigDecimal("49.9")
}
}
sqlClient.save(store) {
setMode(SaveMode.UPDATE_ONLY)
// There is no need to call `setAssociatedSaveMode`,
// because `REPLACE` is the default behavior
}
The following SQL statements are generated:
-
Same as the previous example, can be ignored
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.ID = ? /* 10 */ -
Same as the previous example, can be ignored
update BOOK
set
NAME = ? /* GraphQL in Action */,
EDITION = ? /* 1 */,
PRICE = ? /* 59.9 */,
STORE_ID = ? /* 2 */
where
ID = ? /* 10 */ -
Same as the previous example, can be ignored
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.NAME = ? /* Redis in Action */
and
tb_1_.EDITION = ? /* 2 */ -
Same as the previous example, can be ignored
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
)
values(
? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
) -
One step of the dissociation operation
The associated collection property
books
of theBookStore
object being saved contains two associated objects of typeBook
, their ids are 10 (the object that was previously modified) and 100 (the object that was previously inserted, assuming the automatically assigned id is 100).Then, apart from them, are there any other
Book
objects that need to be dissociated from the currentBookStore
object in the database?select
ID
from BOOK
where
STORE_ID = ? /* 2 */
and
// highlight-next-line
ID not in ( // Be careful, this is `not in`
? /* 10 */, ? /* 100 */
) -
One step of the dissociation operation
The dissociation operation is affected by other configurations, and different configurations will lead to different behaviors. This article does not delve into this issue but merely shows one possibility.
delete from BOOK_AUTHOR_MAPPING
where
BOOK_ID in (
? /* 11 */, ? /* 12 */
) -
One step of the dissociation operation
The dissociation operation is affected by other configurations, and different configurations will lead to different behaviors. This article does not delve into this issue but merely shows one possibility.
delete from BOOK
where
ID in (
? /* 11 */, ? /* 12 */
)
The dissociation operation is affected by other configurations. Depending on the different configurations, the final action may be raising an error, clearing the foreign key of the associated object, or even deleting the associated object.
The dissociation operation is not the focus of this article. Please refer to the relevant chapter.
Summary
According to the previous discussion, the essence of the save command is to compare the data structure that the user wants to save with the existing data structure in the database, and synchronize the changed parts,
.It is not difficult to find that the default REPLACE
mode aligns with this illustration. However, MERGE
and APPEND
are weakened variants. Yes, they exist objectively as demands.
Sometimes, developers face simpler scenarios and need to perform simpler operations.