Skip to main content

Save Mode of Aggregate-Root

Save commands support 3 save modes that control how the aggregate root itself is saved:

  • UPSERT: This is the default mode. First check if the aggregate root object to be saved exists via a query:

    • If it does not exist: Execute INSERT

    • If it exists: Execute UPDATE

  • INSERT_ONLY: Unconditionally execute INSERT

  • UPDATE_ONLY: Unconditionally execute UPDATE

info

Save modes only affect the aggregate root object, not other associated objects.

For associated objects, please view Save Mode of Associated Objects.

Specifying Object Id

Let's look at an example:

Book book = Objects.createBook(draft -> {
draft.setId(20L);
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("39.9"));
draft.setStore(
ImmutableObjects.makeIdOnly(BookStore.class, 2L)
);
});
sqlClient.save(book);

In this example, save(book) is shorthand equivalent to save(book, SaveMode.UPSERT), because UPSERT is the default save mode.

Executing the code will generate two SQL statements:

  1. Query if the object exists in the database

    select
    tb_1_.ID
    from BOOK tb_1_
    where
    tb_1_.ID = ? /* 20 */
  2. The second SQL statement will differ depending on the result of the first SQL:

    • If no data is returned from the first SQL, execute INSERT:

      insert into BOOK(ID, NAME, EDITION, PRICE, STORE_ID)
      values(
      ? /* 20 */, ? /* SQL in Action */,
      ? /* 1 */, ? /* 39.9 */, ? /* 2 */
      )
    • If data is returned from the first SQL, execute UPDATE:

      update BOOK
      set
      NAME = ? /* SQL in Action */,
      EDITION = ? /* 1 */,
      PRICE = ? /* 39.9 */,
      STORE_ID = ? /* 2 */
      where
      ID = ? /* 20 */
info

Some databases support UPSERT (such as Postgres' insert into ... on conflict ...), which will be supported before Jimmer-1.0.0

The usage of the other two modes is simple:

  • INSERT_ONLY:

    sqlClient.save(book, SaveMode.INSERT_ONLY)

    or

    sqlClient.insert(book)

    If executed, it will only generate the INSERT SQL above, without the SELECT.

  • UPDATE_ONLY:

    sqlClient.save(book, SaveMode.UPDATE_ONLY) 

    or

    sqlClient.update(book)

    If executed, it will only generate the UPDATE SQL above, without the SELECT.

Not Specifying Object Id

In the above example, we specify the id for the object to be saved.

However, often our entities have auto-incrementing id strategies, so specifying the id is unnecessary for insertion.

Book.java
@Entity
public interface Book {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();

String name();

int edition();

...other properties omitted...
}

Here, Book.id is decorated with @GeneratedValue using auto increment. So specifying the id is unnecessary for inserting a Book.

There are two ways to insert an object without an id property:

  • Not specifying @Key property

  • Specifying @Key property

@Key is a very important concept in save commands that will be explained in detail later. It is not discussed here.

Book book = Objects.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("39.9"));
draft.setStore(
ImmutableObjects.makeIdOnly(BookStore.class, 2L)
);
});
sqlClient.save(book);

Obviously, the id property of the object to be saved is not specified, and the Book type does not declare a @Key property either. So this is an object without neither id nor key.

info

In this article, the objects with neither id nor key that we discuss are aggregate roots.

For associated objects, having neither id nor key will cause exceptions by default. This will be discussed in subsequent documentations.

Trying to save an aggregate root object without neither id nor key results in different behaviors for different save modes:

  • UPSERT: Insert the object without querying and checking, same as INSERT_ONLY.

  • INSERT_ONLY: Insert the object.

  • UPDATE_ONLY: Do not execute any SQL, just tell the developer the affected row count is 0.

The save mode in the above example is the default UPSERT, so it generates:

insert into BOOK(NAME, EDITION, PRICE, STORE_ID)
values
(? /* SQL in Action */, ? /* 1 */, ? /* 39.9 */, ? /* 2 */)

Here, the ID column value is not specified, using the database's auto increment.

The developer can also obtain the automatically assigned id after insertion:

Book book = ...
long newestBookId = sqlClient.save(book)
.getNewEntity()
.getId();

The save function returns an object containing (but not limited to) two properties: originalEntity and modifiedEntity.

Among them, originalEntity is the original data structure to be saved passed in by the developer.

modifiedEntity represents a new data structure identical in shape to originalEntity. Their differences are:

  • If the id property of some objects in originalEntity is not specified, the id property of the corresponding objects in modifiedEntity will be specified.

  • If some objects in originalEntity have optimistic locking properties and correspond to UPDATE statements, the optimistic locking fields of the corresponding objects in modifiedEntity will hold the new version number.

So we can obtain the assigned id of the inserted aggregate root object via modifiedEntity.id.

If the id of an entity is designated some auto-generation strategy (e.g. auto increment, sequence, UUID, snowflake id etc.), it brings a problem that the id property of the entity serves no purpose other than being a unique identifier.

To address this, Jimmer introduces a concept called @Key, adding a "second primary key" with actual business meaning to entities. Due to limited space, click

for the precise definition of Key.

tip

For save commands, @Key is an extremely important concept.

As long as the id of an entity serves no purpose other than being a unique identifier, a key property should be configured for the entity.

  1. Define @Key property for entity type Book

    There are two ways to define the Key property for an entity:

    • Use annotation to statically configure it globally.

    • Dynamically configure it in code, dynamic configuration only affects the current save command, and it can override static configuration.

    Below we demonstrate both approaches:

    • Static configuration (default for most business scenarios)

      Book.java
      @Entity
      public interface Book {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      long id();

      @Key
      String name();

      @Key
      int edition();

      ...other properties omitted...
      }

      In this example, name and edition together form the key. This means these two properties jointly form a uniqueness constraint as a business-meaningful second primary key.

      That is, book names can repeat, but only within different editions. Book name and edition combined must be unique. This requires adding the following unique constraint on the table:

      alter table BOOK
      add constraint UQ_BOOK
      unique(NAME, EDITION);
    • Override at runtime (only for individual save commands, rarely needed for special requirements)

      sqlClient
      .getEntities()
      .saveCommand(book)
      .setKeyProps(BookProps.NAME, BookProps.EDITION)
      .execute();
      info

      Without special circumstances, Key properties should be statically configured via annotations for entity types in most cases.

  2. Use save commands to save objects without id

    Book book = Objects.createBook(draft -> {
    draft.setName("SQL in Action");
    draft.setEdition(1);
    draft.setPrice(new BigDecimal("39.9"));
    draft.setStore(
    ImmutableObjects.makeIdOnly(BookStore.class, 2L)
    );
    });
    sqlClient.save(book);

    This time, two SQL statements will be generated:

    1. Check if the object exists in the database

      select
      tb_1_.ID,
      tb_1_.NAME,
      tb_1_.EDITION
      from BOOK tb_1_
      where
      /* highlight-next-line */
      tb_1_.NAME = ? /* SQL in Action */
      and
      /* highlight-next-line */
      tb_1_.EDITION = ? /* 1 */

      Here, the key (name and edition) is used to determine the existence of the to-be-saved object without id.

    2. The second SQL statement will differ depending on the result of the first SQL:

      • If no data is returned from the first SQL, insert:

        insert into BOOK(NAME, EDITION, PRICE, STORE_ID)
        values(
        ? /* SQL in Action */, ? /* 1 */, ? /* 39.9 */, ? /* 2 */
        )
      • If data is returned from the first SQL, update:

        update BOOK
        set
        NAME = ? /* SQL in Action */,
        EDITION = ? /* 1 */,
        PRICE = ? /* 39.9 */,
        STORE_ID = ? /* 2 */
        where
        ID = ? /* 20 */
      info

      Some databases support UPSERT (such as Postgres' insert into ... on conflict ...), which will be supported before Jimmer-1.0.0

      caution

      Once Key properties are declared for an entity type, for objects to be saved (whether aggregate root or not):

      • Either the id property must be specified

      • Or all key properties must be specified (name and edition for this example)

        If a key property is an associated object based on foreign key (whether fake or not), this associated object:

        • Either must be null

        • Or must be associated object with its id property

      Otherwise, the save command will throw an exception indicating some key properties are not set.

Summary

INSERT_ONLY and UPDATE_ONLY modes are simple and need no summary. Here we focus on summarizing the UPSERT mode.

If Key properties are configured for the entity type, the behavior of the UPSERT mode is:

PreconditionCheck if object existsCheck resultFinal behavior
Id property specifiedQuery data existence by idData existsUpdate specified properties including key properties by id
Data does not existInsert data, no id generation needed since id is known
Id property not specifiedQuery data existence by all key propertiesData existsUpdate specified properties except key properties by queried id
Data does not existInsert data, id generation needed since id is unknown