从查询插入和合并数据
使用createInsert或createUpsert,可以把base-query的类型安全查询结果
写入目标表。这适用于导入查询结果、复制数据,以及对多行执行计算更新,无需为每行构造实体对象。
source决定参与操作的数据行,指令则显式地把source表达式映射到一个目标表的物理属性。
直接选择实体表
对于实体source,可以选择table并定义过滤条件,无需在source和mutation中重复列出属性。
下面假定存在单独的BookStoreArchive实体,其物理属性id和name与BookStore兼容:
- Java
- Kotlin
BookStoreTable store = BookStoreTable.$;
BookStoreArchiveTable archive = BookStoreArchiveTable.$;
BookStoreTable selectedStores = sqlClient
.createBaseQuery(store)
.where(store.name().eq("MANNING"))
.select(store)
.asBaseTable();
int affectedRowCount = sqlClient
.createInsert(archive, selectedStores)
.set(archive.id(), selectedStores.id())
.set(archive.name(), selectedStores.name())
.execute();
val selectedStores = sqlClient.createBaseQuery(BookStore::class) {
where(table.name eq "MANNING")
select(table)
}.asBaseTable()
val affectedRowCount = sqlClient
.createInsert(BookStoreArchive::class, selectedStores) {
set(table.id, sourceTable.id)
set(table.name, sourceTable.name)
}
.execute()
Java直接使用selectedStores.id(),Kotlin直接使用sourceTable.id,无需get_1()/_1
包装。投影反向传播只导出mutation需要的列,因此未使用的website等属性不会被选择。
过滤条件仍决定哪些行参与操作。
同一个source也适用于createUpsert、returning指令和普通查询。查询示例、嵌套source和
集合操作的行为请参阅直接选择实体表。
导出的table始终绑定到source查询,不能用作mutation目标;应像上例一样单独指定目标表。
普通table单例需要先通过base-query导出,才能作为mutation的source。
准备类型安全的source
source可以来自实体查询、关联表查询、派生表、CTE、递归CTE或联合查询。连接、过滤和聚合都在 source查询中定义。类型化元组可以为查询结果的列提供有意义的名称。
下面先声明输入行,再用一个没有根表的查询提供新书店的数据。示例假定BookStore模型的id类型为Long:
- Java
- Kotlin
@TypedTuple
@lombok.Data
public class StoreInput {
private final Long id;
private final String name;
private final String website;
}
@TypedTuple
data class StoreInput(
val id: Long,
val name: String,
val website: String?
)
- Java
- Kotlin
StoreInputTable source = sqlClient
.createBaseQuery()
.select(
StoreInputMapper
.id(Expression.value(100L))
.name(Expression.value("New Store"))
.website(Expression.value("https://example.com"))
)
.asBaseTable();
val source = baseTableSymbol {
sqlClient.createBaseQuery {
select(
StoreInputMapper
.id(value(100L))
.name(value("New Store"))
.website(value("https://example.com"))
)
}
}
把source替换为普通的多行base-query,不需要改变后面的修改API。也可以通过createQuery(source)
单独查询它。需要CTE时,对base-query使用asCteBaseTable()。
插入
- Java
- Kotlin
BookStoreTable store = BookStoreTable.$;
int affectedRowCount = sqlClient
.createInsert(store, source)
.set(store.id(), source.getId())
.set(store.name(), source.getName())
.set(store.website(), source.getWebsite())
.execute();
val affectedRowCount = sqlClient
.createInsert(BookStore::class, source) {
set(table.id, sourceTable.id)
set(table.name, sourceTable.name)
set(table.website, sourceTable.website)
}
.execute()
set定义插入赋值。值可以是source表达式或常量,不能读取目标表中已经存在的行。
数据库方言支持该操作时,会生成INSERT ... SELECT。
未指定的目标列保留正常的生成值、数据库默认值以及框架初始化 行为。例如,未指定的@Version
会在插入时初始化。没有默认值的必填列被省略时,插入仍然会失败。
忽略冲突
插入默认采用严格模式,冲突会导致错误。要跳过与指定唯一键冲突的数据行,在execute之前添加以下配置之一:
- Java
- Kotlin
.onConflictDoNothing(store.id())
// Or infer the conflict key from metadata:
.onConflictDoNothing()
onConflictDoNothing(table.id)
// Or infer the conflict key from metadata:
onConflictDoNothing()
显式形式必须覆盖完整的id或元数据声明的一个key组,每个冲突列都需要插入赋值。无参数形式 选择优先级最高且具备完整赋值的组:先检查id,再按元数据顺序检查key组。它选择的是一个键, 不是表上的所有唯一约束。没有可用的组时,指令会在修改数据前报错。
显式传入空属性数组是不合法的;需要推断时应使用无参数重载。被跳过的冲突行不会包含在returning结果中。
从实体查询提供source数据行
同一 个StoreInputMapper也可以选择普通查询的结果。下面的source选择已有书店,并提供新的网址。
把它用于后面的upsert,即可更新被选中的数据行:
- Java
- Kotlin
BookStoreTable existing = new BookStoreTable();
StoreInputTable source = sqlClient
.createBaseQuery(existing)
.where(existing.name().eq("Old Store"))
.select(
StoreInputMapper
.id(existing.id())
.name(existing.name())
.website(Expression.value("https://new.example.com"))
)
.asBaseTable();
val source = baseTableSymbol {
sqlClient.createBaseQuery(BookStore::class) {
where(table.name eq "Old Store")
select(
StoreInputMapper
.id(table.id)
.name(table.name)
.website(value("https://new.example.com"))
)
}
}
查询提供全部匹配行,应用代码无需先把它们加载为实体对象。这些id已经存在,因此把这个source 用于严格插入会产生冲突。
Upsert
Upsert分别声明冲突键、仅插入的值、仅更新的表达式,以及两个分支都要写入的值:
- Java
- Kotlin
int affectedRowCount = sqlClient
.createUpsert(store, source)
.key(store.id(), source.getId())
.insert(store.name(), source.getName())
.merge(store.website(), source.getWebsite())
.update(store.version(), store.version().plus(1))
.updateWhere(store.version().lt(10))
.execute();
val affectedRowCount = sqlClient
.createUpsert(BookStore::class, source) {
key(table.id, sourceTable.id)
insert(table.name, sourceTable.name)
merge(table.website, sourceTable.website)
update(table.version, table.version + 1)
updateWhere(table.version lt 10)
}
.execute()
对于新id,该指令插入名称和网址,并把版本初始化为零。对于已经存在且版本小于十的id,它保留 名称、替换网址,并把数据库中的版本加一。版本已经达到十或更大的冲突行保持不变。
| 方法 | 插入分支 | 被接受的更新分支 |
|---|---|---|
key(target, source) | 插入source值,并用于定位冲突 | 保留键值 |
insert(target, source) | 插入source值 | 保留原值 |
update(target, expression) | 使用默认值或框架初始化值 | 赋予表达式的值 |
merge(target, source) | 插入source值 | 赋予source值 |
merge(target, insertSource, updateExpression) | 使用插入表达式 | 使用更新表达式 |
每个物理目标列只能赋值一次。同一列需要不同的插入和更新表达式时,使用三个参数的merge。
更新表达式
更新表达式可以读取目标表中的现有行和source。例如,给定价格source,
merge(book.price(), source.getPrice(), book.price().plus(source.getPrice()))
会为新行插入source价格,为冲突行把source价格累加到数据库价格上。
update不提供插入值,适用于需要保留数据库默认值,或者只对现有行有意义的表达式。其目标必须
映射到一个物理列,包括嵌入对象的标量成员或关联维护方的外键id。Id、鉴别器和逻辑删除属性
不能作为仅更新的目标。
版本采用赋值语义:不会隐式添加乐观锁条件,也不会自动递增。需要递增时必须像上面的示例一样 显式指定。保存指令中对应的配置请参阅版本模式。
键和更新条件
所有key赋值必须共同覆盖一个完整的唯一id或key组。source中不能出现该键相同的多行数据。
物化执行计划会在写入前检查source键重复;原生SQL执行计划由数据库处理对这一前提的违反。
updateWhere只限制冲突行的更新分支。没有冲突的行仍然插入。多次调用中的非null条件以AND
组合,条件可以同时使用target和source的值。
没有update或merge赋值的upsert仍然具有更新语义。必要时Jimmer会生成安全的自赋值操作,
这可能影响数据库触发器、锁、生成值、returning和影响行数。它与onConflictDoNothing()不同,
而且updateWhere仍然生效。
返回数据行
- Java
- Kotlin
List<Tuple2<Long, Integer>> rows = sqlClient
.createUpsert(store, source)
.key(store.id(), source.getId())
.insert(store.name(), source.getName())
.merge(store.website(), source.getWebsite())
.update(store.version(), store.version().plus(1))
.returning(store.id(), store.version())
.execute();