Skip to main content

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:

    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.

  • 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.

  • 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

    BookStore store = ...any data structure, omitted...

    sqlClient
    .getEntities()
    .saveCommand(store)
    .setAssociatedMode(
    BookStoreProps.BOOKS,
    AssociatedSaveMode.MERGE
    )
    .execute();

    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

    BookStore store = ...any data structure, omitted...

    sqlClient
    .getEntities()
    .saveCommand(store)
    .setAssociatedModeAll(AssociatedSaveMode.MERGE)
    .execute();

    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.

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();

Finally, two SQL statements are generated:

  1.  insert into BOOK(
    NAME, EDITION, PRICE, STORE_ID
    ) values(
    ? /* SQL in Action */, ? /* 2 */, ? /* 59.9 */, ? /* 2 */
    )
  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.

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.

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();

The following SQL statements are generated:

  1. Based on the id property, determine if the first associated object exists

    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

    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

    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

    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.

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.

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();

The following SQL statements are generated:

  1. 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 */
  2. 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 */
  3. 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 */
  4. Same as the previous example, can be ignored

    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?

    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.

    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.

    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.

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.