Skip to content

实体定义 ​

基本结构 ​

使用 @Refine 宏标记的类会被识别为实体。id 字段约定为 Int64 类型的主键。

cangjie
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 场景:

cangjie
@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:

cangjie
@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 覆盖:

cangjie
@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:

cangjie
@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 字母前插入 _,整串转小写;连续大写缩写按词边界拆分:

字段/类名转换结果说明
userNameuser_name常规驼峰
userIDuser_id结尾缩写不拆分
HTMLParserhtml_parserParser 是新词边界
user_nameuser_name已 snake_case 幂等不变
idid单字母不变
  • 表名: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 类型StorageTypeSQLiteMySQLPostgreSQL
Int64 / Int32 / Int16 / Int8 / UInt*IntegerINTEGERBIGINTBIGINT
Float64 / Float32FloatREALDOUBLEDOUBLE PRECISION
StringTextTEXTVARCHAR(255)VARCHAR(255)
BoolBoolINTEGERTINYINT(1)BOOLEAN
DateTimeTimestampTEXTDATETIME(6)TIMESTAMP

自定义存储类型 ​

使用 @Field[StorageType] 覆盖字段的存储类型:

cangjie
@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 接口(编译期强制,未实现则编译失败):

cangjie
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 自动 ::JSONB cast)
  • 限制:顶层 struct 字段不支持 Option 包裹(var profile: Option<Profile> 会被当作 ref_one 关系处理,编译失败——fail-fast 而非静默错)。可空 Json 字段需用 @Field[Json] + 手动可空列,或记档后续支持

关联字段 ​

四种关联:@Ref = 引用,@Rel = 拥有,各有一对一和一对多。

@Ref[Target, foreignKey] — ref_one ​

引用一个。你的外键指向目标表。你删,目标还在。

cangjie
@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 ​

引用多个。通过中间表间接引用。你删,目标还在。

cangjie
@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 集合)
cangjie
@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 类型)字段即自动启用审计,与软删除的字段名约定一致,零配置:

cangjie
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 时自动校验版本冲突:

cangjie
@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 自动置 1
  • Tx.update:SQL 的 WHERE 追加 AND version = ?,更新成功则 version 自动 +1(内存与数据库同步);若匹配行数为 0(版本过期或行被删)抛 OptimisticLockException
  • Tx.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 置为非零值而非物理删除,查询自动过滤已删除记录:

cangjie
@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 字段也走物理删除:

cangjie
@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列名数组函数
UserSchemaTableSchema 实现

MIT License