Files
orm-cj/README.md
T

21 KiB
Raw Blame History

SimOrm for Cangjiesimcu::orm

仓颉版 EF Core 风格 ORM基于数据模型(POCO + 注解)的映射、增删改查、数据库迁移,移植自 C# 项目 SimApiEF Core + SimApi 数据访问思路(E:\simcu\simapi-net)。

面向多数据库的方言架构:内置 PostgreSQL(PostgreSqlDialect)与 openGaussOpenGaussDialect,继承 PG)方言(对接 opengauss-driver),可通过实现 ISqlDialect 接口接入 sqlite / mysql 等新数据库。包本身零外部依赖。


引入

[dependencies]
"simcu::orm" = { path = "../orm-cj" }

数据库驱动由使用者自行引入(如 opengauss path 依赖 openGauss 驱动);构建前需设置 CANGJIE_STDX_PATH 指向本地 stdx 的 static/stdx 目录。


快速开始

1. 定义实体(数据模型)

package your_app.models

import simcu::orm.*

@Table["users"]                     // 指定表名(不写则用类简单名)
public class User {
    public var id: Int64 = 0        // 整型 id → 主键 + 数据库自增
    public var name: String = ""    // 列名 name
    @Column["full_name"]
    public var alias: String = ""   // 注解覆盖列名
    @Ignore
    public var temp: String = ""    // 不映射
    public var age: Int32 = 0
}

2. 定义 DbContext 并增删改查

@DbContext 宏(推荐,EF Core 声明式体验):纯声明类即可,宏自动补 <: DbContext、把每个 DbSet<T> 声明变成 public prop 并注入 this.set<T>()、生成 (driverName, connStr) 构造与 migrations() override

import std.database.sql.*
import opengauss.driver.*
import simcu::orm.*
import simcu::orm.macros.*

@DbContext
public class AppDbContext {
    prop users: DbSet<User>
    prop orders: DbSet<Order>
}

main() {
    // 1. 构造 DbContext"pgsql" 驱动 + 连接串),迁移 CLI 与 CRUD 共用同一个实例
    let db = AppDbContext("pgsql", "Host=..;Database=app;Username=..;Password=..")

    // 2. 增
    let user = User()
    user.name = "alice"
    db.users.add(user)            // 入队 INSERT
    db.saveChanges()              // 事务内执行,回读自增主键到 user.id

    // 3. 改
    user.age = 30
    db.users.update(user)         // 入队 UPDATE(全部非主键列)
    db.saveChanges()

    // 4. 查
    let list = db.users.query()
        .filter("age", ">", Int64(18))      // 属性名/列名自动映射列名
        .orderBy("name")
        .skip(0).take(10)
        .toList()                             // ArrayList<User>
    let one = db.users.find(Int64(1))       // ?User,按主键
    let total = db.users.query().count()

    // 5. 删
    db.users.remove(user)
    db.saveChanges()
}

不用宏时手动继承 DbContext 等价写法:补 public init(...) { super(...) } 构造,每个 DbSet 声明为 public prop users: DbSet<User> { get() { this.set<User>() } },并 override migrations()。宏就是把这几个样板步骤自动做完(详见「模块说明 3」)。

3. 迁移

import simcu::orm.migrations.*

// 定义迁移(migrationId 用 "yyyyMMddHHmmss_名称",按字典序应用)
public class InitialCreate <: Migration {
    public init() { super("20250701000000_InitialCreate", "创建用户表") }
    public override func up(builder: MigrationBuilder): Unit {
        builder.createTable("users") { tb =>
            tb.column("id", ColumnTypes.BigIntCol).primary().autoInc().notNull()
            tb.column("name", ColumnTypes.TextCol).withMaxLength(100).notNull()
            tb.column("active", ColumnTypes.BoolCol).withDefault(true)
        }
        builder.createIndex("ix_users_name", "users", idxCols)
    }
    public override func down(builder: MigrationBuilder): Unit {
        builder.dropTable("users")
    }
}

main() {
    let idxCols = ArrayList<String>()   // 索引列(API 为 ArrayList<String>
    idxCols.add("name")
    let migrator = Migrator(datasource)
    let pending = migrator.pending(migrations)   // 未执行的迁移(预览)
    migrator.migrate(migrations)                 // 事务内按序执行并记录历史
}

迁移历史记录在 simcu_orm_migrations 表(id / name / applied_at),已应用的迁移自动跳过。


从数据模型生成迁移(MigrationGenerator

不用手写 Migration 子类,直接从实体模型生成(对齐 EF Core migrations add):

import simcu::orm.*
import simcu::orm.migrations.*

main() {
    let gen = MigrationGenerator()

    // 1. 初始迁移:模型集合 → 全部 CREATE TABLEdown 自动生成 DROP TABLE
    let models = ArrayList<EntityModel>()
    models.add(ModelCache.get<User>())
    models.add(ModelCache.get<Product>())
    let m0 = gen.initial("20250701000000_InitialCreate", "初始建表", models)
    Migrator(datasource).migrate([m0])

    // 2. 模型变更后:旧模型快照(上次的 models) vs 新模型 → 增量迁移(表/列增删改)
    let models2 = ArrayList<EntityModel>()
    models2.add(ModelCache.get<User>())        // 假设 User 新增了 age 字段
    models2.add(ModelCache.get<Product>())
    let m1 = gen.diff("20250801000000_AddAge", "新增 age 列", models, models2)
    Migrator(datasource).migrate([m1])          // 旧模型列表 models 由调用方保存,充当快照
}

快照持久化(ModelSnapshot

上面 diff 需要调用方手存旧模型列表。ModelSnapshot 把模型序列化成 JSON 持久化到文件,diff 不依赖手存:

import simcu::orm.*
import simcu::orm.migrations.*

main() {
    let gen = MigrationGenerator()
    // ensure: 快照文件不存在 → 生成初始迁移并保存快照;
    //         已存在 → 载入旧快照 diff 出新迁移并覆盖保存(对齐 EF Core migrations add
    let m = gen.ensure("20250801000000_AddAge", "新增 age 列", "snapshot.json", models())
    Migrator(datasource).migrate([m])
}
  • 快照 JSON 结构:{"version":1,"models":[{"table":"users","columns":[{...}]}]},含表名、列名、类型、主键/自增/必填/最大长度;
  • ModelSnapshot 也提供 capture(models) / toJson() / fromJson(text) / save(path) / load(path)(文件不存在返回 None),可自行接入版本化/比较逻辑;
  • 零依赖:MiniJson 内置迷你 JSON 解析/序列化,不引入第三方包。

生成规则与限制(v1):

  • 初始initial(id, desc, models) → 每个模型一张 CreateTabledown 为逆序 DropTable
  • 增量diff(id, desc, oldModels, newModels) → 新增表 CreateTable、删除表 DropTable、列增删改(AddColumn/DropColumn/AlterColumn),down 自动按 up 逆序反转;
  • 字段类型 → 列类型映射:String→TextColBool→BoolColInt8/16/32/64→Tiny/Small/Int/BigIntColFloat32→RealColFloat64→FloatColDateTime→DateTimeColDecimal→DecimalColArray<Byte>→BinaryColRune→IntColDuration→BigIntCol
  • 不支持(抛异常提示手写迁移):主键列名变更、主键/自增属性变更;模型无索引注解,不生成索引;@Ignore/@Column/@Required/@MaxLength/@AutoIncrement 均会反映到 DDL。

迁移 CLI(对齐 dotnet ef

orm-cj 附带迁移 CLIsimcu::orm.cli.MigrationCli),把迁移落成 .cj 文件 + 快照 JSON,应用内嵌运行,无需安装外部工具。

应用接线(一行接入)main() 最上方先构造 DbContext 子类实例,app orm <命令> 时执行 CLI 并退出,否则走正常应用逻辑:

import simcu::orm.*
import simcu::orm.macros.*
// import myapp.migrations.*      // 生成首个迁移后手动取消注释(见下方「生成约定」)

@DbContext
public class MyDbContext {
    prop users: DbSet<User>
    prop orders: DbSet<Order>
}

main(args: Array<String>) {
    let db = MyDbContext("pgsql", "Host=..;Database=..;Username=..;Password=..")
    if (db.cli(args)) {             // `app orm add/rm/update/downgrade/list/help`
        return
    }
    // ... 正常应用启动逻辑(db 可继续用于 CRUD)
}

db.cli(args) 是 DbContext 的实例方法:命中 orm <命令> 时执行 CLI 并返回 true,否则返回 false(正常启动)。模型从实例的 DbSet 属性反射收集,连接/驱动/方言来自构造参数,无需任何回调或宏(@DbContext 宏只是省去手写继承样板,CLI 不依赖它)。

编译后运行(cjpm run -- <args> 是把参数传给应用的方式):

命令 作用
cjpm run -- orm add <迁移名> 从模型生成迁移 .cj 文件 + 更新注册文件 + 写入快照;模型无变化则跳过
cjpm run -- orm rm <迁移名或id> 移除已生成的迁移(删除迁移文件 + 注册条目;注册表清空时重置快照)
cjpm run -- orm update 连接数据库,按 migrationId 字典序应用所有未执行的迁移
cjpm run -- orm downgrade [目标] 回退迁移:无目标=回退最近一个;有目标=回退到该迁移之后(含该迁移的 down)
cjpm run -- orm list 列出已注册迁移(需真实连接读取历史表)
cjpm run -- orm help 显示帮助

生成约定

  • 首次使用(无迁移):不需要建 src/migrations/ 目录,也不要写 import <应用包名>.migrations.*(该子包不存在会导致编译失败);首次 add 时 CLI 自动创建目录并生成迁移文件 + 注册文件;
  • 生成后手动加 import:执行 add 生成首个迁移后,在 main() 所在文件手动加一行 import <应用包名>.migrations.*(如 import myapp.migrations.*)——注册文件在子包内惰性加载,只有 import 触发后 update/downgrade/list 才能看到迁移;之后新增迁移无需再动这行;
  • 迁移 / 注册文件生成在 src/migrations/,该目录是子包,文件头 package <应用包名>.migrations(如应用包 myapppackage myapp.migrations);
  • add 只生成不执行;执行迁移用 update(回退用 downgrade),或在应用启动时直接 db.migrate()
  • 快照持久化在 src/migrations/snapshot.jsonadd 用「旧快照 vs 当前模型」做 diff,所以改模型后重新编译应用再 add 即生成增量迁移;
  • 生成源码文本已包含 import std.collection.* 等标准库引用,应用无需额外配置。

项目结构

orm-cj/
├── cjpm.toml                  # 包配置(name = orm, organization = simcu
├── src/
│   ├── Orm.cj                 # 聚合导出:import simcu::orm.* 即全部可见
│   ├── annotations/           # @Table / @Column / @Key / @AutoIncrement / @Ignore / @Required / @MaxLength
│   ├── macros/                # @DbContext 类级宏(纯声明 DbSet 类 → 完整 DbContext 子类)
│   ├── model/                 # EntityModel(反射映射)、ModelCache(模型缓存)、
│   │                          #   ColumnNamingPolicy(列命名策略)、ValueReader、ParamBinder、GuidUtil
│   ├── tracking/              # ChangeTracker(操作队列)、EntityState、EntityEntry
│   ├── sql/                   # ISqlDialect 接口(独立文件)+ PostgreSqlDialectPG 实现)+
│   │                          #   OpenGaussDialect(继承 PG)、ColumnTypes(列类型)
│   ├── query/                 # QueryBuilder<T>(条件/排序/分页)、PagedResult<T>
│   ├── db/                    # DbContext(连接 + 提交 + 物化 + 数据库存在性/迁移状态检查)、DbSet<T>
│   ├── cli/                   # MigrationCliadd/rm/update/downgrade/list 命令)、
│   │                          #   MigrationFileGenerator(迁移/注册文件源码生成)
│   ├── migrations/            # Migration 基类、MigrationBuilder、ColumnDefinition、
│   │                          #   DdlFactory(操作 → DDL SQL,差异语法委托方言)、
│   │                          #   MigrationGenerator(模型 → 迁移)、Migrator(历史表 + 执行)、
│   │                          #   ModelSnapshot(模型快照 JSON 持久化)+ MiniJson(零依赖 JSON 引擎)
│   └── tests/                 # 单元测试(cjpm test,纯逻辑,不连库)

模块说明

1. 注解(对齐 .NET EF Core 数据注解)

注解 对齐 .NET 位置 说明
@Table["users"] [Table("users")] 指定表名
@Column["full_name"] [Column("full_name")] 字段 指定列名
@Key [Key] 字段 标记主键(字段名 id / _id 自动为主键)
@AutoIncrement [DatabaseGenerated(Identity)] 字段 整型主键自增(整型 id 默认自增)
@Ignore [NotMapped] 字段 不映射该字段
@Required [Required] 字段 非空列(仅影响建表 DDL
@MaxLength[100] [MaxLength(100)] 字段 字符串列最大长度(仅影响建表 DDL

2. 模型映射约定(对齐 EF Core 数据模型)

  • 表名@Table 优先,否则类简单名;
  • 列名@Column 优先,否则按 ColumnNamingPolicy(默认 StripUnderscore_ididid 原样;可切换 SnakeCaseuserNameuser_nameKeep 原样);
  • 主键@Key 标注,或字段名 _id / idv1 仅支持单主键
  • 自增@AutoIncrement,或整型(Int8-64id 主键默认自增;INSERT 跳过该列并通过 RETURNING 回读;
  • 客户端生成主键String 主键默认客户端生成(GuidUtil:时间戳 + 自增序号 hex),INSERT 时为空自动生成;
  • 实体要求:字段必须是 public var 标量类型,且提供无参 public init()

支持的字段标量类型:String / Bool / Int8~Int64 / UInt8~UInt64 / Float32 / Float64 / Rune / DateTime / Duration / Decimal / Array<Byte>

3. DbContext / DbSet(对齐 EF Core

推荐:@DbContext。把 DbSet 声明为 prop,宏自动补继承/构造/注入/migrations()

import simcu::orm.*
import simcu::orm.macros.*
import simcu::orm.migrations.*   // migrations() 签名需要 Migration
import std.collection.*          // migrations() 签名需要 ArrayList

@DbContext
public class AppDbContext {
    prop users: DbSet<User>
    prop orders: DbSet<Order>
}

// 用法(宏生成 (driverName, connStr) 构造)
let db = AppDbContext("pgsql", "Host=..;Database=app;Username=..;Password=..")
db.users.add(user)
db.saveChanges()
db.migrate()                                    // 应用迁移(对齐 EF Database.Migrate

// 状态检查(应用启动时可选)
db.databaseExists()          // 数据库是否存在(连系统库探测,不建库)
db.hasPendingMigrations()    // 是否存在未应用的迁移(历史表不存在视为有)
if (!db.databaseExists() || db.hasPendingMigrations()) { db.migrate() }

不用宏的等价手动写法:继承 DbContextpublic prop xxx: DbSet<T> { get() { this.set<T>() } },补构造并 override migrations()

public class AppDbContext <: DbContext {
    public let users: DbSet<User>
    public let orders: DbSet<Order>

    public init(driverName: String, connStr: String) { super(driverName, connStr) }
    public init(datasource: Datasource) { super(datasource) }
    public override func migrations(): ArrayList<Migration> { /* 迁移列表 */ }
}
API 说明
AppDbContext("pgsql", connStr) 构造:驱动名 + 连接串(驱动构建收敛在 DatasourceFactory;驱动名归一化,pgsql/pg/postgresqlpostgres,具体驱动由应用链接的驱动包注册)
AppDbContext(connStr) 构造:只给连接串,自动探测驱动(默认 openGauss 方言)
AppDbContext(datasource) 构造:直接给驱动 Datasource(默认 OpenGaussDialect,继承 PG
AppDbContext(datasource, dialect) 构造:自定义方言,如 PostgreSqlDialect()
db.set<T>() 获取实体的 DbSet(同类型复用同一实例;宏展开的 prop 内部即调用它)
db.pendingCount() 待提交变更条数
db.saveChanges() 事务内按入队顺序执行全部变更,返回影响条数
db.getDialect() 当前方言(子类/应用可读取)
db.migrate(migrations) 应用指定迁移(委托 Migrator),返回本次应用数量
db.migrate() 便捷版:应用子类 migrations() 提供的全部迁移
db.databaseExists() 数据库是否存在(连系统库参数化查询,需带连接串构造;sqlite 查文件)
db.hasPendingMigrations() 是否存在未应用的迁移(数据库/历史表不存在视为有待应用迁移)
set<T>().add(e) 入队新增
set<T>().update(e) 入队修改(UPDATE 全部非主键列)
set<T>().remove(e) 入队删除
set<T>().find(key) 按主键查(?T
set<T>().toList() 全表查询
set<T>().count() 全表计数
set<T>().query() 构建查询

语义说明(v1,操作队列模式)add/update/remove 只入队,saveChanges 时开启事务按入队顺序逐条执行后清空队列。与 EF Core 的 identity map 不同,同一实体重复入队会重复执行(如先 addupdate = INSERT + UPDATE),请勿对同一实体的同一种操作重复调用。

4. 查询构建器 — QueryBuilder(对齐 EF Core IQueryable 常用子集)

let qb = db.users.query()        // 宏方式直接 db.users;等价 db.set<User>().query()

qb.filter("age", ">", Int64(18))             // (属性名, 操作符, 值):自动映射列名
let ps = ArrayList<Any>()                    // (SQL 片段, 参数):按 ? 顺序,写原始列名
ps.add(Int64(18))
ps.add(true)
qb.filter("age > ? AND active = ?", ps)
qb.orderBy("name").orderByDesc("age")        // 排序
qb.skip(10).take(5)                          // 分页(LIMIT/OFFSET
qb.toList()                                  // ArrayList<User>
qb.first()                                   // ?User
qb.count()                                   // Int64,满足条件的总数
qb.page(2, 10)                               // PagedResult<User>items/total/page/pageSize/totalPages

注意:where 是仓颉关键字,条件方法命名为 filter(两参原始片段 / 三参属性映射两种重载)。

5. 迁移(对齐 EF Core Migrations

  • Migration 基类:super("20250701000000_InitialCreate", "描述"),实现 up / down
  • MigrationBuildercreateTable / dropTable / addColumn / dropColumn / alterColumn / renameColumn / createIndex / dropIndex / rawSql
  • ColumnDefinition 链式:.primary() .autoInc() .notNull() .withUnique() .withMaxLength(n) .withDefault(value)
  • ColumnTypes(定义于 simcu::orm.sql):BigIntColopenGauss 自增 → BIGSERIAL/ IntCol(自增 → SERIAL/ SmallIntCol / TinyIntCol / TextColVARCHAR,默认 255/ BoolCol / FloatColDOUBLE PRECISION/ RealCol / DateTimeCol / DecimalColDECIMAL(18,6)/ BinaryCol(BYTEA);实际 DDL 映射由方言 columnTypeSql 决定;
  • Migratorpending(migrations) 预览未执行项;migrate(migrations) 事务内按 migrationId 字典序应用未执行项并写入历史表 simcu_orm_migrations

运行测试

cjpm test    # 纯逻辑单元测试(模型映射/SQL 生成/DDL 生成/QueryBuilder),无需数据库

已知限制(v1

  • cjc 1.1.3 编译器缺陷(重要):类上带 @Table[...] 注解时,若成员变量带 ≥2 个注解且用 = "" 做空字符串初始化(如 @Required @MaxLength[100] public var x: String = ""),编译报 expected expression after '=', found '<EOF>'。规避:默认值改用 = String() 或直接省略初始化器。orm-cj 包内代码与 e2e 示例均按此规避写法;
  • 仅单主键;不支持复合主键;
  • 不支持 Option 字段?String 等请改用具体标量类型)与父类字段(不扫描继承字段);
  • 不支持导航属性 / 延迟加载 / 级联删除(对齐 EF Core 这些能力属于 v2+);
  • 不支持 LINQ 表达式树,查询以 filter 条件方法 + SQL 片段组合;
  • QueryBuilder 的 SQL 片段形式(filter(condition, params))需自行保证列名合法(会做标识符引号包裹校验外的处理)——推荐优先使用三参属性形式;
  • 自增主键回读依赖驱动 RETURNING 支持(openGauss/PostgreSQL 原生支持;换方言时由 ISqlDialect.buildInsert 决定回读策略)。

依赖

依赖 用途
数据库驱动(使用者引入,如 opengauss../opengauss-driver 实现 std.database.sqlDatasource/Connection/Statement
stdxCANGJIE_STDX_PATH 标准扩展库(std.database.sql 接口)

orm-cj 包本身零依赖:连接层走 std.database.sql 标准接口,SQL/DDL 生成走 ISqlDialect 方言接口。新数据库接入 = 实现 ISqlDialect(CRUD + 列类型映射 + 差异 DDL 语句)+ 提供对应 std.database.sql 驱动。


许可证

MIT