Skip to content

高级特性


ValueTransformer — 列值转换器

ValueTransformer 在读/写数据库时对字段值做双向转换:可用于加密存储、枚举序列化、JSON 字段等。

定义转换器

cangjie
package demo.transform

import ace_orm.*

// 枚举 ↔ 字符串转换器
public class StatusTransformer <: ValueTransformer {
    // 写入 DB 前:仓颉值 → DbValue
    public func to(v: DbValue): DbValue {
        match (v) {
            case DbText("active")   => DbText("A")
            case DbText("inactive") => DbText("I")
            case DbText("banned")   => DbText("B")
            case _                  => DbNull
        }
    }

    // 从 DB 读出后:DbValue → 仓颉值
    public func from(v: DbValue): DbValue {
        match (v) {
            case DbText("A") => DbText("active")
            case DbText("I") => DbText("inactive")
            case DbText("B") => DbText("banned")
            case _           => DbText("unknown")
        }
    }
}

在 @Column 上绑定

cangjie
@Entity["users"]
public class User {
    @Id[]
    public var id: Int64 = 0

    // transformer = 转换器类名(字符串),type = 存储类型(覆盖自动推断)
    @Column[transformer: "StatusTransformer", type: "TEXT"]
    public var status: String = "active"
}

TIP

转换器在宏编译时注册到全局表,运行时零反射直调。type= 必须显式指定为存储形态的 DDL 类型,否则方言自动推断可能不符预期。

加密转换器示例

cangjie
public class AesTransformer <: ValueTransformer {
    public func to(v: DbValue): DbValue {
        match (v) {
            case DbText(s) => DbText(aesEncrypt(s))
            case _         => v
        }
    }

    public func from(v: DbValue): DbValue {
        match (v) {
            case DbText(s) => DbText(aesDecrypt(s))
            case _         => v
        }
    }
}

@Embeddable / @Embedded — 嵌入实体

将多个字段封装为值对象(@Embeddable 类),嵌入到宿主实体(@Embedded),数据库仍展平到宿主表。

定义嵌入类

cangjie
@Embeddable
public class Address {
    public var street: String = ""
    public var city: String   = ""
    public var country: String = ""
    public var zipCode: String = ""
}

嵌入到宿主实体

cangjie
@Entity["users"]
public class User {
    @Id[]
    public var id: Int64 = 0

    @Column[]
    public var name: String = ""

    // 展平到 users 表,列名 = street/city/country/zipCode
    @Embedded[]
    public var address: Address = Address()

    // 带前缀:列名 = shipping_street / shipping_city / ...
    @Embedded[prefix: "shipping_"]
    public var shippingAddress: Address = Address()
}

实际建表:

sql
CREATE TABLE users (
    id       INTEGER PRIMARY KEY AUTOINCREMENT,
    name     TEXT,
    street   TEXT,
    city     TEXT,
    country  TEXT,
    zipCode  TEXT,
    shipping_street  TEXT,
    shipping_city    TEXT,
    shipping_country TEXT,
    shipping_zipCode TEXT
);

使用

cangjie
let user = User()
user.name = "Alice"
user.address.street  = "中关村大街 1 号"
user.address.city    = "北京"
user.address.country = "CN"

userRepo.insert(user)

let loaded = userRepo.findById(DbInt(1)).getOrThrow()
println(loaded.address.city)   // "北京"

限制

  • 仅支持单层嵌入(嵌入类内不能再 @Embedded
  • 宿主字段类型必须为 @Embeddable 类本身,不支持 ?Address
  • 嵌入类内部 @Column["alias"] 显式别名暂不生效,列名仅由字段名(+ 前缀)决定

单表继承 STI

STI(Single Table Inheritance)将类继承层次的所有子类存到同一张表,通过判别列区分类型。

基类

cangjie
@Entity["vehicles"]
@TableInheritance["type"]    // "type" = 判别列名
open public class Vehicle {
    @Id[]
    public var id: Int64 = 0

    @Column[]
    public var make: String = ""

    @Column[]
    public var model: String = ""
}

子类

cangjie
@ChildEntity[Vehicle, "car"]   // "car" = 判别值(写入 type 列)
public class Car <: Vehicle {
    @Column[]
    public var doors: Int64 = 4
}

@ChildEntity[Vehicle, "truck"]
public class Truck <: Vehicle {
    @Column[]
    public var payload: Float64 = 0.0
}

使用

cangjie
// 子仓储自动按判别值过滤
let cars = carRepo.findAll()      // SELECT ... WHERE type = 'car'

// 子类独有字段可正常赋值
let car = Car()
car.make  = "Toyota"
car.model = "Camry"
car.doors = 4
carRepo.insert(car)

// 基类仓储多态返回(根据 type 列构造对应子类实例)
let all = vehicleRepo.findAll()   // Array<Vehicle>,实例可能是 Car 或 Truck
for (v in all) {
    match (v) {
        case c: Car   => println("Car: ${c.make} ${c.doors} doors")
        case t: Truck => println("Truck: ${t.make} payload ${t.payload}t")
        case _        => ()
    }
}

STI 限制

  • 子类自有关系(@ManyToOne 等)暂不支持 JOIN 展开,使用基类映射器操作
  • 子类之间共享同一张表,子类独有列对其他子类的行为 NULL(ORM 自动处理)

@EventSubscriber — 全局事件订阅

@EventSubscriber 跨实体监听生命周期事件(insert/update/remove),无需在每个实体上重复声明。

定义订阅者

cangjie
package demo.audit

import ace_orm.*
import ace_orm.macros.*

@EventSubscriber
public class AuditSubscriber {
    // 任意实体 insert 成功后触发
    @AfterInsert[]
    func onInsert(table: String, id: Int64): Unit {
        println("[audit] INSERT ${table} id=${id}")
    }

    // 任意实体 update 成功后触发
    @AfterUpdate[]
    func onUpdate(table: String, id: Int64): Unit {
        println("[audit] UPDATE ${table} id=${id}")
    }

    // 任意实体 remove 成功后触发
    @AfterRemove[]
    func onRemove(table: String, id: Int64): Unit {
        println("[audit] DELETE ${table} id=${id}")
    }
}

订阅者在程序启动期自动注册(与 @Service/@Controller 相同机制),无需手动 register

用途示例

  • 写审计日志(操作人、时间、受影响表/行)
  • 发布领域事件到消息队列
  • 清空缓存(实体更新后让 TtlCache 失效)

InstrumentedDriver — 可观测性装饰器

InstrumentedDriver 以装饰器模式包裹任意 Driver,在每次 SQL 执行前后注入观测逻辑:慢查询日志、Metrics 计数、追踪等。

基本用法

cangjie
import ace_orm.*

// 包裹真实驱动
let realDriver = SqliteDriver("app.db")
let driver = InstrumentedDriver(realDriver)

let ds = DataSource(driver)

默认行为:

  • 执行耗时 > 100ms 的 SQL 写入结构化慢查询日志
  • 每次 exec/query 记录执行计数

自定义阈值

cangjie
// 将慢查询阈值改为 50ms
let driver = InstrumentedDriver(realDriver, slowMs: 50)

与 OrmComponent 集成

ace.toml 中开启可观测:

toml
[datasource]
driver       = "postgres"
url          = "host=localhost port=5432 user=app password=secret dbname=appdb"
instrumented = true    # 自动包裹 InstrumentedDriver
slowMs       = 100

启用后,OrmComponent 自动用 InstrumentedDriver 包裹底层驱动,并将指标暴露给 ace-observability(若引入)。

重要:streamQuery 委托

InstrumentedDriverstreamQuery 做了显式委托

cangjie
// InstrumentedDriver 内部
public func streamQuery(sql, params, chunkSize, cb) {
    inner.streamQuery(sql, params, chunkSize, cb)   // 委托给真实驱动
}

这确保 PostgreSQL 的原生游标(DECLARE/FETCH/CLOSE)不被 LIMIT/OFFSET 降级替换。


连接池配置

ConnectionPool 由各驱动内部管理,通过 URL 参数配置:

# PostgreSQL
host=localhost port=5432 user=app password=secret dbname=db pool.max=20 pool.min=2

# MySQL
host=127.0.0.1 port=3306 user=root password=xxx dbname=app pool.max=10 pool.min=1
参数说明默认
pool.max最大连接数10
pool.min最小保持连接数(空闲回收保底)1

SQLite 每操作开关连接(perOp 模式),无连接池概念。

健康检查

cangjie
import ace_orm.*

let health = OrmHealthIndicator(ds)
match (health.check()) {
    case HealthStatus.UP   => println("DB OK")
    case HealthStatus.DOWN => println("DB 不可用")
}

ace-observability/actuator/health 端点自动集成。


多数据源

cangjie
import ace_orm.*

let primary = DataSource("postgres",
    "host=primary-db port=5432 user=app password=secret dbname=app")

let replica = DataSource("postgres",
    "host=replica-db port=5432 user=app password=secret dbname=app pool.max=30")

// 注册命名数据源
DataSourceRegistry.register("primary", primary)
DataSourceRegistry.register("replica", replica)

// 在代码中按名获取
let ds = DataSourceRegistry.get("replica").getOrThrow()
let repo = UserRepository(ds)
let users = repo.findAll()

读写分离

cangjie
@Service
public class UserService {
    var writeRepo: UserRepository
    var readRepo: UserRepository

    public init() {
        writeRepo = UserRepository(DataSourceRegistry.get("primary").getOrThrow())
        readRepo  = UserRepository(DataSourceRegistry.get("replica").getOrThrow())
    }

    @Transactional
    public func createUser(req: CreateUserReq): User {
        writeRepo.insert(User(name: req.name, email: req.email))
    }

    public func listUsers(): Array<User> {
        readRepo.findAll()   // 读副本
    }
}

路由驱动(RoutingDriver)

RoutingDriver 是多数据源路由的另一种方式:单个驱动对象,根据规则动态选择后端:

cangjie
import ace_orm.*

let routing = RoutingDriver(HashMap<String, Driver>([
    ("write", PostgresDriver("host=primary-db ...")),
    ("read",  PostgresDriver("host=replica-db ..."))
]), defaultKey: "write")

// 在事务中强制走写库
let ds = DataSource(routing)

查询缓存

通过 InstrumentedDriver 或 Repository 级配置开启查询结果缓存(基于 TtlCache):

cangjie
// Repository 方法级缓存(@Cacheable 需引入 ace-observability)
@Cacheable[ttl: 300]   // 缓存 300 秒
public func hotQuery(): Array<User> {
    userRepo.findBy([eq("status", DbText("active"))])
}

详见 可观测性文档

基于 Apache-2.0 许可证发布