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.id
property -
Specify both
TreeNode.name
andTreeNode.parent
properties
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
name
andparent
properties of allTreeNode
objects are specified, meaning all@Key
properties are specified -
From the user's perspective, only the root object needs to specify both
name
andparent
properties, while all other objects only need to specify thename
property
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
UPSERT
capability is not used, instead an additional query is made to determine whether the subsequent operation should beINSERT
orUPDATE
.This is because the database's own
UPSERT
capability relies on unique constraints (or unique indexes), and here, theparent
property 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
@Key
property 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 beINSERT
orUPDATE
, and reportQueryReason.NULL_NOT_DISTINCT_REQUIRED
to 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_REQUIRED
to 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
:merge into BOOK_STORE(
NAME
) key(NAME) values(?)
/* batch-0: [MANNING] */ -
Saving the child objects
Book
:// highlight-next-line
Purpose: COMMAND(TARGET_NOT_TRANSFERABLE)
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION,
tb_1_.STORE_ID
from BOOK tb_1_
where
tb_1_.ID = any(? /* [12, 1] */)Strangely, even though the id property of child objects is specified (which usually means Jimmer will utilize the database's own
UPSERT
capability), Jimmer still tries to determine whether the subsequent operation should beINSERT
orUPDATE
through a query, rather than using the database's ownUPSERT
capability. More importantly, Jimmer reportsQueryReason.TARGET_NOT_TRANSFERABLE
. -
Finally, the above code will result in the following exception:
Save error caused by the path: "<root>.books":
Can the move the child object whose type is "org.doc.j.model.Book"
and id is "1" to another parent object because the property
"org.doc.j.model.BookStore.books"
does not support target transfer
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
TargetTransferMode
enum, which has three values:-
ALLOWED: Allows child object transfer and tries to use the database's own
UPSERT
capability whenever possible -
NOT_ALLOWED: Prohibits child object transfer, initiates additional queries with
QueryReason.TARGET_NOT_TRANSFERABLE
for 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.yml
as 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:
merge into BOOK_STORE(
NAME
) key(NAME) values(?)
/* batch-0: [MANNING] */ -
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.