关联对象保存模式
基本概念
在之前的文章中中,我们介绍了如何控制聚合根对象的保存模式。
本文将讨论如何控制关联对象的保存模式,关联对象支持如下保存模式:
| 关联保存模式 | 适用范围 | 接受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 { ...略... }
sqlClient.save(book, AssociatedSaveMode.MERGE); -
saveEntities
- Java
- Kotlin
List<Book> books = ...略...;
sqlClient.saveEntities(books, AssociatedSaveMode.MERGE);val books = listOf<Book>(...略...)
sqlClient.saveEntities(books, AssociatedSaveMode.MERGE); -
saveInputs
- Java
- Kotlin
List<BookInput> inputs = ...略...;
sqlClient.saveInputs(inputs, AssociatedSaveMode.MERGE);val inputs = listOf<BookInput>(...略...)
sqlClient.saveInputs(inputs, AssociatedSaveMode.MERGE);其中,
BookInput类并非实体类型,而是Jimmer自动生成的Input DTO类型。相关内容之前并未介绍,请读者先行忽略。
除了save、saveEntities和saveInputs外,Jimmer还是有其他快捷方法,例如
insert、insertEntities、insertInputsinsertIfAbsent、mergeEntitiesIfAbsent、mergeInputsIfAbsentupdate、updateEntities、updateInputsmerge、mergeEntities、mergeInputs
以为insert、insertEntities和insertInputs为例,,默认的AssociatedSaveMode是APPEND,现在,我们将其修改为MERGE。
-
insert
- Java
- Kotlin
Book book = ...略...;
sqlClient.insert(book, AssociatedSaveMode.MERGE);val book = Book { ...略... }
sqlClient.insert(book, AssociatedSaveMode.MERGE); -
insertEntities
- Java
- Kotlin
List<Book> books = ...略...;
sqlClient.insertEntities(books, AssociatedSaveMode.MERGE);val books = listOf<Book>(...略...)
sqlClient.insertEntities(books, AssociatedSaveMode.MERGE); -
insertInputs
- Java
- Kotlin
List<BookInput> inputs = ...略...;
sqlClient.insertInputs(inputs, AssociatedSaveMode.MERGE);val inputs = listOf<BookInput>(...略...)
sqlClient.insertInputs(inputs, AssociatedSaveMode.MERGE);
调用保存指令的配置方法
除了通过设置保存方法的参数外,我们还可以调用保存指令的配置方法修改AssociatedSaveMode。
-
精确设置特定关联的
AssociatedSaveMode- Java
- Kotlin
Book book = ...略...;
sqlClient
.saveCommand(book)
.setAssociatedMode(
BookProps.AUTHORS,
AssociatedSaveMode.MERGE
)
.execute();val book = Book { ...略... }
sqlClient.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 { ...略... }
sqlClient.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为2AMAZON的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为2AMAZON的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为2AMAZON的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] */- 假设
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为2AMAZON的id为100
生成如下SQL
- H2
- Mysql
- Postgres
merge into BOOK(
NAME, EDITION, PRICE, STORE_ID
) key(NAME, EDITION) 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 */
) on duplicate key
update
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* LINQ in Action */,
? /* 1 */,
? /* 39.9 */,
? /* 2 */
) on duplicate key
update
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* C++ Primer */,
? /* 5 */,
? /* 44.02 */,
? /* 100 */
) on duplicate key
update
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Programming RUST */,
? /* 1 */,
? /* 71.99 */,
? /* 100 */
) on duplicate key
update
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID)
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
?, ?, ?, ?
) on conflict(
NAME, EDITION
) do update set
PRICE = excluded.PRICE,
STORE_ID = excluded.STORE_ID
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] */ -
5. REPLACE
MERGE模式对关联对象INSERT或UPDATE操作,所以,保存执行后,关联对象的数量要么不变,要么变多,但不可能变少。
REPLACE模式不再有此特性,因为REPLACE在MERGE的基础上,对不再需要的关联对象进行脱钩操作。
脱钩操作会去除当前对象不再需要的关联,例如
| 数据库已有数据结构 | 用户期望保存的数据结构 |
|---|---|
| |
-
对于
Bread而言,在新旧数据结构中都存在,对应update操作 -
对于
Drinks而言,在旧数据结构中不存在,但在新数据中存在,对应INSERT操作 -
对于
Meat而言,在旧数据结构中存在,但在新数据中不存在,对应的操作叫做脱勾操作。
脱钩操作会在后续教程中详细解介绍,因此,本文给出一个简化到极致的例子
- Java
- Kotlin
List<Book> books = Arrays.asList(
Immutables.createBook(book -> {
book.setId(1L);
book.addIntoAuthors(author -> author.setId(2L));
book.addIntoAuthors(author -> author.setId(3L));
}),
Immutables.createBook(book -> {
book.setId(2L);
book.addIntoAuthors(author -> author.setId(2L));
book.addIntoAuthors(author -> author.setId(4L));
})
);
sqlClient.saveEntities(
books,
// 可以删除此参数,因为对`saveEntities`方法而言,
// `AssociatedSaveMode.REPLACE`是默认行为
AssociatedSaveMode.REPLACE
);
val books = listOf(
Book {
id = 1L
authors().addBy { id = 2L }
authors().addBy { id = 3L }
},
Book {
id = 2L
authors().addBy { id = 2L }
authors().addBy { id = 4L }
}
)
sqlClient.saveEntities(
books,
// 可以删除此参数,因为对`saveEntities`方法而言,
// `AssociatedSaveMode.REPLACE`是默认行为
AssociatedSaveMode.REPLACE
)
假设数据库中现有数据结构为下表左列
| 数据库已有数据结构 | 用户期望保存的数据结构 |
|---|---|
| |
显然,应该包含4个行为
-
REPLACE特有的脱钩操作-
断开
Book-1和Author-1之间的关联 -
断开
Book-2和Author-1之间的关联
-
-
REPLACE从MERGE那里继承的能力-
新建
Book-1和Author-3之间的关联 -
新建
Book-2和Author-4之间的关联
-
最终会生成如下两条语句
-
REPLACE模式特有的脱钩操作,消除旧关联- H2
- Mysql
- Postgres
delete from BOOK_AUTHOR_MAPPING
where
BOOK_ID = ?
and
not (AUTHOR_ID = any(?))
/* batch-0: [1, [2, 3]] */
/* batch-1: [2, [2, 4]] */delete from BOOK_AUTHOR_MAPPING
where
BOOK_ID in (
? /* 1 */, ? /* 2 */
)
and
(BOOK_ID, AUTHOR_ID) not in (
(? /* 1 */, ? /* 2 */),
(? /* 1 */, ? /* 3 */),
(? /* 2 */, ? /* 2 */),
(? /* 2 */, ? /* 4 */)
)delete from BOOK_AUTHOR_MAPPING
where
BOOK_ID = ?
and
not (AUTHOR_ID = any(?))
/* batch-0: [1, [2, 3]] */
/* batch-1: [2, [2, 4]] */此SQL的目的在于
-
对于
Book-1而言,删除它和除了Author-2和Author-3之外的其他Author关联对于目前这个案例而言,其实就是断开
Book-1和Author-1之间的关联 -
对于
Book-2而言,删除它和除了Author-2和Author-4之外的其他Author关联对于目前这个案例而言,其实就是断开
Book-2和Author-1之间的关联
同时我们能看到,不同数据库的差异巨大
-
H2和Postgres采用了批量化的简单SQL -
MySQL采用了单条复杂SQL
信息这是
H2和Posgres支持=any(数组)的语法,MySQL不支持。 -
REPLACE模式从MERGE模式继承的能力,建立新关联- H2
- Mysql
- Postgres
merge into BOOK_AUTHOR_MAPPING tb_1_
using(values(?, ?)) tb_2_(
BOOK_ID, AUTHOR_ID
)
on
tb_1_.BOOK_ID = tb_2_.BOOK_ID
and
tb_1_.AUTHOR_ID = tb_2_.AUTHOR_ID
when not matched then
insert(BOOK_ID, AUTHOR_ID)
values(tb_2_.BOOK_ID, tb_2_.AUTHOR_ID)
/* batch-0: [1, 2] */
/* batch-1: [1, 3] */
/* batch-2: [2, 2] */
/* batch-3: [2, 3] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
insert
ignore
into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
values(? /* 1 */, ? /* 2 */) -
insert
ignore
into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
values(? /* 1 */, ? /* 3 */) -
insert
ignore
into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
values(? /* 2 */, ? /* 2 */) -
insert
ignore
into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
values(? /* 2 */, ? /* 3 */)
insert
into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
values(?, ?)
on conflict(BOOK_ID, AUTHOR_ID)
do nothing
/* batch-0: [1, 2] */
/* batch-1: [1, 3] */
/* batch-2: [2, 2] */
/* batch-3: [2, 3] */
REPLACE模式整合了INSERT、UPDATE和DELETE操作于一体,期望全量替换关联关系 (如果关联关系是深度很深的长关联,那就是全量替换一颗子树)。
然而,REPLACE模式有一个限制,那就是它期望对象要么具备id,要么具备key。
其实,REPALCE期望通过id和key巧妙地找到新旧数据结构发生变化的部分,从而把INSERT、UPDATE和DELETE操作的影响范围最小化,以达到最高性能。
如果读者对web领域有所了解的话,不难发现,这和react使用循环渲染时指定标签的key属性是一个道理。
事实上,保持指令的设计就是受react启发而来。
6. VIOLENTLY_REPLACE
我们已经讨论了REPLACE模式。然而,如果要处理wild对象,即,关联对象既无id也无key,该怎么办呢?
此时可以使用VIOLENTLY_REPLACE模式。一旦采用此模式,让Jimmer不再根据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.VIOLENTLY_REPLACE
);
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.VIOLENTLY_REPLACE
)
为了演示更简单的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
-
删除所有就的关联对象
假设上个SQL保存根对象后
MANING的id为2AMAZON的id为100
- H2
- Mysql
- Postgres
delete from BOOK
where STORE_ID = any(
? /* [2, 100] */
)delete from BOOK
where STORE_ID in (
? /* 2 */, ? /* 100 */
)delete from BOOK
where STORE_ID = any(
? /* [2, 100] */
)信息如果数据库有其他表通过外键引用
BOOK表,简单的的DELETE语句并不能删除所有关联对象。为了简化讨论,这里假设这种场景并不存在。
-
新建所有关联,以及关联对象
假设上个SQL保存根对象后
MANING的id为2AMAZON的id为100
- 绝大部分数据库
- 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 */
)
不对新旧数据进行细致的比较,只是简单地先删除关联对象再重建,这是一个非常暴力的模式,存在如下缺点
- 如果关联对象数量多,大部分情况下,这是一种低性能的方案
- 如果关联对象还有更深的关联,第1步中的删除行为可能会导致过深的级联删除,导致过多数据的丢失
因此,不推荐,请慎用
默认模式
保持指令提供的方法不仅有save、saveEntities和saveInputs,为了方便各种应用场景,还其他了快捷方法。
不同的保存 指令,AssociatedSaveMode的默认值并不相同,请参考此表
| 方法 | 默认的AssociatedSaveMode |
|---|---|
| save | REPLACE |
| saveEntities | |
| saveInputs | |
| insert | APPEND |
| insertEntities | |
| insertInputs | |
| insertIfAbsent | APPEND_IF_ABSENT |
| insertEntitiesIfAbsent | |
| insertInputsIfAbsent | |
| update | UPDATE |
| updateEntities | |
| updateInputs | |
| merge | MERGE |
| mergeEntities | |
| mergeInputs |