Owner side
Basic Concepts
The owned side only has @OneToMany or @OneToOne objects with mappedBy. Taking @OneToMany as an example:
- Java
- Kotlin
@Entity
public interface BookStore {
@OneToMany(mappedBy = true)
List<Book> books();
}
@Entity
interface BookStore {
@OneToMany(mappedBy = true)
val books: List<Book>
}
This association has a special functionality:
-
Automatically sets reverse associations for child objects
-
Configures whether different parent objects can snatch for child objects
1. Automatically Setting Reverse Associations for Child Objects
Assume we have the following entity:
- Java
- Kotlin
@Entity
public interface TreeNode {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
@Key
String name();
@Key
@ManyToOne
@Nullable
TreeNode parent();
@OneToMany(mappedBy = "parent")
List<TreeNode> childNodes();
}
@Entity
interface TreeNode {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
@Key
val name: String
@Key
@ManyToOne
val parent: TreeNode?
@OneToMany(mappedBy = "parent")
val childNodes: List<TreeNode>
}
The @Key properties of TreeNode are name and parent. Apart from explicitly accepting the persistence mode for wild objects, the object being saved needs to either:
-
Specify the
TreeNode.idproperty -
Specify both
TreeNode.nameandTreeNode.parentproperties
However, the following code works normally:
- Java
- Kotlin
TreeNode rootNode = Immutables.createTreeNode(root -> {
root.setName("Root");
root.setParent(null);
root.addIntoChildNodes(child -> {
child.setName("Child-1");
// For non-root objects, no need to specify the `parent` property
});
root.addIntoChildNodes(child -> {
child.setName("Child-2");
// For non-root objects, no need to specify the `parent` property
});
});
sqlClient
.saveCommand(rootNode)
.setTargetTransferModeAll(TargetTransferMode.ALLOWED)
.execute();
val rootNode = TreeNode {
name = "Root"
parent = null
childNodes().addBy {
name = "Child-1"
// For non-root objects,
// no need to specify the `parent` property
}
childNodes().addBy {
name = "Child-2"
// For non-root objects,
// no need to specify the `parent` property
}
}
sqlClient.save(rootNode) {
setTargetTransferModeAll(TargetTransferMode.ALLOWED)
}
The setTargetTransferModeAll(TargetTransferMode.ALLOWED) is not the focus here, readers can ignore it for now.
Here, although the root object (Root) has both name and parent properties specified,
for non-root objects (Child-1, Child-2), only the name property is specified, while the parent property is not.
The TreeNode.childNodes property is the reverse association of the TreeNode.parent property.
For the owning side of many-to-one (or one-to-one) associations (here TreeNode.parent),
once child objects are specified for the parent object through its inverse one-to-many (or one-to-one) association (here TreeNode.childNodes),
the parent object reference for each child object in the collection will be automatically set.
In this example, the object tree that the user originally expected to save was:
{
"name":"Root",
"parent":null,
"childNodes":[
{"name":"Child-1"},
{"name":"Child-2"}
]
}
Assuming the database assigns auto-number 100 to the root element after insertion, Jimmer will automatically adjust this tree to:
{
"id": 100,
"name":"Root",
"parent":null,
"childNodes":[
{
"name":"Child-1",
"parent": {"id": 100}
},
{
"name":"Child-2",
"parent": {"id": 100}
}
]
}
As you can see, once the parent object is saved, the TreeNode.parent property of all child objects will be automatically set. That is, when the id property is not specified:
-
From Jimmer's perspective, both
nameandparentproperties of allTreeNodeobjects are specified, meaning all@Keyproperties are specified -
From the user's perspective, only the root object needs to specify both
nameandparentproperties, while all other objects only need to specify thenameproperty
The above example will generate three SQL statements:
-
Query whether the root object exists based on
@Key:Purpose: COMMAND(NULL_NOT_DISTINCT_REQUIRED)
select
tb_1_.NODE_ID,
tb_1_.NAME,
tb_1_.PARENT_ID
from TREE_NODE tb_1_
where
tb_1_.PARENT_ID is null
and
tb_1_.NAME = ? /* Root */Here, the database's own
UPSERTcapability is not used, instead an additional query is made to determine whether the subsequent operation should beINSERTorUPDATE.This is because the database's own
UPSERTcapability relies on unique constraints (or unique indexes), and here, theparentproperty of the root object being saved is null, and not all databases have the ability to define null behavior for unique constraints.Therefore, by default, if the
@Keyproperty of the object being saved is null, Jimmer will give up using the database's own UPSERT capability, execute an additional query to determine whether the subsequent operation should beINSERTorUPDATE, and reportQueryReason.NULL_NOT_DISTINCT_REQUIREDto developers.infoSome databases, such as Postgres, can define null behavior for unique constraints. How to solve this problem in such databases is not the focus of this article, please refer to the documentation comments of
QueryReason.NULL_NOT_DISTINCT_REQUIREDto learn more. -
Assuming the above query determines that the object being saved does not exist in the database, simply insert the root object:
insert into TREE_NODE(NAME, PARENT_ID)
values(?, ?)
/* batch-0: [Root, DbNull{type=long}] */ -
Save child objects (assuming the id is known to be 100 after saving the root object):
merge into TREE_NODE(
NAME, PARENT_ID
) key(
NAME, PARENT_ID
) values(?, ?)
/* batch-0: [Child-1, 100] */
/* batch-1: [Child-2, 100] */
2. Configuring Whether Different Parent Objects Can Snatch for Child Objects
Conservative Default Behavior
Let's look at an example first:
- Java
- Kotlin
BookStore store = Immutables.createBookStore(draft -> {
draft.setName("MANNING");
draft.addIntoBooks(book -> {
book.setId(12L);
});
draft.addIntoBooks(book -> {
book.setId(1L);
});
});
sqlClient.save(store);
val store = BookStore {
name = "MANNING"
books().addBy {
id = 12L
}
books().addBy {
id = 1L
}
}
sqlClient.save(store)
Executing this code will generate the following SQL and result in an exception:
-
Saving the root object
BookStore:- H2
- Mysql
- Postgres
merge into BOOK_STORE(
NAME
) key(
NAME
) values(
? /* MANNING */
)insert into BOOK_STORE(
NAME
) values(
? /* MANNING */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID)insert into BOOK_STORE(
NAME
) values(
? /* MANNING */
) on conflict(
NAME
) do update set
/* fake update to return all ids */ NAME = execluded.NAME
returning ID -
Saving the child objects
Book:- H2
- Mysql
- Postgres
merge into BOOK(
ID, STORE_ID
) key(ID) values(?, ?)
/* batch-0: [12, 2] */
/* batch-1: [1, 2] */cautionBy default, MySQL batch operations are not used. For specific details, please refer to MySQL Issues
-
insert into BOOK(
ID, STORE_ID
) values(
? /* 12 */, ? /* 2 */
) on duplicate key update
STORE_ID = VALUES(STORE_ID) -
insert into BOOK(
ID, STORE_ID
) values(
? /* 1 */, ? /* 2 */
) on duplicate key update
STORE_ID = VALUES(STORE_ID)
insert into BOOK(
ID, STORE_ID
) values(
?, ?
) on conflict(
ID
) do update set
STORE_ID = execluded.STORE_ID
returning ID
/* batch-0: [12, 2] */
/* batch-1: [1, 2] */Strangely, even though the id property of child objects is specified (which usually means Jimmer will utilize the database's own
UPSERTcapability), Jimmer still tries to determine whether the subsequent operation should beINSERTorUPDATEthrough a query, rather than using the database's ownUPSERTcapability. More importantly, Jimmer reportsQueryReason.TARGET_NOT_TRANSFERABLE. -
Finally, the above code will result in the following exception:
- H2
- Mysql
- Postgres
update BOOK
set
STORE_ID = null
where
STORE_ID = ?
and
not (
ID = any(?)
)
/* batch-0: [2, [12, 1]] */update BOOK
set
STORE_ID = null
where
STORE_ID = ? /* 2 */
and
ID not in(
? /* 12 */,
? /* 1 */
)update BOOK
set
STORE_ID = null
where
STORE_ID = ?
and
not (
ID = any(?)
)
/* batch-0: [2, [12, 1]] */
Book.store is a many-to-one association, a Book object can only belong to one BookStore object and cannot belong to multiple BookStore objects simultaneously.
Therefore, saving the data structure through the inverse one-to-many association BookStore.books establishes an association between the current BookStore parent object and another existing Book object.
If the Book object already belongs to another parent object, it will cause the current parent object to snatch for the child object from other parent objects. In other words, the child object migrates between different parent objects.
If this is expected behavior by the developers, then there's no problem. However, if this is not expected behavior, it may lead to unintended oversights.
By default, Jimmer adopts a conservative strategy that prohibits child objects from transfering between different parent objects.
In this example, attempting to associate BookStore(MANNING) with Book(12) and Book(1),
Jimmer executes an additional query with QueryReason.TARGET_NOT_TRANSFERABLE to check if any child objects are transfering between different parent objects.
Book(12)already belongs toBookStore(MANNING), no transfer occurs, no problemBook(1)doesn't belong toBookStore(MANNING)but belongs toBookStore(O'REILLY), transfer occurs, therefore an exception is thrown.
The default behavior is very conservative. While it prevents competition for child objects between different parent objects (if developers consider such unintended competition harmful to business),
it leads to additional queries and doesn't fully utilize the database's own UPSERT capability, resulting in suboptimal performance.
If you believe better performance is more important than this conservative defensive behavior, Jimmer provides additional configuration to change this behavior.
Overriding Default Behavior Without Restrictions
To prioritize performance and remove these restrictions, there are two methods:
-
Save command level configuration, which can be divided into two types:
-
Precise configuration, removing restrictions for specific associations:
- Java
- Kotlin
BookStore store = ...omitted...;
sqlClient
.saveCommand(store)
.setTargetTransferMode(
BookStoreProps.BOOKS,
TargetTransferMode.ALLOWED
)
.execute();val store = BookStore {...omitted...}
sqlClient.save(store) {
setTargetTransferMode(
BookStore::books,
TargetTransferMode.ALLOWED
)
} -
Blind configuration, removing restrictions for all associations:
- Java
- Kotlin
BookStore store = ...omitted...;
sqlClient
.saveCommand(store)
.setTargetTransferModeAll(
TargetTransferMode.ALLOWED
)
.execute();val store = BookStore {...omitted...}
sqlClient.save(store) {
setTargetTransferModeAll(
TargetTransferMode.ALLOWED
)
}
Whether precise or blind configuration, the last parameter is the
TargetTransferModeenum, which has three values:-
ALLOWED: Allows child object transfer and tries to use the database's own
UPSERTcapability whenever possible -
NOT_ALLOWED: Prohibits child object transfer, initiates additional queries with
QueryReason.TARGET_NOT_TRANSFERABLEfor verification. If child object transfer occurs, throws an exception -
AUTO (default): Current configuration is invalid, refers to lower priority configuration
-
For precise configuration, refers to blind configuration
-
For blind configuration, refers to global configuration
-
-
-
Global configuration, which can be divided into two types:
-
Global configuration based on Jimmer API:
- Java
- Kotlin
JSqlClient sqlCient = JSqlClient
.newBuilder()
.setTargetTransferable(true)
...other configurations omitted...
.build();val sqlClient = sqlClient {
setTargetTransferable(true)
...other configurations omitted...
} -
Global configuration based on Spring Boot if using Jimmer's spring-boot-starter:
Using
application.ymlas an example:jimmer:
target-transferable: true
...other configurations omitted...
-
Once Jimmer is configured through any of the above methods to not restrict child object transfer for the BookStore.books association, modify the code as follows:
- Java
- Kotlin
public interface Book {
@ManyToOne
@Nullable
// Not related to the current discussion,
// please ignore for now
@OnDissociate(DissociateAction.SET_NULL)
BookStore store();
...other code omitted...
}
public interface Book {
@ManyToOne
// Not related to the current discussion,
// please ignore for now
@OnDissociate(DissociateAction.SET_NULL)
val store: BookStore?
...other code omitted...
}
Re-executing the code will generate the following SQL:
-
Saving the root object:
- H2
- Mysql
- Postgres
merge into BOOK_STORE(
NAME
) key(
NAME
) values(
? /* MANNING */
)insert into BOOK_STORE(
NAME
) values(
? /* MANNING */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID)insert into BOOK_STORE(
NAME
) values(
? /* MANNING */
) on conflict(
NAME
) do update set
/* fake update to return all ids */ NAME = execluded.NAME
returning ID -
Establishing associations between root object and child objects:
merge into BOOK(
ID, STORE_ID
) key(ID) values(?, ?)
/* batch-0: [12, 2] */
/* batch-1: [1, 2] */ -
Breaking associations between root object and no longer needed child objects:
update BOOK
set
STORE_ID = null
where
STORE_ID = ?
and
not (
ID = any(?)
)
/* batch-0: [2, [12, 1]] */
To demonstrate performance-priority scenarios to users, the examples jimmer-examples/java/save-command and jimmer-examples/kotlin/save-command-kt both use global configuration to allow child object transfer.