CRUD 操作
所有写操作(save/update/delete)必须在事务中执行。
Create
let user = User()
user.name = "Alice"
user.email = "alice@example.com"
rf.transaction { tx: Tx =>
tx.save(user)
}
// save 后 user.id 自动填充为数据库自增 ID级联:
tx.save会沿已标记修改的关联字段级联保存子对象(has_many / has_one)并回填 fk,ref_many会重建中间表;ref_to只维护 fk 不动被引对象。详见 关联 - 级联保存。
批量插入
批量插入多条记录,生成单条 INSERT ... VALUES (?, ?), (?, ?), ... SQL:
rf.transaction { tx: Tx =>
let u1 = User()
u1.name = "Alice"
u1.email = "alice@example.com"
let u2 = User()
u2.name = "Bob"
u2.email = "bob@example.com"
tx.batchSave([u1, u2])
// INSERT INTO user (name, email) VALUES (?, ?), (?, ?)
// 自增主键不回写,需要 id 时用 query() 查回
}- 所有参数扁平化收集到单条 SQL,避免 N 次 INSERT
- 自增主键(Int64 id)不回写:多值 INSERT 后
lastInsertId只代表第一条,且 id 不保证连续,需取 id 时用Entity.query().filter(...)查回 - String 主键(UUID/ULID)回写:插入前预生成所有 ID,批量插入后实体已携带完整 id
- Int64 非自增主键(
@Id[auto, false])回写:插入前按需生成(id 为 0 时),可搭配 Sonyflake 雪花 ID 替代数据库自增 - 支持
TxBeforeCreate/TxAfterCreate钩子(每个实体独立触发) - 含审计字段(
created_at/updated_at)的实体插入时自动填充,详见 实体定义 - 审计字段 - 空数组直接返回,不执行 SQL
- 不级联:
batchSave只插入给定实体,不沿关联字段递归(不会级联保存子对象、不会重建ref_many中间表),需要级联时请逐个tx.save(),详见 关联 - 级联保存
Upsert
tx.upsert(entity) 按主键插入或更新(存在冲突时更新,无冲突时插入):
rf.transaction { tx: Tx =>
let user = User()
user.id = 1
user.name = "Alice"
user.email = "alice@example.com"
tx.upsert(user)
// MySQL: INSERT ... ON DUPLICATE KEY UPDATE name = VALUES(name), ...
// PostgreSQL: INSERT ... ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, ...
}- 冲突判定列为主键;String 主键(UUID/ULID)为空时自动生成
- SQLite 不支持 upsert,调用时抛出异常
- 再次 upsert 相同主键不会产生重复行,更新非主键字段
钩子校验
注册 TxBeforeCreate 钩子在插入前校验:
rf.hook<User>("User", HookKind.TxBeforeCreate) { scope: Scope<User> =>
if (scope.entity.name == "") {
scope.abort(Exception("name required"))
}
}Read
最常用的查询方式是通过 Entity.query().using(rf) 链式构建:
// 查全部
let all = User.query().using(rf).all()
// 按条件
let adults = User.query().using(rf)
.filter(User.col().age >= 18)
.all()
// 查单条
let admin: Option<User> = User.query().using(rf)
.filter(User.col().email == "admin@example.com")
.one()
// 排序
let sorted = User.query().using(rf)
.order([User.col().id.desc()])
.all()
// 宏生成的 query() 已自动设置 select 字段
// 如需覆盖,使用 Expr.Column() 传入字段名findOne / findAll
实体提供静态快捷方法,无需 query().using(rf) 链式:
// findAll:无条件查全部
let all = User.findAll(User.query().using(rf))
// findOne:按条件查单条
let admin = User.findOne(
User.query().using(rf)
.filter(User.col().email == "admin@example.com")
)原始 SQL 查询
通过 rowMapper() 结合 Tx.queryAll<T>() / Tx.queryOne<T>() 执行原始 SQL:
rf.transaction { tx: Tx =>
// 查多条
let users = tx.queryAll(
"SELECT * FROM user WHERE age > ?", [18],
User.rowMapper()
)
// 查单条
let user: Option<User> = tx.queryOne(
"SELECT * FROM user WHERE id = ?", [1],
User.rowMapper()
)
}Session 上也提供同样的 queryAll<T>() / queryOne<T>() 方法。
分页
page(page, size) 一步完成分页:自动执行一次 COUNT(*) 统计总数 + 一次 LIMIT/OFFSET 查询,返回 Page<T>:
let pg: Page<User> = User.query().using(rf)
.order([User.col().id.desc()])
.page(2, 20) // 第 2 页,每页 20 条
pg.items // Array<User> 当前页数据
pg.total // Int64 总记录数
pg.page // Int64 当前页码
pg.size // Int64 每页条数
pg.totalPages() // Int64 总页数(total 为 0 时为 0)
pg.hasNext() // Bool 是否有下一页- 页码从 1 开始,
page或size小于 1 时抛异常 - 内部忽略链式设置的
limit()/offset(),以page/size为准 - 若想只取数据、自己维护分页元信息,可改用
count()+limit()+offset()组合
聚合
// 计数(忽略 LIMIT/OFFSET,使用 COUNT(*))
let count = User.query().using(rf)
.filter(User.col().age >= 18)
.count()
// 存在性检查(count > 0)
let exists = User.query().using(rf)
.filter(User.col().email == "test@example.com")
.exists()
// 求和 / 均值 / 最小 / 最大(Int64 与 Float64 字段均有重载)
let totalAge = User.query().using(rf).sum(User.col().age)
let avgAge = User.query().using(rf).avg(User.col().age) // 返回 Float64
let minAge = User.query().using(rf).min(User.col().age)
let maxAge = User.query().using(rf).max(User.col().age)sum/avg/min/max与count一样忽略 LIMIT/OFFSET,但保留filter/groupBy/having条件sum对Col<Int64>返回Int64、对Col<Float64>返回Float64;avg恒返回Float64- 满足条件的行数为 0 时,
sum返回 0,min/max返回 NULL(转换为 0)
悲观锁(SELECT ... FOR UPDATE)
forUpdate() 为查询加悲观锁,须在事务内使用,锁定本次查询命中行直到事务提交/回滚:
rf.transaction { tx: Tx =>
let found = Post.query().using(tx)
.filter(Post.col().id == 1)
.forUpdate()
.all()
// SELECT ... FROM post WHERE id = 1 FOR UPDATE
// 其他事务对被锁行执行 UPDATE/DELETE/FOR UPDATE 会阻塞,直到本事务结束
}- 必须与
using(tx)配合在事务中使用;在非事务上下文中锁无意义 - 锁在事务提交或回滚时自动释放,不会在
closeExecutor时提前释放(锁归事务管) - SQLite 方言渲染时忽略锁子句(SQLite 不支持行级锁,全库写锁由引擎自行管理)
Update
rf.transaction { tx: Tx =>
let user = User.query().using(tx)
.filter(User.col().id == 1)
.one()
.getOrThrow()
user.name = "New Name"
tx.update(user)
}乐观锁:若实体含
@Version字段,tx.update自动在 WHERE 追加版本校验并递增 version,版本过期(或行被删)时抛 OptimisticLockException,详见 实体定义 - 乐观锁。建议在同一个事务内完成读-改-写。
级联:
tx.update会沿已标记修改的关联字段级联更新子对象(子有 id → update、无 id → save),ref_many会按关联列表重建中间表;列表移除不自动删库。详见 关联 - 级联保存。
批量更新
tx.batchUpdate(entities) 用单条 SQL 更新多行不同值:
rf.transaction { tx: Tx =>
let u1 = User()
u1.id = 1
u1.name = "Alice2"
let u2 = User()
u2.id = 2
u2.name = "Bob2"
tx.batchUpdate([u1, u2])
// UPDATE users SET name = CASE id WHEN ? THEN ? WHEN ? THEN ? END WHERE id IN (?, ?)
}- 单条 SQL 跨方言(MySQL / PostgreSQL / SQLite),CASE WHEN 是标准语法
- 复合主键支持:
CASE WHEN pk1 = ? AND pk2 = ? THEN ? ... WHERE (pk1, pk2) IN ((?, ?), ...) - 空数组直接返回;全主键表(无更新列)抛出异常
- 触发
TxBeforeUpdate/TxAfterUpdate钩子(每个实体独立触发) - 含审计字段的实体自动刷新
updated_at,详见 实体定义 - 审计字段 - 含
@Version字段的实体,version 以 CASE 参与更新(值取 version+1),匹配行数不足时抛 OptimisticLockException,详见 实体定义 - 乐观锁 - 参数量为 2 × 列数 × 行数 + 主键数,大批量时注意 SQL 长度(业界同款方案,参考 GORM)
- 不级联:
batchUpdate只更新给定实体本身,不沿关联字段递归(子对象、ref_many中间表均不处理),需要级联时请逐个tx.update(),详见 关联 - 级联保存
条件更新
updateWhere(cols, vals) 按查询条件批量更新,返回受影响行数:
let updated: Int64 = Post.query().using(rf)
.filter(Post.col().user_id == author.id)
.updateWhere(["title"], ["UPDATED"])
// UPDATE post SET title = ? WHERE user_id = ?- 列名与值须等长,按序一一对应;条件复用链式
filter(),仅WHERE(及 JOIN)生效 - 返回受影响行数(数据库层面匹配的行数,MySQL 默认按变更行数统计)
- 不触发实体级
TxBeforeUpdate/TxAfterUpdate钩子(未经过实体映射层),如需钩子请逐个实体tx.update()
Delete
rf.transaction { tx: Tx =>
let user = User.query().using(tx)
.filter(User.col().id == 1)
.one()
.getOrThrow()
tx.delete(user)
}级联删除:
tx.delete沿 has 系关联从库查询子对象并级联删除(子对象遵循自身软删/硬删策略),ref_many清空中间表,ref_to不动被引对象。不依赖内存关联列表。详见 关联 - 级联保存。⚠ 原子性:级联删除会执行多条 SQL(父 + 子 + 中间表),
tx.delete不隐式开启事务——请在rf.transaction内调用以保证整体原子性(中途失败整体回滚),见 事务 与 关联 - 原子性。
条件删除
deleteWhere() 按查询条件批量删除,返回受影响行数:
let deleted: Int64 = Post.query().using(rf)
.filter(Post.col().user_id == author.id)
.deleteWhere()
// DELETE FROM post WHERE user_id = ?- 条件复用链式
filter();若不加条件则删除全表,请务必先确认WHERE已设置 - 不触发
TxBeforeDelete/TxAfterDelete钩子(未经过实体映射层)