级联更新插入
本文将指导您如何使用Kronos的级联插入或更新功能。
级联插入或更新
使用KPojo.upsert().on(...).cascade(...).execute()可以对根记录执行更新插入,并让已经声明的@Cascade关系参与本次操作。
级联 upsert 使用 📚更新插入 中的按匹配字段 upsert 流程:
1. 通过 `on { ... }` 指定的字段检查根记录是否存在。
2. 记录存在时,执行 update 分支,并应用级联更新行为。
3. 记录不存在时,执行 insert 分支,并应用级联插入行为。级联 upsert 会进入 insert 或 update 分支,因此@Cascade.usage允许KOperationType.INSERT或KOperationType.UPDATE时,该关系会参与级联。@Cascade默认usage已经包含这两类操作。关系映射规则见 📚级联关系定义。
开启级联 upsert
级联默认开启,也可以通过.cascade(true)显式开启。
kotlin
@Table("account")
data class Account(
@PrimaryKey
var id: Long? = null,
var email: String? = null,
var profile: AccountProfile? = null,
var auditLogs: List<AccountAudit> = listOf()
) : KPojo
@Table("account_profile")
data class AccountProfile(
@PrimaryKey
var id: Long? = null,
var accountEmail: String? = null,
var displayName: String? = null,
@Cascade(["accountEmail"], ["email"])
var account: Account? = null
) : KPojo
@Table("account_audit")
data class AccountAudit(
@PrimaryKey
var id: Long? = null,
var accountEmail: String? = null,
var action: String? = null,
@Cascade(["accountEmail"], ["email"])
var account: Account? = null
) : KPojo
Account(
id = 1,
email = "[email protected]",
profile = AccountProfile(displayName = "Ada")
)
.upsert { it.email }
.on { it.id }
.cascade(true)
.execute()mysql
SELECT COUNT(1) FROM `account` WHERE `id` = :id LIMIT 1 FOR UPDATE;
# 根记录不存在时
INSERT INTO `account` (`id`, `email`) VALUES (:id, :email);
INSERT INTO `account_profile` (`account_email`, `display_name`) VALUES (:accountEmail, :displayName);
# 根记录存在时
SELECT `id`, `email` FROM `account` WHERE `id` = :id;
SELECT `id`, `account_email` AS `accountEmail`, `display_name` AS `displayName`
FROM `account_profile`
WHERE `account_email` = :accountEmail;
UPDATE `account_profile`
SET `account_email` = :accountEmailNew
WHERE `id` = :id AND `account_email` = :accountEmail;
UPDATE `account` SET `email` = :emailNew WHERE `id` = :id;结果形态:
result
根记录不存在:先插入 account,再插入 account_profile,account_profile.account_email = account.email。
根记录存在:当 email 变化时,先更新 account_profile.account_email,再更新 account.email。一对一级联 upsert
一对一关系中,在根对象上放置引用对象,并通过@Cascade声明引用字段。
kotlin
@Table("user_profile")
data class UserProfile(
@PrimaryKey
var id: Long? = null,
var userCode: String? = null,
var nickname: String? = null,
@Cascade(["userCode"], ["code"])
var user: User? = null
) : KPojo
@Table("user")
data class User(
@PrimaryKey
var id: Long? = null,
var code: String? = null,
var profile: UserProfile? = null
) : KPojo
User(
id = 10,
code = "U-001",
profile = UserProfile(nickname = "Ada")
)
.upsert { it.code }
.on { it.id }
.execute()execution order
1. 通过 id 对 `user` 执行 SELECT COUNT(1)。
2. insert 分支:插入 `user`,再插入 `user_profile`。
3. update 分支:查询已有 `user`,查询匹配的 `user_profile`,更新 profile 引用字段,再更新 `user`。mysql
INSERT INTO `user` (`id`, `code`) VALUES (:id, :code);
INSERT INTO `user_profile` (`user_code`, `nickname`) VALUES (:userCode, :nickname);
UPDATE `user_profile`
SET `user_code` = :userCodeNew
WHERE `id` = :id AND `user_code` = :userCode;
UPDATE `user` SET `code` = :codeNew WHERE `id` = :id;结果形态:
result
子表引用列会由 `@Cascade` 声明的父表字段填充。
update 分支中,当父表字段变化时,Kronos 会更新依赖该字段的子表引用列。一对多级联 upsert
一对多关系中,在父对象上放置子对象列表,并在子对象回到父对象的引用属性上声明@Cascade。
kotlin
@Table("catalog")
data class Catalog(
@PrimaryKey
var id: Long? = null,
var code: String? = null,
var items: List<CatalogItem> = listOf()
) : KPojo
@Table("catalog_item")
data class CatalogItem(
@PrimaryKey
var id: Long? = null,
var catalogCode: String? = null,
var sku: String? = null,
@Cascade(["catalogCode"], ["code"])
var catalog: Catalog? = null
) : KPojo
Catalog(
id = 20,
code = "SPRING",
items = listOf(
CatalogItem(sku = "SKU-1"),
CatalogItem(sku = "SKU-2")
)
)
.upsert { it.code }
.on { it.id }
.execute()execution order
1. 通过 id 对 `catalog` 执行 SELECT COUNT(1)。
2. insert 分支:插入 `catalog`,再插入每条 `catalog_item`。
3. update 分支:查询已有 `catalog`,通过旧 catalog code 查询子记录,更新子表引用字段,再更新 `catalog`。mysql
INSERT INTO `catalog` (`id`, `code`) VALUES (:id, :code);
INSERT INTO `catalog_item` (`catalog_code`, `sku`) VALUES (:catalogCode, :sku);
SELECT `id`, `catalog_code` AS `catalogCode`, `sku`
FROM `catalog_item`
WHERE `catalog_code` = :catalogCode;
UPDATE `catalog_item`
SET `catalog_code` = :catalogCodeNew
WHERE `id` = :id AND `catalog_code` = :catalogCode;
UPDATE `catalog` SET `code` = :codeNew WHERE `id` = :id;结果形态:
result
插入的子记录会从 catalog.code 得到 catalog_code。
update 分支中,当 catalog.code 变化时,匹配的子记录会先得到新的 catalog_code,然后更新父记录。指定级联字段
使用.cascade { [...] }可以只让选中的引用字段参与本次级联 upsert。回调中必须选中至少一个引用属性。
kotlin
Account(
id = 1,
email = "[email protected]",
profile = AccountProfile(displayName = "Ada"),
auditLogs = listOf(AccountAudit(action = "created"))
)
.upsert { it.email }
.on { it.id }
.cascade { [Account::profile] }
.execute()execution order
1. 根记录存在性检查仍然使用 `on { it.id }`。
2. 根记录 insert 或 update 任务照常执行。
3. 只有 `Account::profile` 进入级联树。mysql
SELECT COUNT(1) FROM `account` WHERE `id` = :id LIMIT 1 FOR UPDATE;
INSERT INTO `account` (`id`, `email`) VALUES (:id, :email);
INSERT INTO `account_profile` (`account_email`, `display_name`) VALUES (:accountEmail, :displayName);结果形态:
result
profile 记录会参与级联。
Account 上其他可级联属性本次调用保持数据库现状。关闭级联 upsert
当本次调用只需要影响根表时,使用.cascade(false)。
kotlin
Account(
id = 1,
email = "[email protected]",
profile = AccountProfile(displayName = "Ada")
)
.upsert { it.email }
.on { it.id }
.cascade(false)
.execute()mysql
SELECT COUNT(1) FROM `account` WHERE `id` = :id LIMIT 1 FOR UPDATE;
# 根记录不存在时
INSERT INTO `account` (`id`, `email`) VALUES (:id, :email);
# 根记录存在时
UPDATE `account` SET `email` = :emailNew WHERE `id` = :id;结果形态:
result
只有 account 记录被插入或更新。
内存中的 profile 对象不会参与本次数据库调用。逻辑删除和乐观锁字段
级联 upsert 沿用 insert 与 update 的策略字段。insert 分支会为@LogicDelete和@Version字段写入初始值。update 分支可以通过on字段匹配逻辑删除记录,将逻辑删除列恢复为正常值,并通过更新策略递增版本列。
kotlin
@Table("account_flag")
data class AccountFlag(
@PrimaryKey
var id: Long? = null,
var name: String? = null,
@LogicDelete
@Default("0") // @Default("false") for Postgres
var deleted: Boolean? = null,
@Version
var version: Int? = null
) : KPojo
AccountFlag(id = 1, name = "active")
.upsert { it.name }
.on { it.id }
.execute()mysql
# insert 分支
INSERT INTO `account_flag` (`id`, `name`, `deleted`, `version`)
VALUES (:id, :name, :deleted, :version);
# update 分支
UPDATE `account_flag`
SET `name` = :nameNew,
`deleted` = :deletedNew,
`version` = `version` + :version2PlusNew
WHERE `id` = :id;结果形态:
result
新插入记录:deleted = false,version = 0。
通过 id 匹配到的逻辑删除记录:name 被更新,deleted 恢复为 false,version 递增。
@Version字段交给 Kronos 管理。update 分支中手动更新 version 字段时,update planner 会抛出错误。
主键、唯一键和 onConflict 边界
on { ... }中的字段决定根记录存在性检查。可以使用主键,也可以使用与业务唯一键一致的一组字段。
kotlin
@Table("product")
@TableIndex(name = "uk_product_tenant_code", columns = ["tenant_id", "code"], type = "UNIQUE")
data class Product(
@PrimaryKey
var id: Long? = null,
var tenantId: Long? = null,
var code: String? = null,
var name: String? = null
) : KPojo
Product(id = 100, tenantId = 7, code = "A-100", name = "Desk")
.upsert { it.name }
.on { [it.tenantId, it.code] }
.execute()mysql
SELECT COUNT(1)
FROM `product`
WHERE `tenant_id` = :tenantId AND `code` = :code
LIMIT 1 FOR UPDATE;
UPDATE `product`
SET `name` = :nameNew
WHERE `tenant_id` = :tenantId AND `code` = :code;结果形态:
result
tenantId + code 匹配到的记录会被更新。
tenantId + code 没有匹配记录时,Kronos 会插入 product 记录。对于自增主键,insert 分支的级联可以使用根记录生成的 id 来写入子记录引用。
kotlin
@Table("identity_parent")
data class IdentityParent(
@PrimaryKey(identity = true)
var id: Long? = null,
var code: String? = null,
var children: List<IdentityChild> = listOf()
) : KPojo
@Table("identity_child")
data class IdentityChild(
@PrimaryKey(identity = true)
var id: Long? = null,
var parentId: Long? = null,
var name: String? = null,
@Cascade(["parentId"], ["id"])
var parent: IdentityParent? = null
) : KPojo
IdentityParent(
code = "P-1",
children = listOf(IdentityChild(name = "child-1"))
)
.upsert { it.code }
.on { it.code }
.execute()result
父记录插入时会省略自增 id。
父记录插入返回生成 id 后,child.parentId 会在子记录插入前被填充。onConflict()只处理根表 upsert。需要执行级联树时,使用上面的按匹配字段 upsert 流程。
kotlin
Product(id = 100, tenantId = 7, code = "A-100", name = "Desk")
.upsert { it.name }
.on { [it.tenantId, it.code] }
.onConflict()
.execute()mysql
INSERT INTO `product` (`id`, `tenant_id`, `code`, `name`)
VALUES (:id, :tenantId, :code, :name)
ON DUPLICATE KEY UPDATE `name` = :name;结果形态:
result
生成的任务包含 product 根表 upsert 语句。
`onConflict()` 需要数据库在冲突字段上存在唯一性约束。
省略 `on { ... }` 时,Kronos 会从主键值或已声明唯一索引中推导冲突字段。