实体定义
基本结构
使用 @Refine 宏标记的类会被识别为实体。id 字段约定为 Int64 类型的主键。
import refine.*
import refine.macros.*
@Refine
class User {
var id: Int64 = 0
var name: String = ""
var email: String = ""
}String 主键(UUID / ULID)
id: String 时启用 String 主键模式:INSERT 包含 id 列,Tx.save / Tx.batchSave 时 id 为空则自动调用 Refine 的 ID 生成器(默认 Sonyflake,可通过 setIdGenerator(UlidIdGenerator()) 等切换),并回写实体。适合分布式无冲突 ID 场景:
@Refine
class Event {
var id: String = "" // String 主键,空时自动生成
var name: String = ""
var payload: String = ""
}
rf.setIdGenerator(UlidIdGenerator())
rf.transaction { tx: Tx =>
let ev = Event()
ev.name = "deploy"
tx.save(ev)
// ev.id 已填充,形如 "01KYVGFGPFCJEZRKBBHJ2Q59QW"
}相关配置见 Refine API - getIdGenerator/setIdGenerator。
Int64 非自增主键(应用侧生成)
@Id[auto, false] 关闭自增后,id 为 0 时自动调用 ID 生成器(结果经 Int64.parse 转换)并回写。可搭配 SonyflakeIdGenerator(趋势递增、索引友好)替代数据库自增,批量插入无需回写 id:
@Refine
class Order {
@Id[auto, false] // 不用数据库自增,应用侧生成雪花 ID
var id: Int64 = 0
var amount: Int64 = 0
}
rf.setIdGenerator(SonyflakeIdGenerator())
rf.transaction { tx: Tx =>
let o = Order()
o.amount = 100
tx.save(o)
// o.id 已填充雪花 ID
}自定义表名
默认表名是类名首字母小写(User → user, BlogPost → blogPost)。使用 @Table 覆盖:
@Refine
@Table["sys_users"]
class User {
var id: Int64 = 0
var name: String = ""
}PostgreSQL 注意:
- 标识符统一按小写处理(未加引号的 SQL 会被 PostgreSQL 折叠为小写,Refine 已保证查询/建表/CRUD 三方一致)
- 若类名映射的表名是 PostgreSQL 保留字(如
User→user、Order→order),必须用@Table指定其他表名,例如@Table["users"]- 关系(
@Ref/@Rel)的目标表名会跟随目标实体的@Table覆盖
命名约定(@Refine[naming])
实体默认表名 = 类名首字母小写(BlogPost → blogPost),列名 = 字段名(userName 即列名,无转换)。通过 @Refine[naming: "snake"] 可将表名/列名统一转换为 snake_case:
@Refine[naming: "snake"]
class BlogPost {
var id: Int64 = 0
var userName: String = "" // 列名 → user_name
}语法与内置策略
- 语法:
@Refine[naming: "snake"]/@Refine[naming: "none"];裸@Refine等价naming: "none" - 内置策略:
"snake"(驼峰 → snake_case)、"none"(默认,不转换);其他值在编译期报错
转换规则(camelToSnake)
除首字母外每个大写 ASCII 字母前插入 _,整串转小写;连续大写缩写按词边界拆分:
| 字段/类名 | 转换结果 | 说明 |
|---|---|---|
userName | user_name | 常规驼峰 |
userID | user_id | 结尾缩写不拆分 |
HTMLParser | html_parser | Parser 是新词边界 |
user_name | user_name | 已 snake_case 幂等不变 |
id | id | 单字母不变 |
- 表名:
BlogPost→blog_post;列名:userName→user_name
优先级:注解显式指定 > 全局策略 > 默认
@Table显式表名 > snake:@Table["CustomTable"]的表名原样使用(不转 snake,即仍是CustomTable而非custom_table),列名仍随策略转换(userName→user_name)@Field覆盖 storageType:@Field目前只覆盖字段存储类型、不做列名——本迭代字段列名的显式指定未实现(记档后续功能)。snake 下字段列名仍按策略转换(displayName→display_name),存储类型覆盖正常生效(如@Field[Text]→Text列)- 显式
"none"= 默认行为:@Refine[naming: "none"]与裸@Refine完全一致
关联路径的命名转换
- r.by 目标字段名:
@Ref/@Rel的by是目标实体上的字段名,SQL 列名侧随策略转换(authorId→author_id),字段访问侧保持字段名(entity.authorId可编译可读值) - junction 中间表列:中间表源列 = 源表名 +
_id,目标列 = 目标类名 + 策略转换 +_id(SnakeAuthor→snake_author_id);schema 建表 /RefMany描述符 / 管理方法 SQL 三处同源一致 - via 中间表名:显式
via不转换(用户给完整表名);默认via(关系字段名)跟随表名策略转换(relatedAuthors→ 中间表related_authors)
审计 / 软删除字段
created_at / updated_at / deleted_at 本身即 snake_case,策略转换幂等,保持字面列名不变。
⚠ 关联实体应统一命名策略:
has_one/has_many的by外键字段位于目标实体上,Refine 按源实体的策略计算该外键列名。若关联双方命名策略不一致(如源实体用snake、目标实体默认none),转换后的外键列名可能无法匹配目标实体实际的列名,导致关联查询/维护静默失效(不报错、查不到行)。同一关联链上的实体请统一使用相同的命名策略;ref_to的外键在源实体自身,不受跨实体策略差异影响。
字段类型映射
| Cangjie 类型 | StorageType | SQLite | MySQL | PostgreSQL |
|---|---|---|---|---|
Int64 / Int32 / Int16 / Int8 / UInt* | Integer | INTEGER | BIGINT | BIGINT |
Float64 / Float32 | Float | REAL | DOUBLE | DOUBLE PRECISION |
String | Text | TEXT | VARCHAR(255) | VARCHAR(255) |
Bool | Bool | INTEGER | TINYINT(1) | BOOLEAN |
DateTime | Timestamp | TEXT | DATETIME(6) | TIMESTAMP |
自定义存储类型
使用 @Field[StorageType] 覆盖字段的存储类型:
@Refine
class Article {
var id: Int64 = 0
@Field[String]
var title: String = ""
@Field[Text]
var body: String = ""
}Json struct 字段
实体字段类型是自定义 struct 时,默认映射为 StorageType.Json(SQLite TEXT / MySQL JSON / PG JSONB)。struct 需实现 stdx.encoding.json.stream 接口(编译期强制,未实现则编译失败):
import stdx.encoding.json.stream.*
import std.collection.ArrayList
class Profile <: JsonSerializable & JsonDeserializable<Profile> {
var name: String = ""
var age: Int64 = 0
var tags: ArrayList<String> = ArrayList()
public func toJson(w: JsonWriter): Unit {
w.startObject()
w.writeName("name").writeValue(this.name)
w.writeName("age").writeValue(this.age)
w.writeName("tags").writeValue(this.tags)
w.endObject()
}
public static func fromJson(r: JsonReader): Profile {
var res = Profile()
r.startObject()
while (r.peek() != EndObject) {
match (r.readName()) {
case "name" => res.name = r.readValue<String>()
case "age" => res.age = r.readValue<Int64>()
case "tags" => res.tags = r.readValue<ArrayList<String>>()
case _ => r.skip()
}
}
r.endObject()
res
}
}
@Refine
class JsonUser {
var id: Int64 = 0
var name: String = ""
var profile: Profile = Profile() // Json 列,自动序列化/反序列化
}- 写路径自动
jsonToString序列化,读路径自动stringToJson反序列化,无需手动处理 - struct 未实现
JsonSerializable/JsonDeserializable接口 → 编译失败(比运行时注册表更强的保障) - 支持嵌套 struct(Option/集合/HashMap 字段),stdx 已内置标量与集合的序列化
@Field[Text]可将 struct 存为 TEXT 列(仍 JSON 序列化);@Field[Json]覆盖 String 字段建 JSON 列(绑定原始文本,PG batchUpdate 自动::JSONBcast)- 限制:顶层 struct 字段不支持
Option包裹(var profile: Option<Profile>会被当作 ref_one 关系处理,编译失败——fail-fast 而非静默错)。可空 Json 字段需用@Field[Json]+ 手动可空列,或记档后续支持
关联字段
四种关联:@Ref = 引用,@Rel = 拥有,各有一对一和一对多。
@Ref[Target, foreignKey] — ref_one
引用一个。你的外键指向目标表。你删,目标还在。
@Refine
class Order {
var id: Int64 = 0
var total: Float64 = 0.0
var status: String = ""
var user_id: Int64 = 0
@Ref[User, user_id]
var creator: Option<User> = None
}@Ref[Target, via: ...] — ref_many
引用多个。通过中间表间接引用。你删,目标还在。
@Refine
class Order {
var id: Int64 = 0
var total: Float64 = 0.0
var status: String = ""
@Ref[Tag, via: order_tags]
var tags: ArrayList<Tag> = ArrayList<Tag>()
}via 参数指定中间表名。中间表需含 sourceTable_id 和 targetTable_id 两列。
@Rel[kind, Target, foreignKey]
| kind | 语义 | Cangjie 类型 | include 批量查询 |
|---|---|---|---|
has_many | 拥有多个 | ArrayList<Target> | SELECT target.* WHERE target.fk IN (父 id 集合) |
has_one | 拥有一个 | Option<Target> | SELECT target.* WHERE target.fk IN (父 id 集合) |
@Refine
class Order {
var id: Int64 = 0
var total: Float64 = 0.0
var status: String = ""
var user_id: Int64 = 0
// has_many: 拥有明细,删单明细跟着删
@Rel[has_many, OrderItem, order_id]
var items: ArrayList<OrderItem> = ArrayList<OrderItem>()
// has_one: 拥有一条发票记录,删单发票跟着删
@Rel[has_one, Invoice, order_id]
var invoice: Option<Invoice> = None
}
@Refine
class OrderItem {
var id: Int64 = 0
var product: String = ""
var quantity: Int64 = 0
var price: Float64 = 0.0
var order_id: Int64 = 0
}
@Refine
class Invoice {
var id: Int64 = 0
var invoice_no: String = ""
var amount: Float64 = 0.0
var order_id: Int64 = 0
}
@Refine
class Tag {
var id: Int64 = 0
var name: String = ""
}has_one/has_many:被关联物外键指向你的 id(外键在目标表),你拥有其生命周期
审计字段(created_at / updated_at)
实体声明 created_at / updated_at(DateTime 类型)字段即自动启用审计,与软删除的字段名约定一致,零配置:
import std.time.DateTime
@Refine
class Note {
var id: Int64 = 0
var content: String = ""
var created_at: DateTime = DateTime.now()
var updated_at: DateTime = DateTime.now()
}行为说明:
Tx.save/Tx.batchSave:插入时自动注入created_at与updated_at(DateTime.now())Tx.update/Tx.batchUpdate/Tx.upsert冲突更新侧:自动刷新updated_at;对于从数据库加载的实体,created_at保持不变(仅updated_at刷新);手动构造的新实体执行 update 时其created_at会被写入- 注入发生在钩子之前,
TxBeforeCreate/TxBeforeUpdate钩子中可见已填好的值 - 若需自定义时间,在钩子中覆盖即可
- 注意:
updateWhere/deleteWhere(条件批量操作)不经过实体映射层,不自动填充,由用户自行处理
乐观锁(@Version)
@Version 注解标记实体的 Int64 版本字段,Tx.update 时自动校验版本冲突:
@Refine
class Note {
var id: Int64 = 0
var content: String = ""
@Version
var version: Int64 = 0
}行为说明:
- 字段必须为
Int64,且每个实体至多一个@Version字段(违反则编译报错) Tx.save/Tx.batchSave/Tx.upsert插入时 version 为 0 自动置 1Tx.update:SQL 的 WHERE 追加AND version = ?,更新成功则 version 自动 +1(内存与数据库同步);若匹配行数为 0(版本过期或行被删)抛 OptimisticLockExceptionTx.batchUpdate:version 参与 CASE 更新(值取 version+1),执行后若匹配行数不足(有行不存在)抛 OptimisticLockException- 注意:
batchUpdate只检测「行缺失」,不校验存量行的版本是否过期(CASE 更新不比较旧版本);若并发修改了同一行,batchUpdate 会静默覆盖并写回 version+1。需要逐行版本强校验时,请使用tx.update逐条更新 Tx.upsert冲突更新侧:version 自动 +1- 注意:
Tx.upsert冲突更新时仅数据库侧的 version 递增,实体内存中的 version 不回写;若 upsert 后需继续tx.update(entity),请先重新查询实体以获取最新 version - 注意:
updateWhere/deleteWhere不校验 version - 注意:
tx.update前若查询了实体,建议在同一个事务内完成读-改-写,避免长时间持有旧 version
软删除
实体包含 deleted_at 字段时自动启用软删除。Tx.delete() 会将 deleted_at 置为非零值而非物理删除,查询自动过滤已删除记录:
@Refine
class SoftUser {
var id: Int64 = 0
var name: String = ""
@Field[Integer]
var deleted_at: Int64 = 0
}
// 软删除:UPDATE softUser SET deleted_at = 1 WHERE id = ?
rf.transaction { tx: Tx => tx.delete(user) }
// 物理删除:DELETE FROM softUser WHERE id = ?
rf.transaction { tx: Tx => tx.physicalDelete(user) }@HardDelete
@HardDelete 注解标记的实体即使有 deleted_at 字段也走物理删除:
@Refine
@HardDelete
class Log {
var id: Int64 = 0
var message: String = ""
@Field[Integer]
var deleted_at: Int64 = 0
}自动生成的代码
@Refine 宏为每个实体生成以下代码(以 User 为例):
| 生成项 | 说明 |
|---|---|
UserCols | 字段描述符结构体,内含 Col<T> 字段 |
UserRel | 关联定义类,内含 IRelation 字段 |
User.query() | 预配置的 Query<User> 工厂方法 |
User.col() | 返回 UserCols() |
User.rowMapper() | 返回 UserRowMapper 函数引用,用于 queryAll/queryOne |
User.schemas() | 返回所有 Schema(主表 + junction 表),用于迁移 |
User.findAll(q) | 静态快捷方法,调用 q.all() |
User.findOne(q) | 静态快捷方法,调用 q.one() |
Tx.save(User) | INSERT 扩展方法 |
Tx.update(User) | UPDATE 扩展方法 |
Tx.delete(User) | DELETE 扩展方法(软删除实体为 UPDATE deleted_at) |
Tx.physicalDelete(User) | 物理 DELETE 扩展方法(绕过软删除) |
UserRowMapper | 行映射器函数 |
UserColumnNames | 列名数组函数 |
UserSchema | TableSchema 实现 |