Files
orm-cj/README.md
T

406 lines
21 KiB
Markdown
Raw Normal View History

# SimOrm for Cangjiesimcu::orm
> 仓颉版 EF Core 风格 ORM:**基于数据模型(POCO + 注解)的映射、增删改查、数据库迁移**,移植自 C# 项目 [SimApi](https://github.com/SimcuTeam/simapi-net) 的 `EF Core + SimApi` 数据访问思路(`E:\simcu\simapi-net`)。
面向多数据库的方言架构:内置 PostgreSQL(`PostgreSqlDialect`)与 openGauss`OpenGaussDialect`,继承 PG)方言(对接 [opengauss-driver](../opengauss-driver)),可通过实现 `ISqlDialect` 接口接入 sqlite / mysql 等新数据库。包本身零外部依赖。
---
## 引入
```toml
[dependencies]
"simcu::orm" = { path = "../orm-cj" }
```
> 数据库驱动由使用者自行引入(如 `opengauss` path 依赖 openGauss 驱动);构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。
---
## 快速开始
### 1. 定义实体(数据模型)
```cangjie
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
```cangjie
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. 迁移
```cangjie
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`):
```cangjie
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` 不依赖手存:
```cangjie
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)` → 每个模型一张 `CreateTable``down` 为逆序 `DropTable`
- **增量**`diff(id, desc, oldModels, newModels)` → 新增表 `CreateTable`、删除表 `DropTable`、列增删改(`AddColumn`/`DropColumn`/`AlterColumn`),`down` 自动按 up 逆序反转;
- 字段类型 → 列类型映射:`String→TextCol``Bool→BoolCol``Int8/16/32/64→Tiny/Small/Int/BigIntCol``Float32→RealCol``Float64→FloatCol``DateTime→DateTimeCol``Decimal→DecimalCol``Array<Byte>→BinaryCol``Rune→IntCol``Duration→BigIntCol`
- **不支持**(抛异常提示手写迁移):主键列名变更、主键/自增属性变更;模型无索引注解,不生成索引;`@Ignore`/`@Column`/`@Required`/`@MaxLength`/`@AutoIncrement` 均会反映到 DDL。
### 迁移 CLI(对齐 dotnet ef
`orm-cj` 附带迁移 CLI`simcu::orm.cli.MigrationCli`),把迁移落成 **.cj 文件** + **快照 JSON**,应用内嵌运行,无需安装外部工具。
**应用接线(一行接入)**`main()` 最上方先构造 `DbContext` 子类实例,`app orm <命令>` 时执行 CLI 并退出,否则走正常应用逻辑:
```cangjie
import simcu::orm.*
import simcu::orm.macros.*
import myapp.migrations.* // 注册文件所在子包(仅 import 触发注册)
@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/` 空目录**并放一个占位文件(如 `_placeholder.cj`,内容仅 `package <应用包名>.migrations`),否则应用侧那行 `import <应用包名>.migrations.*` 会编译失败;首次 `add` 后迁移类与注册文件会加进该目录;
- 迁移 / 注册文件生成在 `src/migrations/`,该目录是**子包**,文件头 `package <应用包名>.migrations`(如应用包 `myapp``package myapp.migrations`);
- `add` 只生成不执行;执行迁移用 `update`(回退用 `downgrade`),或在应用启动时直接 `db.migrate()`
- 快照持久化在 `src/migrations/snapshot.json``add` 用「旧快照 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``_id``id``id` 原样;可切换 `SnakeCase``userName``user_name``Keep` 原样);
- **主键**`@Key` 标注,或字段名 `_id` / `id`v1 仅支持**单主键**
- **自增**`@AutoIncrement`,或整型(Int8-64`id` 主键默认自增;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()`
```cangjie
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() }
```
**不用宏的等价手动写法**:继承 `DbContext``public prop xxx: DbSet<T> { get() { this.set<T>() } }`,补构造并 override `migrations()`
```cangjie
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/postgresql``postgres`,具体驱动由应用链接的驱动包注册) |
| `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 不同,同一实体重复入队会重复执行(如先 `add` 再 `update` = INSERT + UPDATE),请勿对同一实体的同一种操作重复调用。
### 4. 查询构建器 — QueryBuilder<T>(对齐 EF Core IQueryable 常用子集)
```cangjie
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`
- `MigrationBuilder``createTable` / `dropTable` / `addColumn` / `dropColumn` / `alterColumn` / `renameColumn` / `createIndex` / `dropIndex` / `rawSql`
- `ColumnDefinition` 链式:`.primary()` `.autoInc()` `.notNull()` `.withUnique()` `.withMaxLength(n)` `.withDefault(value)`
- `ColumnTypes`(定义于 `simcu::orm.sql`):`BigIntCol`openGauss 自增 → BIGSERIAL/ `IntCol`(自增 → SERIAL/ `SmallIntCol` / `TinyIntCol` / `TextCol`VARCHAR,默认 255/ `BoolCol` / `FloatCol`DOUBLE PRECISION/ `RealCol` / `DateTimeCol` / `DecimalCol`DECIMAL(18,6)/ `BinaryCol`(BYTEA);实际 DDL 映射由方言 `columnTypeSql` 决定;
- `Migrator``pending(migrations)` 预览未执行项;`migrate(migrations)` 事务内按 `migrationId` 字典序应用未执行项并写入历史表 `simcu_orm_migrations`
---
## 运行测试
```bash
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.sql``Datasource`/`Connection`/`Statement` |
| `stdx`CANGJIE_STDX_PATH | 标准扩展库(std.database.sql 接口) |
> orm-cj 包本身零依赖:连接层走 `std.database.sql` 标准接口,SQL/DDL 生成走 `ISqlDialect` 方言接口。新数据库接入 = 实现 `ISqlDialect`CRUD + 列类型映射 + 差异 DDL 语句)+ 提供对应 `std.database.sql` 驱动。
---
## 许可证
MIT