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
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:
- Java
- Kotlin
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);
val book = new(Book::class).by {
id = 20
name = "SQL in Action"
edition = 1
price = BigDecimal("39.9")
store = 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:
-
Query if the object exists in the database
select
tb_1_.ID
from BOOK tb_1_
where
tb_1_.ID = ? /* 20 */ -
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 */
-
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.
- Java
- Kotlin
@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
String name();
int edition();
...other properties omitted...
}
@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
val name: String
val edition: Int
...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:
@Key is a very important concept in save commands that will be explained in detail later. It is not discussed here.
Not Specifying @Key Property (Not Recommended)
- Java
- Kotlin
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);
val book = new(Book::class).by {
name = "SQL in Action"
edition = 1
price = BigDecimal("39.9")
store = 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.
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:
- Java
- Kotlin
Book book = ...
long newestBookId = sqlClient.save(book)
.getNewEntity()
.getId();
val book = ...
val newestBookId = sqlClient.save(book)
.modifiedEntity
.id
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 inoriginalEntity
is not specified, theid
property of the corresponding objects inmodifiedEntity
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 inmodifiedEntity
will hold the new version number.
So we can obtain the assigned id of the inserted aggregate root object via modifiedEntity.id
.
Specifying @Key Property (Recommended)
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
Key
.
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.
-
Define
@Key
property for entity typeBook
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)
- Java
- Kotlin
Book.java@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
@Key
String name();
@Key
int edition();
...other properties omitted...
}Book.kt@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
@Key
val name: String
@Key
val edition: Int
...other properties omitted...
}In this example,
name
andedition
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)
- Java
- Kotlin
sqlClient
.getEntities()
.saveCommand(book)
.setKeyProps(BookProps.NAME, BookProps.EDITION)
.execute();sqlClient.save(book) {
.setKeyProps(Book::name, Book::edition)
}infoWithout special circumstances, Key properties should be statically configured via annotations for entity types in most cases.
-
-
Use save commands to save objects without id
- Java
- Kotlin
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);val book = new(Book::class).by {
name = "SQL in Action"
edition = 1
price = BigDecimal("39.9")
store = makeIdOnly(BookStore::class, 2L)
}
sqlClient.save(book)This time, two SQL statements will be generated:
-
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
andedition
) is used to determine the existence of the to-be-saved object without id. -
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 */
infoSome databases support
UPSERT
(such as Postgres'insert into ... on conflict ...
), which will be supported before Jimmer-1.0.0cautionOnce 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
andedition
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:
Precondition | Check if object exists | Check result | Final behavior |
---|---|---|---|
Id property specified | Query data existence by id | Data exists | Update specified properties including key properties by id |
Data does not exist | Insert data, no id generation needed since id is known | ||
Id property not specified | Query data existence by all key properties | Data exists | Update specified properties except key properties by queried id |
Data does not exist | Insert data, id generation needed since id is unknown |