关联对象保存模式
基本概念
在之前的文章中中,我们介绍了如何控制聚合根对象的保存模式。
本文将讨论如何控制关联对象的保存模式,关联对象支持如下保存模式:
关联保存模式 | 适用范围 | 接受Wild对象 | 描述 |
---|---|---|---|
APPEND | 所有关联 | 是 | 无条件地对关联对象进行INSERT操作 |
APPEND_IF_ABSENT | 所有关联 | 否 |
备注 Jimmer会尽量利用数据库本身的UPSERT的能力,将两个步骤合并。如果做不到,告诉用户原因 |
UPDATE | 所有关联 | 是 |
|
MERGE | 所有关联 | 否 |
备注 Jimmer会尽量利用数据库本身的UPSERT的能力,将两个步骤合并。如果做不到,告诉用户原因 |
REPLACE | 后置关联 | 否 | 在MERGE 的基础上,对不再需要的关联对象进行脱钩操作 |
VIOLENTLY_REPLACE | 后置关联 | 是 | 让Jimmer不再根据id或key去查找关联 (或关联集合) 的变化部分,而执行如下两个操作
这是一个非常暴力的关联更新手段,优缺点如下
|
指定了关联模式的方法
可以为保存指令设置关联关系的AssociatedSaveMode
,支持两种方法
-
设置保存方法的参数 (这是更简单的选择)
-
调用保存指令的配置方法 (这是更强大的选择)
Jimmer的API设计让用户只能选者其中一种方法,不会导致导致两种方法同时出现引起混淆
设置保存方法的参数
很多保存方法都支持,例如
对于save
、saveEntities
和saveInputs
而言,默认的AssociatedSaveMode
是REPLACE
,现在,我们将其修改为MERGE
。
-
save
- Java
- Kotlin
Book book = ...略...;
sqlClient.save(book, AssociatedSaveMode.MERGE);val book = Book { ...略... }
sqlCient.save(book, AssociatedSaveMode.MERGE); -
saveEntities
- Java
- Kotlin
List<Book> books = ...略...;
sqlClient.saveEntities(books, AssociatedSaveMode.MERGE);val books = listOf<Book>(...略...)
sqlCient.saveEntities(books, AssociatedSaveMode.MERGE); -
saveInputs
- Java
- Kotlin
List<BookInput> inputs = ...略...;
sqlClient.saveInputs(inputs, AssociatedSaveMode.MERGE);val inputs = listOf<BookInput>(...略...)
sqlCient.saveInputs(inputs, AssociatedSaveMode.MERGE);其中,
BookInput
类并非实体类型,而是Jimmer自动生成的Input DTO类型。相关内容之前并未介绍,请读者先行忽略。
除了save
、saveEntities
和saveInputs
外,Jimmer还是有其他快捷方法,例如
insert
、insertEntities
、insertInputs
insertIfAbsent
、mergeEntitiesIfAbsent
、mergeInputsIfAbsent
update
、updateEntities
、updateInputs
merge
、mergeEntities
、mergeInputs
以为insert
、insertEntities
和insertInputs
为例,,默认的AssociatedSaveMode
是APPEND
,现在,我们将其修改为MERGE
。
-
insert
- Java
- Kotlin
Book book = ...略...;
sqlClient.insert(book, AssociatedSaveMode.MERGE);val book = Book { ...略... }
sqlCient.insert(book, AssociatedSaveMode.MERGE); -
insertEntities
- Java
- Kotlin
List<Book> books = ...略...;
sqlClient.insertEntities(books, AssociatedSaveMode.MERGE);val books = listOf<Book>(...略...)
sqlCient.insertEntities(books, AssociatedSaveMode.MERGE); -
insertInputs
- Java
- Kotlin
List<BookInput> inputs = ...略...;
sqlClient.insertInputs(inputs, AssociatedSaveMode.MERGE);val inputs = listOf<BookInput>(...略...)
sqlCient.insertInputs(inputs, AssociatedSaveMode.MERGE);
调用保存指令的配置方法
除了通过设置保存方法的参数外,我们还可以调用保存指令的配置方法修改AssociatedSaveMode
。
-
精确设置特定关联的
AssociatedSaveMode
- Java
- Kotlin
Book book = ...略...;
sqlClient
.saveCommand(book)
.setAssociatedMode(
BookProps.AUTHORS,
AssociatedSaveMode.MERGE
)
.execute();val book = Book { ...略... }
sqlCient.save(book) {
setAssociatedMode(
Book::authors,
AssociatedSaveMode.MERGE
)
}-
Java
调用的是
saveCommand
方法,而非save
方法,表示先创建保存指令,但并不马上执行。通过
setAssociatedMode
完成配置后,最后调用execute
方法真正执行。 -
Kotlin
语法相对简洁,仍然调用能直接执行保存指令的
save
方法,只是附带一个lambda完成更多配置。
-
盲目设置所有关联的
AssociatedSaveMode
- Java
- Kotlin
Book book = ...略...;
sqlClient
.saveCommand(book)
.setAssociatedModeAll(
AssociatedSaveMode.MERGE
)
.execute();val book = Book { ...略... }
sqlCient.save(book) {
setAssociatedModeAll(
AssociatedSaveMode.MERGE
)
}-
Java
调用的是
saveCommand
方法,而非save
方法,表示先创建保存指令,但并不马上执行。通过
setAssociatedModeAll
完成配置后,最后调用execute
方法真正执行。 -
Kotlin
语法相对简洁,仍然调用能直接执行保存指令的
save
方法,只是附带一个lambda完成更多配置。
显然,对于特定的关关系而言,精确配置的优先级比盲目配置的优先级高。
1. APPEND
无条件地对关联对象进行INSERT操作
- Java
- Kotlin
List<BookStore> stores = Arrays.asList(
Immutables.createBookStore(draft -> {
draft.setName("MANNING");
draft.addIntoBooks(book -> {
book.setName("SQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("49.9"));
});
draft.addIntoBooks(book -> {
book.setName("LINQ in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("39.9"));
});
}),
Immutables.createBookStore(draft -> {
draft.setName("AMAZON");
draft.addIntoBooks(book -> {
book.setName("C++ Primer");
book.setEdition(5);
book.setPrice(new BigDecimal("44.02"));
});
draft.addIntoBooks(book -> {
book.setName("Programming RUST");
book.setEdition(1);
book.setPrice(new BigDecimal("71.99"));
});
})
);
sqlClient.saveEntities(
stores,
AssociatedSaveMode.APPEND
);
val stores = listOf(
BookStore {
name = "MANNING"
books().addBy {
name = "SQL in Action"
edition = 1
price = BigDecimal("49.9")
}
books().addBy {
name = "LINQ in Action"
edition = 1
price = BigDecimal("39.9")
}
},
BookStore {
name = "AMAZON"
books().addBy {
name = "C++ Primer"
edition = 5
price = BigDecimal("44.02")
}
books().addBy {
name = "Programming RUST"
edition = 1
price = BigDecimal("71.99")
}
}
)
sqlClient.saveEntities(
stores,
AssociatedSaveMode.APPEND
)
为了演示更简单的SQL生成,我们假设sqlClient
的targetTransferable
功能被打开,这是之前已经介绍过的内容,请参见这里
将会生成两条SQL
-
对两个根对象进行UPSERT (insert或update) 操作
保存根对象并非本文的讨论重点,默认折叠
- H2
- Mysql
- Postgres
merge into BOOK_STORE(
NAME
) key(NAME) values(
?
)
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
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(
? /* AMAZON */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID)
insert into BOOK_STORE(
NAME
) values(
?
) on conflict(
NAME
) do update set
/* fake update to return all ids */ NAME = execluded.NAME
returning ID
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */- 假设
MANNING
存在,现有id为2
- 假设
AMAZON
存在,插入后,数据库自动编号新分配的id为100
-
对4个关联对象进行INSERT操作
假设上个SQL保存根对象后
MANING
的id为2
AMAZON
的id为100
生成如下SQL
- 绝大部分数据库
- Mysql
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(?, ?, ?, ?)
/* batch-0: [SQL in Action, 1, 49.9, 2] */
/* batch-1: [LINQ in Action, 1, 39.9, 2] */
/* batch-2: [C++ Primer, 5, 44.02, 100] */
/* batch-3: [Programming RUST, 1, 71.99, 100] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* SQL in Action */,
? /* 1 */,
? /* 49.9 */,
? /* 2 */
) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* LINQ in Action */,
? /* 1 */,
? /* 39.9 */,
? /* 2 */
) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* C++ Primer */,
? /* 5 */,
? /* 44.02 */,
? /* 100 */
) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Programming RUST */,
? /* 1 */,
? /* 71.99 */,
? /* 100 */
)
2. APPEND_IF_ABSENT
- 先判断关联对象在数据库中是否存在
- 如果关联对象的id属性被指定了,按照id判断关联对象在数据库中是否存在
- 否则,按照key判断关联对象在数据库中是否存在
- 根据判断结果执行后续操作
- 如果关联对象已经存在,忽略操作是么也不做
- 否则,插入关联对象
Jimmer会尽量利用数据库本身的UPSERT的能力,将两个步骤合并。如果做不到,告诉用户原因
- Java
- Kotlin
List<BookStore> stores = Arrays.asList(
Immutables.createBookStore(draft -> {
draft.setName("MANNING");
draft.addIntoBooks(book -> {
book.setName("SQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("49.9"));
});
draft.addIntoBooks(book -> {
book.setName("LINQ in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("39.9"));
});
}),
Immutables.createBookStore(draft -> {
draft.setName("AMAZON");
draft.addIntoBooks(book -> {
book.setName("C++ Primer");
book.setEdition(5);
book.setPrice(new BigDecimal("44.02"));
});
draft.addIntoBooks(book -> {
book.setName("Programming RUST");
book.setEdition(1);
book.setPrice(new BigDecimal("71.99"));
});
})
);
sqlClient.saveEntities(
stores,
AssociatedSaveMode.APPEND_IF_ABSENT
);
val stores = listOf(
BookStore {
name = "MANNING"
books().addBy {
name = "SQL in Action"
edition = 1
price = BigDecimal("49.9")
}
books().addBy {
name = "LINQ in Action"
edition = 1
price = BigDecimal("39.9")
}
},
BookStore {
name = "AMAZON"
books().addBy {
name = "C++ Primer"
edition = 5
price = BigDecimal("44.02")
}
books().addBy {
name = "Programming RUST"
edition = 1
price = BigDecimal("71.99")
}
}
)
sqlClient.saveEntities(
stores,
AssociatedSaveMode.APPEND_IF_ABSENT
)
为了演示更简单的SQL生成,我们假设sqlClient
的targetTransferable
功能被打开,这是之前已经介绍过的内容,请参见这里
将会生成两条SQL
-
对两个根对象进行UPSERT (insert或update) 操作
保存根对象并非本文的讨论重点,默认折叠
- H2
- Mysql
- Postgres
merge into BOOK_STORE(
NAME
) key(NAME) values(
?
)
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
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(
? /* AMAZON */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID)
insert into BOOK_STORE(
NAME
) values(
?
) on conflict(
NAME
) do update set
/* fake update to return all ids */ NAME = execluded.NAME
returning ID
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */- 假设
MANNING
存在,现有id为2
- 假设
AMAZON
存在,插入后,数据库自动编号新分配的id为100
-
对4个关联对象进行INSERT_IF_ABSENT操作
假设
-
Book对象的key为
Book.name
和Book.edition
,且Book
实体被@KeyUniqueConstraint
注解修饰 (对于MySQL而言,需要@KeyUniqueConstraint(noMoreUniqueConstraints = true)
) -
上个SQL保存根对象后
MANING
的id为2
AMAZON
的id为100
生成如下SQL
- H2
- Mysql
- Postgres
merge into BOOK tb_1_
using(values(?, ?, ?, ?)) tb_2_(
NAME, EDITION, PRICE, STORE_ID
)
on
tb_1_.NAME = tb_2_.NAME
and
tb_1_.EDITION = tb_2_.EDITION
when not matched then
insert(
NAME, EDITION, PRICE, STORE_ID
) values(
tb_2_.NAME, tb_2_.EDITION, tb_2_.PRICE, tb_2_.STORE_ID
)
/* batch-0: [SQL in Action, 1, 49.9, 2] */
/* batch-1: [LINQ in Action, 1, 39.9, 2] */
/* batch-2: [C++ Primer, 5, 44.02, 100] */
/* batch-3: [Programming RUST, 1, 71.99, 100] */insert
ignore
into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
?, ?, ?, ?
)
/* batch-0: [SQL in Action, 1, 49.9, 2] */
/* batch-1: [LINQ in Action, 1, 39.9, 2] */
/* batch-2: [C++ Primer, 5, 44.02, 100] */
/* batch-3: [Programming RUST, 1, 71.99, 100] */insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
?, ?, ?, ?
) on conflict(
NAME, EDITION
) do nothing
returning ID
/* batch-0: [SQL in Action, 1, 49.9, 2] */
/* batch-1: [LINQ in Action, 1, 39.9, 2] */
/* batch-2: [C++ Primer, 5, 44.02, 100] */
/* batch-3: [Programming RUST, 1, 71.99, 100] */ -
3. UPDATE
- 如果关联对象的id属性被指定了,按照id更新关联对象
- 否则,按照key更新关联对象
- Java
- Kotlin
List<BookStore> stores = Arrays.asList(
Immutables.createBookStore(draft -> {
draft.setName("MANNING");
draft.addIntoBooks(book -> {
book.setName("SQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("49.9"));
});
draft.addIntoBooks(book -> {
book.setName("LINQ in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("39.9"));
});
}),
Immutables.createBookStore(draft -> {
draft.setName("AMAZON");
draft.addIntoBooks(book -> {
book.setName("C++ Primer");
book.setEdition(5);
book.setPrice(new BigDecimal("44.02"));
});
draft.addIntoBooks(book -> {
book.setName("Programming RUST");
book.setEdition(1);
book.setPrice(new BigDecimal("71.99"));
});
})
);
sqlClient.saveEntities(
stores,
AssociatedSaveMode.UPDATE
);
val stores = listOf(
BookStore {
name = "MANNING"
books().addBy {
name = "SQL in Action"
edition = 1
price = BigDecimal("49.9")
}
books().addBy {
name = "LINQ in Action"
edition = 1
price = BigDecimal("39.9")
}
},
BookStore {
name = "AMAZON"
books().addBy {
name = "C++ Primer"
edition = 5
price = BigDecimal("44.02")
}
books().addBy {
name = "Programming RUST"
edition = 1
price = BigDecimal("71.99")
}
}
)
sqlClient.saveEntities(
stores,
AssociatedSaveMode.UPDATE
)
为了演示更简单的SQL生成,我们假设sqlClient
的targetTransferable
功能被打开,这是之前已经介绍过的内容,请参见这里
将会生成两条SQL
-
对两个根对象进行UPSERT (insert或update) 操作
保存根对象并非本文的讨论重点,默认折叠
- H2
- Mysql
- Postgres
merge into BOOK_STORE(
NAME
) key(NAME) values(
?
)
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
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(
? /* AMAZON */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID)
insert into BOOK_STORE(
NAME
) values(
?
) on conflict(
NAME
) do update set
/* fake update to return all ids */ NAME = execluded.NAME
returning ID
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */- 假设
MANNING
存在,现有id为2
- 假设
AMAZON
存在 ,插入后,数据库自动编号新分配的id为100
-
对4个关联对象进行INSERT操作
假设上个SQL保存根对象后
MANING
的id为2
AMAZON
的id为100
生成如下SQL
- 绝大部分数据库
- Mysql
update BOOK
set
PRICE = ?,
STORE_ID = ?
where
NAME = ?
and
EDITION = ?
/* batch-0: [49.9, 2, SQL in Action, 1] */
/* batch-1: [39.9, 2, LINQ in Action, 1] */
/* batch-2: [44.02, 100, C++ Primer, 5] */
/* batch-3: [71.99, 100, Programming RUST, 1] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
update BOOK
set
PRICE = ? /* 49.9 */,
STORE_ID = ? /* 2 */
where
NAME = ? /* SQL in Action */
and
EDITION = ? /* 1 */ -
update BOOK
set
PRICE = ? /* 39.9 */,
STORE_ID = ? /* 2 */
where
NAME = ? /* LINQ in Action */
and
EDITION = ? /* 1 */ -
update BOOK
set
PRICE = ? /* 44.02 */,
STORE_ID = ? /* 100 */
where
NAME = ? /* C++ Primer */
and
EDITION = ? /* 5 */ -
update BOOK
set
PRICE = ? /* 71.99 */,
STORE_ID = ? /* 100 */
where
NAME = ? /* Programming RUST */
and
EDITION = ? /* 1 */
4. MERGE
- 先判断关联对象在数据库中是否存在
- 如果关联对象的id属性被指定了,按照id判断关联对象在数据库中是否存在
- 否则,按照key判断关联对象在数据库中是否存在
- 根据判断结果执行后续操作
- 如果关联对象已经存在,更新关联对象
- 否则,插入关联对象
Jimmer会尽量利用数据库本身的UPSERT的能力,将两个步骤合并。如果做不到,告诉用户原因
- Java
- Kotlin
List<BookStore> stores = Arrays.asList(
Immutables.createBookStore(draft -> {
draft.setName("MANNING");
draft.addIntoBooks(book -> {
book.setName("SQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("49.9"));
});
draft.addIntoBooks(book -> {
book.setName("LINQ in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("39.9"));
});
}),
Immutables.createBookStore(draft -> {
draft.setName("AMAZON");
draft.addIntoBooks(book -> {
book.setName("C++ Primer");
book.setEdition(5);
book.setPrice(new BigDecimal("44.02"));
});
draft.addIntoBooks(book -> {
book.setName("Programming RUST");
book.setEdition(1);
book.setPrice(new BigDecimal("71.99"));
});
})
);
sqlClient.saveEntities(
stores,
AssociatedSaveMode.MERGE
);
val stores = listOf(
BookStore {
name = "MANNING"
books().addBy {
name = "SQL in Action"
edition = 1
price = BigDecimal("49.9")
}
books().addBy {
name = "LINQ in Action"
edition = 1
price = BigDecimal("39.9")
}
},
BookStore {
name = "AMAZON"
books().addBy {
name = "C++ Primer"
edition = 5
price = BigDecimal("44.02")
}
books().addBy {
name = "Programming RUST"
edition = 1
price = BigDecimal("71.99")
}
}
)
sqlClient.saveEntities(
stores,
AssociatedSaveMode.MERGE
)
为了演示更简单的SQL生成,我们假设sqlClient
的targetTransferable
功能被打开,这是之前已经介绍过的内容,请参见这里
将会生成两条SQL
-
对两个根对象进行UPSERT (insert或update) 操作
保存根对象并非本文的讨论重点,默认折叠
- H2
- Mysql
- Postgres
merge into BOOK_STORE(
NAME
) key(NAME) values(
?
)
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
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(
? /* AMAZON */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID)
insert into BOOK_STORE(
NAME
) values(
?
) on conflict(
NAME
) do update set
/* fake update to return all ids */ NAME = execluded.NAME
returning ID
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */