diff --git a/docs/mutation/save-command/associated-save-mode.mdx b/docs/mutation/save-command/associated-save-mode.mdx
deleted file mode 100644
index 910b9b7bb1..0000000000
--- a/docs/mutation/save-command/associated-save-mode.mdx
+++ /dev/null
@@ -1,528 +0,0 @@
----
-sidebar_position: 5
-title: Save Mode of Associated Objects
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-import { ViewMore } from '@site/src/components/ViewMore';
-import { Save } from '@site/src/components/Image';
-
-## Basic Concepts
-
-In the [previous article](./save-mode), 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:
-
- 1. 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.
-
- 2. 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](./dissociation).
-
-- **MERGE**
-
- Compared with `REPLACE`, the behavior of saving the associated objects specified by the user is exactly the same; however, the `MERGE` operation will not trigger the [dissociation operation](./dissociation).
-
-- **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.
-
-:::caution
-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
- BookStore store = ...any data structure, omitted...
-
- sqlClient
- .getEntities()
- .saveCommand(store)
- // highlight-next-line
- .setAssociatedMode(
- BookStoreProps.BOOKS,
- AssociatedSaveMode.MERGE
- )
- .execute();
- ```
-
-
-
-
- ```kotlin
- val store: BookStore = ...any data structure, omitted...
-
- sqlClient.save(store) {
- // highlight-next-line
- setAssociatedMode(
- BookStoreProps.BOOKS,
- AssociatedSaveMode.MERGE
- )
- }
- ```
-
-
-
-
- That is, for the associated property `books` of `BookStore`, the save mode of its associated objects *(of type `Book`)* is `MERGE`.
-
- Other associated properties are not affected.
-
-- Configure all associated properties of the data structure being saved
-
-
-
-
- ```java
- BookStore store = ...any data structure, omitted...
-
- sqlClient
- .getEntities()
- .saveCommand(store)
- // highlight-next-line
- .setAssociatedModeAll(AssociatedSaveMode.MERGE)
- .execute();
- ```
-
-
-
-
- ```kotlin
- val store: BookStore = ...any data structure, omitted...
-
- sqlClient.save(store) {
- // highlight-next-line
- setAssociatedModeAll(AssociatedSaveMode.MERGE)
- }
- ```
-
-
-
-
- For any associated property in the current data structure being saved, the save mode of the associated objects is uniformly `MERGE`.
-
-:::tip
-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
-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,
- // highlight-next-line
- AssociatedSaveMode.APPEND
- )
- .execute();
-```
-
-
-
-
-```kotlin
-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,
- // highlight-next-line
- AssociatedSaveMode.APPEND
- )
-}
-```
-
-
-
-
-Finally, two SQL statements are generated:
-
-1. ```sql
- insert into BOOK(
- NAME, EDITION, PRICE, STORE_ID
- ) values(
- ? /* SQL in Action */, ? /* 2 */, ? /* 59.9 */, ? /* 2 */
- )
- ```
-
-2. ```sql
- 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.
-
-:::note
-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
-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,
- // highlight-next-line
- AssociatedSaveMode.MERGE
- )
- .execute();
-```
-
-
-
-
-```kotlin
-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,
- // highlight-next-line
- AssociatedSaveMode.MERGE
- )
-}
-```
-
-
-
-
-The following SQL statements are generated:
-
-1. Based on the `id` property, determine if the first associated object exists
-
- ```sql
- select
- tb_1_.ID,
- tb_1_.NAME,
- tb_1_.EDITION
- from BOOK tb_1_
- where
- tb_1_.ID = ? /* 10 */
- ```
-
-2. If the result from the previous step indicates the object exists, update it
-
- ```sql
- update BOOK
- set
- NAME = ? /* GraphQL in Action */,
- EDITION = ? /* 1 */,
- PRICE = ? /* 59.9 */,
- STORE_ID = ? /* 2 */
- where
- ID = ? /* 10 */
- ```
-
-3. Based on the `key` properties *(for associated objects of type `Book`, it is `name` and `edition`)*, determine if the second associated object exists
-
- ```sql
- 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 */
- ```
-
-4. If the result from the previous step indicates the object does not exist, insert it
-
- ```sql
- 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](./dissociation).
-
-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.
-
-:::note
-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
-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)
- // highlight-next-line
- // There is no need to call `setAssociatedSaveMode`,
- // highlight-next-line
- // because `REPLACE` is the default behavior
- .execute();
-```
-
-
-
-
-```kotlin
-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)
- // highlight-next-line
- // There is no need to call `setAssociatedSaveMode`,
- // highlight-next-line
- // because `REPLACE` is the default behavior
-}
-```
-
-
-
-
-The following SQL statements are generated:
-
-1. *Same as the previous example, can be ignored*
-
- ```sql
- select
- tb_1_.ID,
- tb_1_.NAME,
- tb_1_.EDITION
- from BOOK tb_1_
- where
- tb_1_.ID = ? /* 10 */
- ```
-
-2. *Same as the previous example, can be ignored*
-
- ```sql
- update BOOK
- set
- NAME = ? /* GraphQL in Action */,
- EDITION = ? /* 1 */,
- PRICE = ? /* 59.9 */,
- STORE_ID = ? /* 2 */
- where
- ID = ? /* 10 */
- ```
-
-3. *Same as the previous example, can be ignored*
-
- ```sql
- 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 */
- ```
-
-4. *Same as the previous example, can be ignored*
-
- ```sql
- insert into BOOK(
- NAME, EDITION, PRICE, STORE_ID
- )
- values(
- ? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
- )
- ```
-
-5. **One step of the dissociation operation**
-
- The associated collection property `books` of the `BookStore` object being saved contains two associated objects of type `Book`,
- 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 current `BookStore` object in the database?
-
- ```sql
- select
- ID
- from BOOK
- where
- STORE_ID = ? /* 2 */
- and
- // highlight-next-line
- ID not in ( // Be careful, this is `not in`
- ? /* 10 */, ? /* 100 */
- )
- ```
-
-6. **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.
-
- ```sql
- delete from BOOK_AUTHOR_MAPPING
- where
- BOOK_ID in (
- ? /* 11 */, ? /* 12 */
- )
- ```
-
-7. **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.
-
- ```sql
- delete from BOOK
- where
- ID in (
- ? /* 11 */, ? /* 12 */
- )
- ```
-
-:::info
-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](./dissociation).
-:::
-
-## 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.
diff --git a/docs/mutation/save-command/association/_dissociate.md b/docs/mutation/save-command/association/_dissociate.md
new file mode 100644
index 0000000000..ba5deeaa9b
--- /dev/null
+++ b/docs/mutation/save-command/association/_dissociate.md
@@ -0,0 +1,45 @@
+
+
+
+Existing Database Structure |
+User's Expected Data Structure |
+
+
+
+
+
+
+```sh
++-Food
+|
+|
+|
+# highlight-next-line
++-----Meat(ignore child nodes)
+|
+\-----Bread(ignore child nodes)
+```
+
+ |
+
+
+```sh
++-Food
+|
++-----Drinks(ignore child nodes)
+|
+|
+|
+\-----Bread(ignore child nodes)
+```
+
+ |
+
+
+
+
+- For `Bread`, it exists in both old and new data structures, corresponding to an UPDATE operation
+
+- For `Drinks`, it doesn't exist in the old data structure but exists in the new data, corresponding to an INSERT operation
+
+- For `Meat`, it exists in the old data structure but not in the new data, corresponding to an operation called dissociation operation.
\ No newline at end of file
diff --git a/docs/mutation/save-command/association/_merge-roots.mdx b/docs/mutation/save-command/association/_merge-roots.mdx
new file mode 100644
index 0000000000..5909a96454
--- /dev/null
+++ b/docs/mutation/save-command/association/_merge-roots.mdx
@@ -0,0 +1,48 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+
+Saving the root object is not the focus of this article, collapsed by default
+
+
+
+```sql
+merge into BOOK_STORE(
+ NAME
+) key(NAME) values(
+ ?
+)
+/* batch-0: [MANNING] */
+/* batch-1: [AMAZON] */
+```
+
+
+
+
+```
+TBD
+```
+
+
+
+
+```sql
+insert into BOOK_STORE(
+ NAME
+) values(
+ ?
+) on conflict(
+ NAME
+) do nothing
+returning ID
+/* batch-0: [MANNING] */
+/* batch-1: [AMAZON] */
+```
+
+
+
+
+- Assume `MANNING` exists with an id of `2`
+- Assume `AMAZON` exists, after insertion, the database automatically assigns a new id of `100`
+
+
\ No newline at end of file
diff --git a/docs/mutation/save-command/association/associated-save-mode.mdx b/docs/mutation/save-command/association/associated-save-mode.mdx
new file mode 100644
index 0000000000..4e07927350
--- /dev/null
+++ b/docs/mutation/save-command/association/associated-save-mode.mdx
@@ -0,0 +1,1463 @@
+---
+sidebar_position: 3
+title: Associated Save Mode
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import { ViewMore } from '@site/src/components/ViewMore';
+import { Save } from '@site/src/components/Image';
+import Dissociate from './_dissociate.md';
+import MergeRoots from './_merge-roots.mdx';
+
+## Basic Concepts
+
+In [the previous article](../save-mode), 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 support the following save modes:
+
+
+
+
+Association Save Mode |
+Applicable Scope |
+Accept Wild Objects |
+Description |
+
+
+
+
+APPEND |
+All associations |
+Yes |
+Unconditionally perform INSERT operation on associated objects |
+
+
+APPEND_IF_ABSENT |
+All associations |
+No |
+
+
+1. First check if the associated object exists in the database
+
+- If the id property of the associated object is specified, check existence by id
+- Otherwise, check existence by key
+
+2. Execute subsequent operations based on the check result
+
+- If the associated object already exists, ignore the operation and do nothing
+- Otherwise, insert the associated object
+
+:::note
+Jimmer will try to utilize the database's native UPSERT capability to combine these two steps. If not possible, it will inform the user why
+:::
+
+ |
+
+
+UPDATE |
+All associations |
+Yes |
+
+
+- If the id property of the associated object is specified, update the associated object by id
+- Otherwise, update the associated object by key
+
+ |
+
+
+MERGE |
+All associations |
+No |
+
+
+1. First check if the associated object exists in the database
+
+- If the id property of the associated object is specified, check existence by id
+- Otherwise, check existence by key
+
+2. Execute subsequent operations based on the check result
+
+- If the associated object already exists, update it
+- Otherwise, insert the associated object
+
+:::note
+Jimmer will try to utilize the database's native UPSERT capability to combine these two steps. If not possible, it will inform the user why
+:::
+
+ |
+
+
+REPLACE |
+Post associations |
+No |
+Based on `MERGE`, perform dissociate operation on no longer needed associated objects |
+
+
+VIOLENTLY_REPLACE |
+Post associations |
+Yes |
+
+
+Make Jimmer no longer search for changes in associations *(or association collections)* by id or key, but execute the following two operations
+
+1. First delete all old associations and related objects of the current object
+2. Then reinsert all associated objects and rebuild associations with the current object
+
+This is a very aggressive association update approach with the following pros and cons
+
+- Pros: Accepts wild associated objects, no longer requires associated objects to either specify id or key
+- Cons:
+ - If there are many associated objects, in most cases, this is a low-performance solution
+ - If associated objects have deeper associations, the deletion in step 1 may cause too deep cascade deletion, leading to loss of too much data
+
+ |
+
+
+
+
+## Methods with Specified Association Mode
+
+You can set the `AssociatedSaveMode` for save instructions in two ways
+
+- Set parameters for the save method *(this is the simpler choice)*
+
+- Call configuration methods on the save instruction *(this is the more powerful choice)*
+
+:::info
+Jimmer's API design only allows users to choose one of these methods, preventing confusion from having both methods appear simultaneously
+:::
+
+### Setting Save Method Parameters
+
+Many save methods support this, for example
+
+For `save`, `saveEntities` and `saveInputs`, the default `AssociatedSaveMode` is `REPLACE`. Now, let's modify it to `MERGE`.
+
+- **save**
+
+
+
+
+ ```java
+ Book book = ...omitted...;
+ sqlClient.save(book, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val book = Book { ...omitted... }
+ sqlCient.save(book, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+- **saveEntities**
+
+
+
+
+ ```java
+ List books = ...omitted...;
+ sqlClient.saveEntities(books, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val books = listOf(...omitted...)
+ sqlCient.saveEntities(books, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+- **saveInputs**
+
+
+
+
+ ```java
+ List inputs = ...omitted...;
+ sqlClient.saveInputs(inputs, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val inputs = listOf(...omitted...)
+ sqlCient.saveInputs(inputs, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ Here, the `BookInput` class is not an entity type, but rather an automatically generated [Input DTO](../input-dto) type by Jimmer.
+
+ This related content hasn't been introduced before, please ignore it for now.
+
+Besides `save`, `saveEntities` and `saveInputs`, Jimmer has other shortcut methods, such as
+
+- `insert`, `insertEntities`, `insertInputs`
+- `insertIfAbsent`, `mergeEntitiesIfAbsent`, `mergeInputsIfAbsent`
+- `update`, `updateEntities`, `updateInputs`
+- `merge`, `mergeEntities`, `mergeInputs`
+
+Taking `insert`, `insertEntities` and `insertInputs` as examples, the default `AssociatedSaveMode` is `APPEND`. Now, let's modify it to `MERGE`.
+
+- **insert**
+
+
+
+
+ ```java
+ Book book = ...omitted...;
+ sqlClient.insert(book, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val book = Book { ...omitted... }
+ sqlCient.insert(book, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+- **insertEntities**
+
+
+
+
+ ```java
+ List books = ...omitted...;
+ sqlClient.insertEntities(books, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val books = listOf(...omitted...)
+ sqlCient.insertEntities(books, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+- **insertInputs**
+
+
+
+
+ ```java
+ List inputs = ...omitted...;
+ sqlClient.insertInputs(inputs, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val inputs = listOf(...omitted...)
+ sqlCient.insertInputs(inputs, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+### Calling Save Instruction Configuration Methods
+
+Besides setting save method parameters, we can also modify `AssociatedSaveMode` by calling configuration methods on the save instruction.
+
+- Precisely set `AssociatedSaveMode` for specific associations
+
+
+
+
+ ```java
+ Book book = ...omitted...;
+ sqlClient
+ .saveCommand(book)
+ // highlight-next-line
+ .setAssociatedMode(
+ BookProps.AUTHORS,
+ AssociatedSaveMode.MERGE
+ )
+ .execute();
+ ```
+
+
+
+
+ ```kotlin
+ val book = Book { ...omitted... }
+ sqlCient.save(book) {
+ // highlight-next-line
+ setAssociatedMode(
+ Book::authors,
+ AssociatedSaveMode.MERGE
+ )
+ }
+ ```
+
+
+
+
+ - **Java**
+
+ Calls the `saveCommand` method instead of `save` method, indicating creation of a save instruction without immediate execution.
+
+ After configuration with `setAssociatedMode`, finally calls the `execute` method to actually execute.
+
+ - **Kotlin**
+
+ The syntax is relatively concise, still calling the `save` method that can directly execute save instructions, just with a lambda for additional configuration.
+
+- Blindly set `AssociatedSaveMode` for all associations
+
+
+
+
+ ```java
+ Book book = ...omitted...;
+ sqlClient
+ .saveCommand(book)
+ // highlight-next-line
+ .setAssociatedModeAll(
+ AssociatedSaveMode.MERGE
+ )
+ .execute();
+ ```
+
+
+
+
+ ```kotlin
+ val book = Book { ...omitted... }
+ sqlCient.save(book) {
+ // highlight-next-line
+ setAssociatedModeAll(
+ AssociatedSaveMode.MERGE
+ )
+ }
+ ```
+
+
+
+
+ - **Java**
+
+ Calls the `saveCommand` method instead of `save` method, indicating creation of a save instruction without immediate execution.
+
+ After configuration with `setAssociatedModeAll`, finally calls the `execute` method to actually execute.
+
+ - **Kotlin**
+
+ The syntax is relatively concise, still calling the `save` method that can directly execute save instructions, just with a lambda for additional configuration.
+
+:::info
+Obviously, for specific associations, precise configuration has higher priority than blind configuration.
+:::
+
+## 1. APPEND
+
+Unconditionally perform INSERT operations on associated objects
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.APPEND
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.APPEND
+)
+```
+
+
+
+
+To demonstrate simpler SQL generation, we assume that the `targetTransferable` feature of `sqlClient` is enabled, which was previously covered, see [here](./owner#2-configuring-whether-different-parent-objects-can-seize-child-objects)
+
+Two SQL statements will be generated:
+
+1. Perform UPSERT *(insert or update)* operations on two root objects
+
+
+
+2. Perform INSERT operations on 4 associated objects
+
+ Assuming after the previous SQL saves the root objects:
+
+ - The id of `MANNING` is `2`
+ - The id of `AMAZON` is `100`
+
+ The following SQL is generated:
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(?, ?, ?, ?)
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+## 2. APPEND_IF_ABSENT
+
+1. First check if the associated object exists in the database
+
+- If the id property of the associated object is specified, check existence by id
+- Otherwise, check existence by key
+
+2. Execute subsequent operations based on the check result
+
+- If the associated object already exists, ignore the operation and do nothing
+- Otherwise, insert the associated object
+
+:::note
+Jimmer will try to utilize the database's native UPSERT capabilities to combine these two steps. If this is not possible, it will inform the user of the reason
+:::
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.APPEND_IF_ABSENT
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.APPEND_IF_ABSENT
+)
+```
+
+
+
+
+To demonstrate simpler SQL generation, we assume that the `targetTransferable` feature of `sqlClient` is enabled, which was previously covered, see [here](./owner#2-configuring-whether-different-parent-objects-can-seize-child-objects)
+
+Two SQL statements will be generated:
+
+1. Perform UPSERT *(insert or update)* operations on two root objects
+
+
+
+2. Perform INSERT_IF_ABSENT operations on 4 associated objects
+
+ Assuming:
+
+ - The key for Book objects is `Book.name` and `Book.edition`, and the `Book` entity is annotated with `@KeyUniqueConstraint`
+ *(For MySQL, `@KeyUniqueConstraint(noMoreUniqueConstraints = true)` is needed)*
+
+ - After the previous SQL saves the root objects:
+
+ - The id of `MANNING` is `2`
+ - The id of `AMAZON` is `100`
+
+ The following SQL is generated:
+
+
+
+
+ ```sql
+ merge into BOOK tb_1_
+ using(values(?, ?, ?, ?)) tb_2_(
+ NAME, EDITION, PRICE, STORE_ID
+ )
+ on
+ tb_1_.NAME = tb_2_.NAME
+ and
+ tb_1_.EDITION = tb_2_.EDITION
+ // highlight-next-line
+ when not matched then
+ insert(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ tb_2_.NAME, tb_2_.EDITION, tb_2_.PRICE, tb_2_.STORE_ID
+ )
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+ ```sql
+ insert
+ // highlight-next-line
+ ignore
+ into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ ?, ?, ?, ?
+ )
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ ?, ?, ?, ?
+ ) on conflict(
+ NAME, EDITION
+ // highlight-next-line
+ ) do nothing
+ returning ID
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+## 3. UPDATE
+
+- If the id property of the associated object is specified, update the associated object by id
+- Otherwise, update the associated object by key
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.UPDATE
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.UPDATE
+)
+```
+
+
+
+
+To demonstrate simpler SQL generation, we assume that the `targetTransferable` feature of `sqlClient` is enabled, which was previously covered, see [here](./owner#2-configuring-whether-different-parent-objects-can-seize-child-objects)
+
+Two SQL statements will be generated:
+
+1. Perform UPSERT *(insert or update)* operations on two root objects
+
+
+
+2. Perform INSERT operations on 4 associated objects
+
+ Assuming after the previous SQL saves the root objects:
+
+ - The id of `MANNING` is `2`
+ - The id of `AMAZON` is `100`
+
+ The following SQL is generated:
+
+ ```sql
+ update BOOK
+ set
+ PRICE = ?,
+ STORE_ID = ?
+ where
+ NAME = ?
+ and
+ EDITION = ?
+ /* batch-0: [49.9, 2, SQL in Action, 1] */
+ /* batch-1: [39.9, 2, LINQ in Action, 1] */
+ /* batch-2: [44.02, 100, C++ Primer, 5] */
+ /* batch-3: [71.99, 100, Programming RUST, 1] */
+ ```
+
+## 4. MERGE
+
+1. First check if the associated object exists in the database
+
+- If the id property of the associated object is specified, check existence by id
+- Otherwise, check existence by key
+
+2. Execute subsequent operations based on the check result
+
+- If the associated object already exists, update it
+- Otherwise, insert it
+
+:::note
+Jimmer will try to utilize the database's native UPSERT capabilities to combine these two steps. If this is not possible, it will inform the user of the reason
+:::
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.MERGE
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.MERGE
+)
+```
+
+
+
+
+To demonstrate simpler SQL generation, we assume that the `targetTransferable` feature of `sqlClient` is enabled, which was previously covered, see [here](./owner#2-configuring-whether-different-parent-objects-can-seize-child-objects)
+
+Two SQL statements will be generated:
+
+1. Perform UPSERT *(insert or update)* operations on two root objects
+
+
+
+2. Perform MERGE operations on 4 associated objects
+
+ Assuming:
+
+ - The key for Book objects is `Book.name` and `Book.edition`, and the `Book` entity is annotated with `@KeyUniqueConstraint`
+ *(For MySQL, `@KeyUniqueConstraint(noMoreUniqueConstraints = true)` is needed)*
+
+ - After the previous SQL saves the root objects:
+
+ - The id of `MANNING` is `2`
+ - The id of `AMAZON` is `100`
+
+ The following SQL is generated:
+
+
+
+
+ ```sql
+ // highlight-next-line
+ merge into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) key(NAME, EDITION) values(
+ ?, ?, ?, ?
+ )
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ ?, ?, ?, ?
+ ) on duplicate key
+ // highlight-next-line
+ update
+ /* fake update to return all ids */ ID = last_insert_id(ID),
+ NAME = values(NAME),
+ EDITION = values(EDITION),
+ PRICE = values(PRICE),
+ STORE_ID = values(STORE_ID)
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ ?, ?, ?, ?
+ ) on conflict(
+ NAME, EDITION
+ // highlight-next-line
+ ) do update set
+ NAME = excluded.NAME,
+ EDITION = excluded.EDITION,
+ PRICE = excluded.PRICE,
+ STORE_ID = excluded.STORE_ID
+ returning ID
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+## 5. REPLACE
+
+The `MERGE` mode performs INSERT or UPDATE operations on associated objects, so after saving, the number of associated objects either remains the same or increases, but never decreases.
+
+The `REPLACE` mode does not have this characteristic because `REPLACE` performs dissociate operations on no-longer-needed associated objects in addition to the `MERGE` functionality.
+
+The dissociate operation removes associations that the current object no longer needs, for example:
+
+
+
+:::info
+The dissociate operation will be explained in detail in subsequent tutorials, so this article provides a highly simplified example
+:::
+
+
+
+
+```java
+List books = Arrays.asList(
+ Immutables.createBook(book -> {
+ book.setId(1L);
+ book.addIntoAuthors(author -> author.setId(2L));
+ book.addIntoAuthors(author -> author.setId(3L));
+ }),
+ Immutables.createBook(book -> {
+ book.setId(2L);
+ book.addIntoAuthors(author -> author.setId(2L));
+ book.addIntoAuthors(author -> author.setId(4L));
+ })
+);
+sqlClient.saveEntities(
+ books,
+ // This parameter can be removed because for the `saveEntities` method,
+ // `AssociatedSaveMode.REPLACE` is the default behavior
+ // highlight-next-line
+ AssociatedSaveMode.REPLACE
+);
+```
+
+
+
+
+```kotlin
+val books = listOf(
+ Book {
+ id = 1L
+ authors().addBy { id = 2L }
+ authors().addBy { id = 3L }
+ },
+ Book {
+ id = 2L
+ authors().addBy { id = 2L }
+ authors().addBy { id = 4L }
+ }
+)
+sqlClient.saveEntities(
+ books,
+ // This parameter can be removed because for the `saveEntities` method,
+ // `AssociatedSaveMode.REPLACE` is the default behavior
+ // highlight-next-line
+ AssociatedSaveMode.REPLACE
+)
+```
+
+
+
+
+Assuming the existing database structure is shown in the left column of the table below:
+
+
+
+
+Existing Database Structure |
+Desired Data Structure to Save |
+
+
+
+
+
+
+```sh
+--+-Book(1)
+ |
+ +---Author(1)
+ |
+ \---Author(2)
+
+
+
+--+-Book(2)
+ |
+ +---Author(1)
+ |
+ \---Author(2)
+
+
+```
+
+ |
+
+
+```sh
+--+-Book(1)
+ |
+ |
+ |
+ +---Author(2)
+ |
+ \---Author(3)
+
+--+-Book(2)
+ |
+ |
+ |
+ +---Author(2)
+ |
+ \---Author(4)
+```
+
+ |
+
+
+
+
+Obviously, there should be 4 behaviors:
+
+- Dissociate operations specific to `REPLACE`:
+
+ - Dissociate the association between `Book-1` and `Author-1`
+ - Dissociate the association between `Book-2` and `Author-1`
+
+- Capabilities inherited from `MERGE`:
+
+ - Create a new association between `Book-1` and `Author-3`
+ - Create a new association between `Book-2` and `Author-4`
+
+Finally, two SQL statements will be generated:
+
+1. The dissociate operation specific to `REPLACE` mode, removing old associations:
+
+
+
+
+ ```sql
+ delete from BOOK_AUTHOR_MAPPING
+ where
+ BOOK_ID = ?
+ and
+ not (AUTHOR_ID = any(?))
+ /* batch-0: [1, [2, 3]] */
+ /* batch-1: [2, [2, 4]] */
+ ```
+
+
+
+
+ ```sql
+ delete from BOOK_AUTHOR_MAPPING
+ where
+ BOOK_ID in (
+ ? /* 1 */, ? /* 2 */
+ )
+ and
+ (BOOK_ID, AUTHOR_ID) not in (
+ (? /* 1 */, ? /* 2 */),
+ (? /* 1 */, ? /* 3 */),
+ (? /* 2 */, ? /* 2 */),
+ (? /* 2 */, ? /* 4 */)
+ )
+ ```
+
+
+
+
+ ```sql
+ delete from BOOK_AUTHOR_MAPPING
+ where
+ BOOK_ID = ?
+ and
+ not (AUTHOR_ID = any(?))
+ /* batch-0: [1, [2, 3]] */
+ /* batch-1: [2, [2, 4]] */
+ ```
+
+
+
+
+ The purpose of this SQL is:
+
+ - For `Book-1`, delete its associations with Authors other than `Author-2` and `Author-3`
+
+ In this case, it means dissociating `Book-1` from `Author-1`
+
+ - For `Book-2`, delete its associations with Authors other than `Author-2` and `Author-4`
+
+ In this case, it means dissociating `Book-2` from `Author-1`
+
+ We can see significant differences between different databases:
+
+ - `H2` and `Postgres` use simple batched SQL
+ - `MySQL` uses a single complex SQL statement
+
+ :::info
+ This is because `H2` and `Postgres` support the `=any(array)` syntax, while MySQL does not.
+ :::
+
+2. Capabilities inherited by `REPLACE` mode from `MERGE` mode to establish new associations
+
+
+
+
+ ```sql
+ merge into BOOK_AUTHOR_MAPPING tb_1_
+ using(values(?, ?)) tb_2_(
+ BOOK_ID, AUTHOR_ID
+ )
+ on
+ tb_1_.BOOK_ID = tb_2_.BOOK_ID
+ and
+ tb_1_.AUTHOR_ID = tb_2_.AUTHOR_ID
+ // highlight-next-line
+ when not matched then
+ insert(BOOK_ID, AUTHOR_ID)
+ values(tb_2_.BOOK_ID, tb_2_.AUTHOR_ID)
+ /* batch-0: [1, 2] */
+ /* batch-1: [1, 3] */
+ /* batch-2: [2, 2] */
+ /* batch-3: [2, 3] */
+ ```
+
+
+
+
+ ```sql
+ insert
+ // highlight-next-line
+ ignore
+ into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
+ values(?, ?)
+ /* batch-0: [1, 2] */
+ /* batch-1: [1, 3] */
+ /* batch-2: [2, 2] */
+ /* batch-3: [2, 3] */
+ ```
+
+
+
+
+ ```sql
+ insert
+ into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
+ values(?, ?)
+ on conflict(BOOK_ID, AUTHOR_ID)
+ // highlight-next-line
+ do nothing
+ /* batch-0: [1, 2] */
+ /* batch-1: [1, 3] */
+ /* batch-2: [2, 2] */
+ /* batch-3: [2, 3] */
+ ```
+
+
+
+
+## 6. VIOLENTLY_REPLACE
+
+The previous examples can be simplified because the dissociate operation included in the `REPLACE` mode will be explained in more detail in subsequent documents, as it's not the focus of this article.
+
+However, it's not hard to see that the `REPLACE` mode integrates `INSERT`, `UPDATE`, and `DELETE` operations into one, expecting to replace all associated relationships completely *(if the association relationship is a deep long association, then it's replacing an entire subtree)*.
+
+However, `REPLACE` mode has one limitation: it expects objects to either have an `id` or a `key`.
+It expects to cleverly find the parts where the new and old data structures have changed through `id` and `key`, thereby minimizing the impact range of `INSERT`, `UPDATE`, and `DELETE` operations to achieve maximum performance.
+
+:::tip
+If readers are familiar with the web domain, it's not hard to see that this is the same principle as specifying the `key` attribute when using loop rendering in React.
+
+In fact, the save instruction design was inspired by React.
+:::
+
+However, what should we do when dealing with wild objects, i.e., associated objects that have neither `id` nor `key`?
+
+In this case, we can use the `VIOLENTLY_REPLACE` mode. Once this mode is adopted, Jimmer no longer looks for changes in associations *(or association collections)* based on id or key, but performs the following two operations:
+
+1. First, delete all old associations and related objects of the current object
+2. Then reinsert all associated objects and rebuild associations with the current object
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.VIOLENTLY_REPLACE
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.VIOLENTLY_REPLACE
+)
+```
+
+
+
+
+To demonstrate simpler SQL generation, let's assume that the `targetTransferable` feature of `sqlClient` is enabled, which was previously introduced, please refer to [here](./owner#2-configure-whether-to-allow-different-parent-objects-to-seize-child-objects)
+
+Three SQL statements will be generated:
+
+1. Perform UPSERT *(insert or update)* operations on the two root objects
+
+
+
+2. Delete all old associated objects
+
+ Assuming after saving the root objects in the previous SQL
+
+ - The id of `MANNING` is `2`
+ - The id of `AMAZON` is `100`
+
+ ```sql
+ delete from BOOK
+ where STORE_ID in (
+ ? /* 2 */, ? /* 100 */
+ )
+ ```
+
+ :::info
+ If there are other tables in the database that reference the `BOOK` table through foreign keys, a simple `DELETE` statement cannot delete all associated objects.
+
+ To simplify the discussion, let's assume this scenario doesn't exist.
+ :::
+
+3. Create all new associations and associated objects
+
+ Assuming after saving the root objects in the previous SQL
+
+ - The id of `MANNING` is `2`
+ - The id of `AMAZON` is `100`
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(?, ?, ?, ?)
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+Not comparing old and new data in detail, but simply deleting associated objects and rebuilding them is a very violent mode with the following disadvantages:
+
+- If there are many associated objects, in most cases, this is a low-performance solution
+- If the associated objects have deeper associations, the deletion behavior in step 1 may lead to too deep cascade deletion, resulting in the loss of too much data
+
+:::caution
+Therefore, it is not recommended, please use with caution
+:::
+
+## Default Mode
+
+The save instruction provides not only `save`, `saveEntities`, and `saveInputs` methods, but also other shortcut methods for various application scenarios.
+
+Different save instructions have different default values for `AssociatedSaveMode`, please refer to this table:
+
+
+
+
+Method |
+Default AssociatedSaveMode |
+
+
+
+
+save |
+REPLACE |
+
+
+saveEntities |
+
+
+saveInputs |
+
+
+insert |
+APPEND |
+
+
+insertEntities |
+
+
+insertInputs |
+
+
+insertIfAbsent |
+APPEND_IF_ABSENT |
+
+
+insertEntitiesIfAbsent |
+
+
+insertInputsIfAbsent |
+
+
+update |
+UPDATE |
+
+
+updateEntities |
+
+
+updateInputs |
+
+
+merge |
+MERGE |
+
+
+mergeEntities |
+
+
+mergeInputs |
+
+
+
\ No newline at end of file
diff --git a/docs/quick-view/get-started/create-project.mdx b/docs/quick-view/get-started/create-project.mdx
index 807badfe6d..76ad81a5e1 100644
--- a/docs/quick-view/get-started/create-project.mdx
+++ b/docs/quick-view/get-started/create-project.mdx
@@ -57,7 +57,7 @@ Modify build.gradle or pom.xml to add dependencies:
```xml title="pom.xml"
- 0.9.20
+ 0.9.23
@@ -75,7 +75,7 @@ Modify build.gradle or pom.xml to add dependencies:
```groovy
ext {
- jimmerVersion = '0.9.20'
+ jimmerVersion = '0.9.23'
}
dependencies {
@@ -90,7 +90,7 @@ dependencies {
```kotlin
-val jimmerVersion = "0.9.20"
+val jimmerVersion = "0.9.23"
dependencies {
@@ -114,7 +114,7 @@ plugins {
jimmer {
// Set the Jimmer dependency version, where you can also use version range expressions such as "latest.release" or "0.+"
- version = "0.9.20"
+ version = "0.9.23"
}
```
@@ -132,7 +132,7 @@ plugins {
jimmer {
// Set the Jimmer dependency version, where you can also use version range expressions such as "latest.release" or "0.+"
- version = "0.9.20"
+ version = "0.9.23"
}
```
diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/associated-save-mode.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/associated-save-mode.mdx
deleted file mode 100644
index 5ce35b8fa1..0000000000
--- a/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/associated-save-mode.mdx
+++ /dev/null
@@ -1,525 +0,0 @@
----
-sidebar_position: 5
-title: 关联对象保存模式
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-import { ViewMore } from '@site/src/components/ViewMore';
-import { Save } from '@site/src/components/Image';
-
-## 基本概念
-
-在[上一篇文章](./save-mode)中,我们介绍了如何控制聚合根对象的保存模式。
-
-本文将讨论如何控制关联对象的保存模式,关联对象有三种保存模式:
-
-- **REPLACE** *(默认策略)*
-
- 此策略包含两个方面的能力:
-
- 1. 保存用户指定的关联对象。
-
- 首先,判断关联对象在数据库中是否已经存在。
-
- - 如果关联对象的id属性被指定,按照id判断现有数据中是否存在相同对象。
-
- - 否则按照关联对象的key属性判断。
-
- 对于不同的判断结果,给予不同的处理
-
- - 如果关联对象已经存在,执行UPDATE操作
-
- - 否则,执行INSET操作
-
- 2. 脱钩用户未指定的其他关联对象。
-
- 如果某些关联对象在数据库中存在,但是在被保存的数据结构中并不存在,则对这些不再需要的关联对象进行脱钩操作。
-
- 脱钩操作受到其他配置的影响,根据不同的配置,最终可能被执行的操作是报错、清空关联对象的外键、甚至删除关联对象。
-
- 脱钩操作并非本文讨论的重点,请参考[相关章节](./dissociation)
-
-- **MERGE**
-
- 和`REPLACE`相比,有关保存用户指定的关联对象的行为完全一样;然而,`MERGE`操作并不会导致[脱钩操作](./dissociation)。
-
-- **APPEND**
-
- 和前两种模式差异较大,除了无条件地对关联对象进行INSERT外,无任何多余的操作。因此,自然也不需要关联对象的key配置。
-
-:::caution
-本文根据难易程度,对讲解顺序做了调整:`APPEND`, `MERGE`, `REPLACE`。
-:::
-
-## 两种配置方法
-
- Jimmer提供了一些快捷方法,它们可以快速配置关联保存模式,*例如`sqlClient.merge`, `sqlClient.append`*。这些Api很简单,用户一看就明白,本文不予讨论。
-
- 本文只讨论最原始的配置方法,存在两种配置方法。
-
- - 配置特定的关联属性
-
-
-
-
- ```java
- BookStore store = ...任意数据结构,略...
-
- sqlClient
- .getEntities()
- .saveCommand(store)
- // highlight-next-line
- .setAssociatedMode(
- BookStoreProps.BOOKS,
- AssociatedSaveMode.MERGE
- )
- .execute();
- ```
-
-
-
-
- ```kotlin
- val store: BookStore = ...任意数据结构,略...
-
- sqlClient.save(store) {
- // highlight-next-line
- setAssociatedMode(
- BookStoreProps.BOOKS,
- AssociatedSaveMode.MERGE
- )
- }
- ```
-
-
-
-
- 即,对于`BookStore`的关联属性`books`而言,其关联对象 *(类型为`Book`)* 的保存模式为`MERGE`。
-
- 对于其他属性而言,不受影响。
-
- - 配置被保存数据结构的所有关联属性
-
-
-
-
- ```java
- BookStore store = ...任意数据结构,略...
-
- sqlClient
- .getEntities()
- .saveCommand(store)
- // highlight-next-line
- .setAssociatedModeAll(AssociatedSaveMode.MERGE)
- .execute();
- ```
-
-
-
-
- ```kotlin
- val store: BookStore = ...任意数据结构,略...
-
- sqlClient.save(store) {
- // highlight-next-line
- setAssociatedModeAll(AssociatedSaveMode.MERGE)
- }
- ```
-
-
-
-
- 对于当前被保存数据结构中的任何关联而言,关联对象的保存模式一律为`MERGE`。
-
-:::tip
-针对特定关联的配置,优先于针对所有关联的配置。
-:::
-
-这两种配置方法,唯一的区别在控制粒度的不同,功能层面不存在任何差异,因此,本文统一采用第一种配置方法。
-
-## 1. APPEND
-
-APPEND是最简单的模式,没有任何判断,无条件插入关联对象
-
-
-
-
-```java
-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,
- // highlight-next-line
- AssociatedSaveMode.APPEND
- )
- .execute();
-```
-
-
-
-
-```kotlin
-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,
- // highlight-next-line
- AssociatedSaveMode.APPEND
- )
-}
-```
-
-
-
-
-最终生成两条SQL语句
-
-1. ```sql
- insert into BOOK(
- NAME, EDITION, PRICE, STORE_ID
- ) values(
- ? /* SQL in Action */, ? /* 2 */, ? /* 59.9 */, ? /* 2 */
- )
- ```
-
-2. ```sql
- insert into BOOK(
- NAME, EDITION, PRICE, STORE_ID
- ) values(
- ? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
- )
- ```
-
-## 2. MERGE
-
-和`APPEND`不同,`MERGE`不会无条件插入关联对象;它会判断关联对象是否存在,从而决定应该执行update还是insert操作。
-
-- 如果关联对象的id属性被指定,按照id判断现有数据中是否存在相同对象。
-
-- 否则,按照关联对象的key属性判断。
-
-:::note
-在接下来的例子中,我们让关联集合`BookStore.books`既包含有id的对象也包含没有id属性的对象,以展示这两种情况。
-:::
-
-
-
-
-```java
-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,
- // highlight-nex-line
- AssociatedSaveMode.MERGE
- )
- .execute();
-```
-
-
-
-
-```kotlin
-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,
- // highlight-next-line
- AssociatedSaveMode.MERGE
- )
-}
-```
-
-
-
-
-生成如下SQL语句
-
-1. 基于`id`属性,判断第一个关联对象是否存在
-
- ```sql
- select
- tb_1_.ID,
- tb_1_.NAME,
- tb_1_.EDITION
- from BOOK tb_1_
- where
- tb_1_.ID = ? /* 10 */
- ```
-
-2. 假如上一步的判断结果指明对象存在,更新
-
- ```sql
- update BOOK
- set
- NAME = ? /* GraphQL in Action */,
- EDITION = ? /* 1 */,
- PRICE = ? /* 59.9 */,
- STORE_ID = ? /* 2 */
- where
- ID = ? /* 10 */
- ```
-
-3. 基于`key` *(对于这里类型为`Book`的关联对象而言,就是`name`和`edition`)* 属性,判断第二个关联对象是否存在
-
- ```sql
- 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 */
- ```
-
-4. 假如上一步的判断结果指明对象不存在,插入
-
- ```sql
- insert into BOOK(
- NAME, EDITION, PRICE, STORE_ID
- )
- values(
- ? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
- )
- ```
-
-## 3. REPLACE
-
-在保存被用户指定的关联对象方面,`REPLACE`和`MERGE`并无区别。
-
-然而,`REPLACE`比`MERGE`多了一个功能:[脱钩操作](dissociation)。
-
-如果某些关联对象在数据库中存在,但是在被保存的数据结构中并不存在,则对这些不再需要的关联对象进行脱钩操作。
-
-:::note
-下面这个例子和上一个例子唯一的区别在于并未改变关联对象保存模式,采用了默认行为`REPLACE`。
-:::
-
-
-
-
-```java
-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)
- // highlight-next-line
- // There is no need to call `setAssociatedSaveMode`,
- // highlight-next-line
- // because `REPLACE` is the default behavior
- .execute();
-```
-
-
-
-
-```kotlin
-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)
- // highlight-next-line
- // There is no need to call `setAssociatedSaveMode`,
- // highlight-next-line
- // because `REPLACE` is the default behavior
-}
-```
-
-
-
-
-生成如下SQL语句
-
-1. *和上个例子相同,可忽略*
-
- ```sql
- select
- tb_1_.ID,
- tb_1_.NAME,
- tb_1_.EDITION
- from BOOK tb_1_
- where
- tb_1_.ID = ? /* 10 */
- ```
-
-2. *和上个例子相同,可忽略*
-
- ```sql
- update BOOK
- set
- NAME = ? /* GraphQL in Action */,
- EDITION = ? /* 1 */,
- PRICE = ? /* 59.9 */,
- STORE_ID = ? /* 2 */
- where
- ID = ? /* 10 */
- ```
-
-3. *和上个例子相同,可忽略*
-
- ```sql
- 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 */
- ```
-
-4. *和上个例子相同,可忽略*
-
- ```sql
- insert into BOOK(
- NAME, EDITION, PRICE, STORE_ID
- )
- values(
- ? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
- )
- ```
-
-5. **脱钩操作步骤之一**
-
- 被保存的`BookStore`对象的关联集合属性`books`包含两个`Book`类型的关联对象,
- 它们的id分别为10 *(之前被修改的那个对象)* 和100 *(之前被插入的那个对象,假设自动编号分配结果为100)*。
-
- 那么, **除了它们之外**,当前`BookStore`对象在数据库中是是否存在其他`Book`对象需要被脱钩?
-
- ```sql
- select
- ID
- from BOOK
- where
- STORE_ID = ? /* 2 */
- and
- // highlight-next-line
- ID not in ( // Be careful, this is `not in`
- ? /* 10 */, ? /* 100 */
- )
- ```
-
-6. **脱钩操作步骤之一**
-
- > 脱钩操作受到其他配置的影响,不同的配置会导致不同的行为。本文不深入讨论这个问题,仅展示一种可能性
-
- ```sql
- delete from BOOK_AUTHOR_MAPPING
- where
- BOOK_ID in (
- ? /* 11 */, ? /* 12 */
- )
- ```
-
-7. **脱钩操作步骤之一**
-
- > 脱钩操作受到其他配置的影响,不同的配置会导致不同的行为。本文不深入讨论这个问题,仅展示一种可能性
-
- ```sql
- delete from BOOK
- where
- ID in (
- ? /* 11 */, ? /* 12 */
- )
- ```
-
-:::info
-脱钩操作受到其他配置的影响,根据不同的配置,最终可能报错、清空关联对象外键、甚至删除关联对象。
-
-脱钩操作并非本文讨论的重点,请参考[相关章节](./dissociation)
-:::
-
-## 总结
-
-按照之前的论述,保存指令的本质是将用户要保存的数据结构和数据库中现有的数据结构做对比,
-对有变化的局部进行同步,。
-
-不难发现,默认的`REPLACE`模式和此图契合。然而,`MERGE`和`APPEND`是削弱后变种。是的,它们是客观存在的需求,有的时候,开发人员面对更简单的场景,需要执行更简单的操作。
\ No newline at end of file
diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/_dissociate.md b/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/_dissociate.md
new file mode 100644
index 0000000000..c8bdcf9a65
--- /dev/null
+++ b/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/_dissociate.md
@@ -0,0 +1,45 @@
+
+
+
+数据库已有数据结构 |
+用户期望保存的数据结构 |
+
+
+
+
+
+
+```sh
++-Food
+|
+|
+|
+# highlight-next-line
++-----Meat(忽略子节点)
+|
+\-----Bread(忽略子节点)
+```
+
+ |
+
+
+```sh
++-Food
+|
++-----Drinks(忽略子节点)
+|
+|
+|
+\-----Bread(忽略子节点)
+```
+
+ |
+
+
+
+
+- 对于`Bread`而言,在新旧数据结构中都存在,对应update操作
+
+- 对于`Drinks`而言,在旧数据结构中不存在,但在新数据中存在,对应INSERT操作
+
+- 对于`Meat`而言,在旧数据结构中存在,但在新数据中不存在,对应的操作叫做脱勾操作。
\ No newline at end of file
diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/_merge-roots.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/_merge-roots.mdx
new file mode 100644
index 0000000000..62788adb1c
--- /dev/null
+++ b/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/_merge-roots.mdx
@@ -0,0 +1,48 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+
+保存根对象并非本文的讨论重点,默认折叠
+
+
+
+```sql
+merge into BOOK_STORE(
+ NAME
+) key(NAME) values(
+ ?
+)
+/* batch-0: [MANNING] */
+/* batch-1: [AMAZON] */
+```
+
+
+
+
+```
+TBD
+```
+
+
+
+
+```sql
+insert into BOOK_STORE(
+ NAME
+) values(
+ ?
+) on conflict(
+ NAME
+) do nothing
+returning ID
+/* batch-0: [MANNING] */
+/* batch-1: [AMAZON] */
+```
+
+
+
+
+- 假设`MANNING`存在,现有id为`2`
+- 假设`AMAZON`存在,插入后,数据库自动编号新分配的id为`100`
+
+
\ No newline at end of file
diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/associated-save-mode.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/associated-save-mode.mdx
new file mode 100644
index 0000000000..cbcf3343e6
--- /dev/null
+++ b/i18n/zh/docusaurus-plugin-content-docs/current/mutation/save-command/association/associated-save-mode.mdx
@@ -0,0 +1,1467 @@
+---
+sidebar_position: 3
+title: 关联对象保存模式
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import { ViewMore } from '@site/src/components/ViewMore';
+import { Save } from '@site/src/components/Image';
+import Dissociate from './_dissociate.md';
+import MergeRoots from './_merge-roots.mdx';
+
+## 基本概念
+
+在[之前的文章中](../save-mode)中,我们介绍了如何控制聚合根对象的保存模式。
+
+本文将讨论如何控制关联对象的保存模式,关联对象支持如下保存模式:
+
+
+
+
+关联保存模式 |
+适用范围 |
+接受Wild对象 |
+描述 |
+
+
+
+
+APPEND |
+所有关联 |
+是 |
+无条件地对关联对象进行INSERT操作 |
+
+
+APPEND_IF_ABSENT |
+所有关联 |
+否 |
+
+
+1. 先判断关联对象在数据库中是否存在
+
+- 如果关联对象的id属性被指定了,按照id判断关联对象在数据库中是否存在
+- 否则,按照key判断关联对象在数据库中是否存在
+
+2. 根据判断结果执行后续操作
+
+- 如果关联对象已经存在,忽略操作是么也不做
+- 否则,插入关联对象
+
+:::note
+Jimmer会尽量利用数据库本身的UPSERT的能力,将两个步骤合并。如果做不到,告诉用户原因
+:::
+
+ |
+
+
+UPDATE |
+所有关联 |
+是 |
+
+
+- 如果关联对象的id属性被指定了,按照id更新关联对象
+- 否则,按照key更新关联对象
+
+ |
+
+
+MERGE |
+所有关联 |
+否 |
+
+
+1. 先判断关联对象在数据库中是否存在
+
+- 如果关联对象的id属性被指定了,按照id判断关联对象在数据库中是否存在
+- 否则,按照key判断关联对象在数据库中是否存在
+
+2. 根据判断结果执行后续操作
+
+- 如果关联对象已经存在,更新关联对象
+- 否则,插入关联对象
+
+:::note
+Jimmer会尽量利用数据库本身的UPSERT的能力,将两个步骤合并。如果做不到,告诉用户原因
+:::
+
+ |
+
+
+REPLACE |
+后置关联 |
+否 |
+在`MERGE`的基础上,对不再需要的关联对象进行脱钩操作 |
+
+
+VIOLENTLY_REPLACE |
+后置关联 |
+是 |
+
+
+让Jimmer不再根据id或key去查找关联 *(或关联集合)* 的变化部分,而执行如下两个操作
+
+1. 先删除当前对象的所有旧关联和相关对象
+2. 再重新插入所有关联对象,并重建和当前对象的关联
+
+这是一个非常暴力的关联更新手段,优缺点如下
+
+- 优点:接受wild关联对象,不再要求关联对象要么指定id要么指定key
+- 缺点:
+ - 如果关联对象数量多,大部分情况下,这是一种低性能的方案
+ - 如果关联对象还有更深的关联,第1步中的删除行为可能会导致过深的级联删除,导致过多数据的丢失
+
+ |
+
+
+
+
+## 指定了关联模式的方法
+
+可以为保存指令设置关联关系的`AssociatedSaveMode`,支持两种方法
+
+- 设置保存方法的参数 *(这是更简单的选择)*
+
+- 调用保存指令的配置方法 *(这是更强大的选择)*
+
+:::info
+Jimmer的API设计让用户只能选者其中一种方法,不会导致导致两种方法同时出现引起混淆
+:::
+
+### 设置保存方法的参数
+
+很多保存方法都支持,例如
+
+对于`save`、`saveEntities`和`saveInputs`而言,默认的`AssociatedSaveMode`是`REPLACE`,现在,我们将其修改为`MERGE`。
+
+- **save**
+
+
+
+
+ ```java
+ Book book = ...略...;
+ sqlClient.save(book, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val book = Book { ...略... }
+ sqlCient.save(book, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+- **saveEntities**
+
+
+
+
+ ```java
+ List books = ...略...;
+ sqlClient.saveEntities(books, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val books = listOf(...略...)
+ sqlCient.saveEntities(books, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+- **saveInputs**
+
+
+
+
+ ```java
+ List inputs = ...略...;
+ sqlClient.saveInputs(inputs, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val inputs = listOf(...略...)
+ sqlCient.saveInputs(inputs, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ 其中,`BookInput`类并非实体类型,而是Jimmer自动生成的[Input DTO](../input-dto)类型。
+
+ 相关内容之前并未介绍,请读者先行忽略。
+
+
+除了`save`、`saveEntities`和`saveInputs`外,Jimmer还是有其他快捷方法,例如
+
+- `insert`、`insertEntities`、`insertInputs`
+- `insertIfAbsent`、`mergeEntitiesIfAbsent`、`mergeInputsIfAbsent`
+- `update`、`updateEntities`、`updateInputs`
+- `merge`、`mergeEntities`、`mergeInputs`
+
+以为`insert`、`insertEntities`和`insertInputs`为例,,默认的`AssociatedSaveMode`是`APPEND`,现在,我们将其修改为`MERGE`。
+
+- **insert**
+
+
+
+
+ ```java
+ Book book = ...略...;
+ sqlClient.insert(book, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val book = Book { ...略... }
+ sqlCient.insert(book, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+- **insertEntities**
+
+
+
+
+ ```java
+ List books = ...略...;
+ sqlClient.insertEntities(books, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val books = listOf(...略...)
+ sqlCient.insertEntities(books, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+- **insertInputs**
+
+
+
+
+ ```java
+ List inputs = ...略...;
+ sqlClient.insertInputs(inputs, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+ ```kotlin
+ val inputs = listOf(...略...)
+ sqlCient.insertInputs(inputs, AssociatedSaveMode.MERGE);
+ ```
+
+
+
+
+### 调用保存指令的配置方法
+
+除了通过设置保存方法的参数外,我们还可以调用保存指令的配置方法修改`AssociatedSaveMode`。
+
+- 精确设置特定关联的`AssociatedSaveMode`
+
+
+
+
+ ```java
+ Book book = ...略...;
+ sqlClient
+ .saveCommand(book)
+ // highlight-next-line
+ .setAssociatedMode(
+ BookProps.AUTHORS,
+ AssociatedSaveMode.MERGE
+ )
+ .execute();
+ ```
+
+
+
+
+ ```kotlin
+ val book = Book { ...略... }
+ sqlCient.save(book) {
+ // highlight-next-line
+ setAssociatedMode(
+ Book::authors,
+ AssociatedSaveMode.MERGE
+ )
+ }
+ ```
+
+
+
+
+ - **Java**
+
+ 调用的是`saveCommand`方法,而非`save`方法,表示先创建保存指令,但并不马上执行。
+
+ 通过`setAssociatedMode`完成配置后,最后调用`execute`方法真正执行。
+
+ - **Kotlin**
+
+ 语法相对简洁,仍然调用能直接执行保存指令的`save`方法,只是附带一个lambda完成更多配置。
+
+- 盲目设置所有关联的`AssociatedSaveMode`
+
+
+
+
+ ```java
+ Book book = ...略...;
+ sqlClient
+ .saveCommand(book)
+ // highlight-next-line
+ .setAssociatedModeAll(
+ AssociatedSaveMode.MERGE
+ )
+ .execute();
+ ```
+
+
+
+
+ ```kotlin
+ val book = Book { ...略... }
+ sqlCient.save(book) {
+ // highlight-next-line
+ setAssociatedModeAll(
+ AssociatedSaveMode.MERGE
+ )
+ }
+ ```
+
+
+
+
+ - **Java**
+
+ 调用的是`saveCommand`方法,而非`save`方法,表示先创建保存指令,但并不马上执行。
+
+ 通过`setAssociatedModeAll`完成配置后,最后调用`execute`方法真正执行。
+
+ - **Kotlin**
+
+ 语法相对简洁,仍然调用能直接执行保存指令的`save`方法,只是附带一个lambda完成更多配置。
+
+:::info
+显然,对于特定的关关系而言,精确配置的优先级比盲目配置的优先级高。
+:::
+
+## 1. APPEND
+
+无条件地对关联对象进行INSERT操作
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.APPEND
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.APPEND
+)
+```
+
+
+
+
+为了演示更简单的SQL生成,我们假设`sqlClient`的`targetTransferable`功能被打开,这是之前已经介绍过的内容,请参见[这里](./owner#2-配置是否允许不同父对象抢夺子对象)
+
+将会生成两条SQL
+
+1. 对两个根对象进行UPSERT *(insert或update)* 操作
+
+
+
+2. 对4个关联对象进行INSERT操作
+
+ 假设上个SQL保存根对象后
+
+ - `MANING`的id为`2`
+ - `AMAZON`的id为`100`
+
+ 生成如下SQL
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(?, ?, ?, ?)
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+## 2. APPEND_IF_ABSENT
+
+1. 先判断关联对象在数据库中是否存在
+
+- 如果关联对象的id属性被指定了,按照id判断关联对象在数据库中是否存在
+- 否则,按照key判断关联对象在数据库中是否存在
+
+2. 根据判断结果执行后续操作
+
+- 如果关联对象已经存在,忽略操作是么也不做
+- 否则,插入关联对象
+
+:::note
+Jimmer会尽量利用数据库本身的UPSERT的能力,将两个步骤合并。如果做不到,告诉用户原因
+:::
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.APPEND_IF_ABSENT
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.APPEND_IF_ABSENT
+)
+```
+
+
+
+
+为了演示更简单的SQL生成,我们假设`sqlClient`的`targetTransferable`功能被打开,这是之前已经介绍过的内容,请参见[这里](./owner#2-配置是否允许不同父对象抢夺子对象)
+
+将会生成两条SQL
+
+1. 对两个根对象进行UPSERT *(insert或update)* 操作
+
+
+
+2. 对4个关联对象进行INSERT_IF_ABSENT操作
+
+ 假设
+
+ - Book对象的key为`Book.name`和`Book.edition`,且`Book`实体被`@KeyUniqueConstraint`注解修饰
+ *(对于MySQL而言,需要`@KeyUniqueConstraint(noMoreUniqueConstraints = true)`)*
+
+ - 上个SQL保存根对象后
+
+ - `MANING`的id为`2`
+ - `AMAZON`的id为`100`
+
+ 生成如下SQL
+
+
+
+
+ ```sql
+ merge into BOOK tb_1_
+ using(values(?, ?, ?, ?)) tb_2_(
+ NAME, EDITION, PRICE, STORE_ID
+ )
+ on
+ tb_1_.NAME = tb_2_.NAME
+ and
+ tb_1_.EDITION = tb_2_.EDITION
+ // highlight-next-line
+ when not matched then
+ insert(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ tb_2_.NAME, tb_2_.EDITION, tb_2_.PRICE, tb_2_.STORE_ID
+ )
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+ ```sql
+ insert
+ // highlight-next-line
+ ignore
+ into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ ?, ?, ?, ?
+ )
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ ?, ?, ?, ?
+ ) on conflict(
+ NAME, EDITION
+ // highlight-next-line
+ ) do nothing
+ returning ID
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+## 3. UPDATE
+
+- 如果关联对象的id属性被指定了,按照id更新关联对象
+- 否则,按照key更新关联对象
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.UPDATE
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.UPDATE
+)
+```
+
+
+
+
+为了演示更简单的SQL生成,我们假设`sqlClient`的`targetTransferable`功能被打开,这是之前已经介绍过的内容,请参见[这里](./owner#2-配置是否允许不同父对象抢夺子对象)
+
+将会生成两条SQL
+
+1. 对两个根对象进行UPSERT *(insert或update)* 操作
+
+
+
+2. 对4个关联对象进行INSERT操作
+
+ 假设上个SQL保存根对象后
+
+ - `MANING`的id为`2`
+ - `AMAZON`的id为`100`
+
+ 生成如下SQL
+
+ ```sql
+ update BOOK
+ set
+ PRICE = ?,
+ STORE_ID = ?
+ where
+ NAME = ?
+ and
+ EDITION = ?
+ /* batch-0: [49.9, 2, SQL in Action, 1] */
+ /* batch-1: [39.9, 2, LINQ in Action, 1] */
+ /* batch-2: [44.02, 100, C++ Primer, 5] */
+ /* batch-3: [71.99, 100, Programming RUST, 1] */
+ ```
+
+## 4. MERGE
+
+1. 先判断关联对象在数据库中是否存在
+
+- 如果关联对象的id属性被指定了,按照id判断关联对象在数据库中是否存在
+- 否则,按照key判断关联对象在数据库中是否存在
+
+2. 根据判断结果执行后续操作
+
+- 如果关联对象已经存在,更新关联对象
+- 否则,插入关联对象
+
+:::note
+Jimmer会尽量利用数据库本身的UPSERT的能力,将两个步骤合并。如果做不到,告诉用户原因
+:::
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.MERGE
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.MERGE
+)
+```
+
+
+
+
+为了演示更简单的SQL生成,我们假设`sqlClient`的`targetTransferable`功能被打开,这是之前已经介绍过的内容,请参见[这里](./owner#2-配置是否允许不同父对象抢夺子对象)
+
+将会生成两条SQL
+
+1. 对两个根对象进行UPSERT *(insert或update)* 操作
+
+
+
+2. 对4个关联对象进行INSERT_IF_ABSENT操作
+
+ 假设
+
+ - Book对象的key为`Book.name`和`Book.edition`,且`Book`实体被`@KeyUniqueConstraint`注解修饰
+ *(对于MySQL而言,需要`@KeyUniqueConstraint(noMoreUniqueConstraints = true)`)*
+
+ - 上个SQL保存根对象后
+
+ - `MANING`的id为`2`
+ - `AMAZON`的id为`100`
+
+ 生成如下SQL
+
+
+
+
+ ```sql
+ // highlight-next-line
+ merge into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) key(NAME, EDITION) values(
+ ?, ?, ?, ?
+ )
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ ?, ?, ?, ?
+ ) on duplicate key
+ // highlight-next-line
+ update
+ /* fake update to return all ids */ ID = last_insert_id(ID),
+ NAME = values(NAME),
+ EDITION = values(EDITION),
+ PRICE = values(PRICE),
+ STORE_ID = values(STORE_ID)
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(
+ ?, ?, ?, ?
+ ) on conflict(
+ NAME, EDITION
+ // highlight-next-line
+ ) do update set
+ NAME = excluded.NAME,
+ EDITION = excluded.EDITION,
+ PRICE = excluded.PRICE,
+ STORE_ID = excluded.STORE_ID
+ returning ID
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+
+
+
+## 5. REPLACE
+
+`MERGE`模式对关联对象`INSERT`或`UPDATE`操作,所以,保存执行后,关联对象的数量要么不变,要么变多,但不可能变少。
+
+`REPLACE`模式不再有次特性,因为`REPLACE`在`MERGE`的基础上,对不再需要的关联对象进行脱钩操作。
+
+脱钩操作会去除当前对象不再需要的关联,例如
+
+
+
+:::info
+脱钩操作会在后续教程中详细解介绍,因此,本文给出一个简化到极致的例子
+:::
+
+
+
+
+```java
+List books = Arrays.asList(
+ Immutables.createBook(book -> {
+ book.setId(1L);
+ book.addIntoAuthors(author -> author.setId(2L));
+ book.addIntoAuthors(author -> author.setId(3L));
+ }),
+ Immutables.createBook(book -> {
+ book.setId(2L);
+ book.addIntoAuthors(author -> author.setId(2L));
+ book.addIntoAuthors(author -> author.setId(4L));
+ })
+);
+sqlClient.saveEntities(
+ books,
+ // 可以删除此参数,因为对`saveEntities`方法而言,
+ // `AssociatedSaveMode.REPLACE`是默认行为
+ // highlight-next-line
+ AssociatedSaveMode.REPLACE
+);
+```
+
+
+
+
+```kotlin
+val books = listOf(
+ Book {
+ id = 1L
+ authors().addBy { id = 2L }
+ authors().addBy { id = 3L }
+ },
+ Book {
+ id = 2L
+ authors().addBy { id = 2L }
+ authors().addBy { id = 4L }
+ }
+)
+sqlClient.saveEntities(
+ books,
+ // 可以删除此参数,因为对`saveEntities`方法而言,
+ // `AssociatedSaveMode.REPLACE`是默认行为
+ // highlight-next-line
+ AssociatedSaveMode.REPLACE
+)
+```
+
+
+
+
+假设数据库中现有数据结构为下表左列
+
+
+
+
+数据库已有数据结构 |
+用户期望保存的数据结构 |
+
+
+
+
+
+
+```sh
+--+-Book(1)
+ |
+ +---Author(1)
+ |
+ \---Author(2)
+
+
+
+--+-Book(2)
+ |
+ +---Author(1)
+ |
+ \---Author(2)
+
+
+```
+
+ |
+
+
+```sh
+--+-Book(1)
+ |
+ |
+ |
+ +---Author(2)
+ |
+ \---Author(3)
+
+--+-Book(2)
+ |
+ |
+ |
+ +---Author(2)
+ |
+ \---Author(4)
+```
+
+ |
+
+
+
+
+显然,应该包含4个行为
+
+- `REPLACE`特有的脱钩操作
+
+ - 断开`Book-1`和`Author-1`之间的关联
+
+ - 断开`Book-2`和`Author-1`之间的关联
+
+- `REPLACE`从`MERGE`那里继承的能力
+
+ - 新建`Book-1`和`Author-3`之间的关联
+
+ - 新建`Book-2`和`Author-4`之间的关联
+
+最终会生成如下两条语句
+
+1. `REPLACE`模式特有的脱钩操作,消除旧关联
+
+
+
+
+ ```sql
+ delete from BOOK_AUTHOR_MAPPING
+ where
+ BOOK_ID = ?
+ and
+ not (AUTHOR_ID = any(?))
+ /* batch-0: [1, [2, 3]] */
+ /* batch-1: [2, [2, 4]] */
+ ```
+
+
+
+
+ ```sql
+ delete from BOOK_AUTHOR_MAPPING
+ where
+ BOOK_ID in (
+ ? /* 1 */, ? /* 2 */
+ )
+ and
+ (BOOK_ID, AUTHOR_ID) not in (
+ (? /* 1 */, ? /* 2 */),
+ (? /* 1 */, ? /* 3 */),
+ (? /* 2 */, ? /* 2 */),
+ (? /* 2 */, ? /* 4 */)
+ )
+ ```
+
+
+
+
+ ```sql
+ delete from BOOK_AUTHOR_MAPPING
+ where
+ BOOK_ID = ?
+ and
+ not (AUTHOR_ID = any(?))
+ /* batch-0: [1, [2, 3]] */
+ /* batch-1: [2, [2, 4]] */
+ ```
+
+
+
+
+ 此SQL的目的在于
+
+ - 对于`Book-1`而言,删除它和除了`Author-2`和`Author-3`之外的其他Author关联
+
+ 对于目前这个案例而言,其实就是断开`Book-1`和`Author-1`之间的关联
+
+ - 对于`Book-2`而言,删除它和除了`Author-2`和`Author-4`之外的其他Author关联
+
+ 对于目前这个案例而言,其实就是断开`Book-2`和`Author-1`之间的关联
+
+ 同时我们能看到,不同数据库的差异巨大
+
+ - `H2`和`Postgres`采用了批量化的简单SQL
+
+ - `MySQL`采用了单条复杂SQL
+
+ :::info
+ 这是`H2`和`Posgres`支持`=any(数组)`的语法,MySQL不支持。
+ :::
+
+2. `REPLACE`模式从`MERGE`模式继承的能力,建立新关联
+
+
+
+
+ ```sql
+ merge into BOOK_AUTHOR_MAPPING tb_1_
+ using(values(?, ?)) tb_2_(
+ BOOK_ID, AUTHOR_ID
+ )
+ on
+ tb_1_.BOOK_ID = tb_2_.BOOK_ID
+ and
+ tb_1_.AUTHOR_ID = tb_2_.AUTHOR_ID
+ // highlight-next-line
+ when not matched then
+ insert(BOOK_ID, AUTHOR_ID)
+ values(tb_2_.BOOK_ID, tb_2_.AUTHOR_ID)
+ /* batch-0: [1, 2] */
+ /* batch-1: [1, 3] */
+ /* batch-2: [2, 2] */
+ /* batch-3: [2, 3] */
+ ```
+
+
+
+
+ ```sql
+ insert
+ // highlight-next-line
+ ignore
+ into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
+ values(?, ?)
+ /* batch-0: [1, 2] */
+ /* batch-1: [1, 3] */
+ /* batch-2: [2, 2] */
+ /* batch-3: [2, 3] */
+ ```
+
+
+
+
+ ```sql
+ insert
+ into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
+ values(?, ?)
+ on conflict(BOOK_ID, AUTHOR_ID)
+ // highlight-next-line
+ do nothing
+ /* batch-0: [1, 2] */
+ /* batch-1: [1, 3] */
+ /* batch-2: [2, 2] */
+ /* batch-3: [2, 3] */
+ ```
+
+
+
+
+## 6. VIOLENTLY_REPLACE
+
+前面的例子经过可以简化,因为`REPLACE`模式所包含的脱钩操作会在后续文档中更详细地讲解,非本文重点。
+
+然而,不难发现,`REPLACE`模式整合了`INSERT`、`UPDATE`和`DELETE`操作于一体,期望全量替换关联关系 *(如果关联关系是深度很深的长关联,那就是全量一颗子树)*。
+
+然而,`REPLACE`模式有一个限制,那就是它期望对象要么具备`id`,要么具备`key`。
+期望通过`id`和`key`巧妙地找到新旧数据结构发生变化的部分,从而把`INSERT`、`UPDATE`和`DELETE`操作的影响范围最小化,以达到最高性能。
+
+:::tip
+如果读者对web领域有所了解的话,不难发现,这和react使用循环渲染时指定标签的`key`属性是一个道理。
+
+事实上,保持指令的设计就是受react其他而来。
+:::
+
+然而,如果要处理wild对象,即,关联对象既无`id`也无`key`,该怎么办呢?
+
+此时可以使用`VIOLENTLY_REPLACE`模式。一旦采用此模式,让Jimmer不再根据id或key去查找关联 *(或关联集合)* 的变化部分,而执行如下两个操作
+
+1. 先删除当前对象的所有旧关联和相关对象
+2. 再重新插入所有关联对象并,并重建和当前对象的关联
+
+
+
+
+```java
+List stores = Arrays.asList(
+ Immutables.createBookStore(draft -> {
+ draft.setName("MANNING");
+ draft.addIntoBooks(book -> {
+ book.setName("SQL in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("49.9"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("LINQ in Action");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("39.9"));
+ });
+ }),
+ Immutables.createBookStore(draft -> {
+ draft.setName("AMAZON");
+ draft.addIntoBooks(book -> {
+ book.setName("C++ Primer");
+ book.setEdition(5);
+ book.setPrice(new BigDecimal("44.02"));
+ });
+ draft.addIntoBooks(book -> {
+ book.setName("Programming RUST");
+ book.setEdition(1);
+ book.setPrice(new BigDecimal("71.99"));
+ });
+ })
+);
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.VIOLENTLY_REPLACE
+);
+```
+
+
+
+
+```kotlin
+val stores = listOf(
+ BookStore {
+ name = "MANNING"
+ books().addBy {
+ name = "SQL in Action"
+ edition = 1
+ price = BigDecimal("49.9")
+ }
+ books().addBy {
+ name = "LINQ in Action"
+ edition = 1
+ price = BigDecimal("39.9")
+ }
+ },
+ BookStore {
+ name = "AMAZON"
+ books().addBy {
+ name = "C++ Primer"
+ edition = 5
+ price = BigDecimal("44.02")
+ }
+ books().addBy {
+ name = "Programming RUST"
+ edition = 1
+ price = BigDecimal("71.99")
+ }
+ }
+)
+
+sqlClient.saveEntities(
+ stores,
+ // highlight-next-line
+ AssociatedSaveMode.VIOLENTLY_REPLACE
+)
+```
+
+
+
+
+为了演示更简单的SQL生成,我们假设`sqlClient`的`targetTransferable`功能被打开,这是之前已经介绍过的内容,请参见[这里](./owner#2-配置是否允许不同父对象抢夺子对象)
+
+将会生成三条SQL
+
+1. 对两个根对象进行UPSERT *(insert或update)* 操作
+
+
+
+2. 删除所有就的关联对象
+
+ 假设上个SQL保存根对象后
+
+ - `MANING`的id为`2`
+ - `AMAZON`的id为`100`
+
+ ```sql
+ delete from BOOK
+ where STORE_ID in (
+ ? /* 2 */, ? /* 100 */
+ )
+ ```
+
+ :::info
+ 如果数据库有其他表通过外键引用`BOOK`表,简单的的`DELETE`语句并不能删除所有关联对象。
+
+ 为了简化讨论,这里假设这种场景并不存在。
+ :::
+
+3. 新建所有关联,以及关联对象
+
+ 假设上个SQL保存根对象后
+
+ - `MANING`的id为`2`
+ - `AMAZON`的id为`100`
+
+ ```sql
+ insert into BOOK(
+ NAME, EDITION, PRICE, STORE_ID
+ ) values(?, ?, ?, ?)
+ /* batch-0: [SQL in Action, 1, 49.9, 2] */
+ /* batch-1: [LINQ in Action, 1, 39.9, 2] */
+ /* batch-2: [C++ Primer, 5, 44.02, 100] */
+ /* batch-3: [Programming RUST, 1, 71.99, 100] */
+ ```
+
+不对新旧数据进行细致的比较,只是简单地先删除关联对象再重建,这是一个非常暴力的模式,存在如下缺点
+
+- 如果关联对象数量多,大部分情况下,这是一种低性能的方案
+- 如果关联对象还有更深的关联,第1步中的删除行为可能会导致过深的级联删除,导致过多数据的丢失
+
+:::caution
+因此,不推荐,请慎用
+:::
+
+## 默认模式
+
+保持指令提供的方法不仅有`save`、`saveEntities`和`saveInputs`,为了方便各种应用场景,还其他了快捷方法。
+
+不同的保存指令,`AssociatedSaveMode`的默认值并不相同,请参考此表
+
+
+
+
+方法 |
+默认的AssociatedSaveMode |
+
+
+
+
+save |
+REPLACE |
+
+
+saveEntities |
+
+
+saveInputs |
+
+
+insert |
+APPEND |
+
+
+insertEntities |
+
+
+insertInputs |
+
+
+insertIfAbsent |
+APPEND_IF_ABSENT |
+
+
+insertEntitiesIfAbsent |
+
+
+insertInputsIfAbsent |
+
+
+update |
+UPDATE |
+
+
+updateEntities |
+
+
+updateInputs |
+
+
+merge |
+MERGE |
+
+
+mergeEntities |
+
+
+mergeInputs |
+
+
+
\ No newline at end of file
diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/quick-view/get-started/create-database.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/quick-view/get-started/create-database.mdx
index 49a4a02f96..e761f79c43 100644
--- a/i18n/zh/docusaurus-plugin-content-docs/current/quick-view/get-started/create-database.mdx
+++ b/i18n/zh/docusaurus-plugin-content-docs/current/quick-view/get-started/create-database.mdx
@@ -19,8 +19,6 @@ title: 创建数据库
本教程例子采用MySQL,新建SQL文件`jimmer-demo.sql`,代码如下
-
-
```sql title="jimmer-demo.sql"
create database jimmer_demo;
use jimmer_demo;
@@ -202,143 +200,6 @@ insert into tree_node(
(24, 'Shirt', 22)
;
```
-
-
-```sql title="jimmer-demo.sql"
-create table if not exists book_store
-(
- id bigint generated by default as identity primary key,
- name varchar(50) not null,
- website varchar(100)
-);
-alter table if exists book_store
- add constraint business_key_book_store unique (name);
-
-
-create table if not exists book
-(
- id bigint generated by default as identity primary key,
- name varchar(50) not null,
- edition integer not null,
- price numeric(10, 2) not null,
- store_id bigint
-);
-alter table if exists book
- add constraint business_key_book unique (name, edition);
-alter table if exists book
- add constraint fk_book__book_store foreign key (store_id) references book_store (id);
-
-
-create table if not exists author
-(
- id bigint generated by default as identity primary key,
- first_name varchar(25) not null,
- last_name varchar(25) not null,
- gender char(1) not null,
- created_time timestamp not null,
- modified_time timestamp not null
-);
-alter table if exists author
- add constraint business_key_author unique (first_name, last_name);
-alter table if exists author
- add constraint ck_author_gender check (gender in ('M', 'F'));
-
-
-create table if not exists book_author_mapping
-(
- book_id bigint not null,
- author_id bigint not null,
- primary key (book_id, author_id)
-);
-alter table if exists book_author_mapping
- add constraint fk_book_author_mapping__book foreign key (book_id) references book (id) on delete cascade;
-alter table if exists book_author_mapping
- add constraint fk_book_author_mapping__author foreign key (author_id) references author (id) on delete cascade;
-
-
-create table if not exists tree_node
-(
- node_id bigint generated by default as identity primary key,
- name varchar(20) not null,
- parent_id bigint
-);
-
-
-alter table if exists tree_node
- add constraint business_key_tree_node unique (parent_id, name);
-alter table if exists tree_node
- add constraint fk_tree_node__parent foreign key (parent_id) references tree_node (node_id);
-
-
-insert into book_store (id, name)
-values (1, 'O''REILLY'),
- (2, 'MANNING');
-insert into book (id, name, edition, price, store_id)
-values (1, 'Learning GraphQL', 1, 50, 1),
- (2, 'Learning GraphQL', 2, 55, 1),
- (3, 'Learning GraphQL', 3, 51, 1),
- (4, 'Effective TypeScript', 1, 73, 1),
- (5, 'Effective TypeScript', 2, 69, 1),
- (6, 'Effective TypeScript', 3, 88, 1),
- (7, 'Programming TypeScript', 1, 47.5, 1),
- (8, 'Programming TypeScript', 2, 45, 1),
- (9, 'Programming TypeScript', 3, 48, 1),
- (10, 'GraphQL in Action', 1, 80, 2),
- (11, 'GraphQL in Action', 2, 81, 2),
- (12, 'GraphQL in Action', 3, 80, 2);
-
-insert into author (id, first_name, last_name, gender, created_time, modified_time)
-values (1, 'Eve', 'Procello', 'F', current_timestamp, current_timestamp),
- (2, 'Alex', 'Banks', 'M', current_timestamp, current_timestamp),
- (3, 'Dan', 'Vanderkam', 'M', current_timestamp, current_timestamp),
- (4, 'Boris', 'Cherny', 'M', current_timestamp, current_timestamp),
- (5, 'Samer', 'Buna', 'M', current_timestamp, current_timestamp);
-
-insert into book_author_mapping (book_id, author_id)
-values (1, 1),
- (2, 1),
- (3, 1),
- (1, 2),
- (2, 2),
- (3, 2),
- (4, 3),
- (5, 3),
- (6, 3),
- (7, 4),
- (8, 4),
- (9, 4),
- (10, 5),
- (11, 5),
- (12, 5);
-
-insert into tree_node (node_id, name, parent_id)
-values (1, 'Home', null),
- (2, 'Food', 1),
- (3, 'Drinks', 2),
- (4, 'Coca Cola', 3),
- (5, 'Fanta', 3),
- (6, 'Bread', 2),
- (7, 'Baguette', 6),
- (8, 'Ciabatta', 6),
- (9, 'Clothing', 1),
- (10, 'Woman', 9),
- (11, 'Casual wear', 10),
- (12, 'Dress', 11),
- (13, 'Miniskirt', 11),
- (14, 'Jeans', 11),
- (15, 'Formal wear', 10),
- (16, 'Suit', 15),
- (17, 'Shirt', 15),
- (18, 'Man', 9),
- (19, 'Casual wear', 18),
- (20, 'Jacket', 19),
- (21, 'Jeans', 19),
- (22, 'Formal wear', 18),
- (23, 'Suit', 22),
- (24, 'Shirt', 22);
-```
-
-
这个SQL文件展示了如下概念
diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/quick-view/get-started/create-project.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/quick-view/get-started/create-project.mdx
index ac06b0a08d..e62cba9982 100644
--- a/i18n/zh/docusaurus-plugin-content-docs/current/quick-view/get-started/create-project.mdx
+++ b/i18n/zh/docusaurus-plugin-content-docs/current/quick-view/get-started/create-project.mdx
@@ -57,7 +57,7 @@ Jimmer本身是高度中立的,可以脱离spring-boot使用;但同时也提
```xml title="pom.xml"
- 0.9.20
+ 0.9.23
@@ -75,7 +75,7 @@ Jimmer本身是高度中立的,可以脱离spring-boot使用;但同时也提
```groovy title="build.gradle"
ext {
- jimmerVersion = "0.9.20"
+ jimmerVersion = "0.9.23"
}
dependencies {
@@ -90,7 +90,7 @@ dependencies {
```kotlin title="build.gradle.kts"
-val jimmerVersion = "0.9.20"
+val jimmerVersion = "0.9.23"
dependencies {
@@ -114,7 +114,7 @@ plugins {
jimmer {
// 设定 jimmer 依赖版本,此处也可以使用 "latest.release" 或 "0.+" 等版本范围表达式
- version = "0.9.20"
+ version = "0.9.23"
}
```
@@ -132,7 +132,7 @@ plugins {
jimmer {
// 设定 jimmer 依赖版本,此处也可以使用 "latest.release" 或 "0.+" 等版本范围表达式
- version = "0.9.20"
+ version = "0.9.23"
}
```