Files
orm-cj/README.md
T

407 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.* // 生成首个迁移后手动取消注释(见下方「生成约定」)
@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`(如应用包 `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