DbContext中只改写let 字段
This commit is contained in:
@@ -6,3 +6,6 @@ src/macros/*.dll
|
|||||||
src/tests/*.cj.macrocall
|
src/tests/*.cj.macrocall
|
||||||
# 临时/日志
|
# 临时/日志
|
||||||
*.log
|
*.log
|
||||||
|
.arts/
|
||||||
|
.cache/
|
||||||
|
.codegraph/
|
||||||
@@ -42,7 +42,7 @@ public class User {
|
|||||||
|
|
||||||
### 2. 定义 DbContext 并增删改查
|
### 2. 定义 DbContext 并增删改查
|
||||||
|
|
||||||
用 `@DbContext` 宏(推荐):纯声明类即可,宏自动补 `<: DbContext`、把每个 `DbSet<T>` 声明变成 `public prop` 并注入 `this.set<T>()`、生成 `(driverName, connStr)` 构造与 `migrations()` override:
|
用 `@DbContext` 宏(推荐):纯声明类即可,宏自动补 `<: DbContext`、把每个 **`let` 前缀**的 `DbSet<T>` 字段改写成 `public prop` 并注入 `this.set<T>()`(`var` / `prop` 前缀一律不改动,交给用户自行管理)、生成 `(driverName, connStr)` 构造与 `migrations()` override:
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import std.database.sql.*
|
import std.database.sql.*
|
||||||
@@ -52,8 +52,8 @@ import simcu::orm.macros.*
|
|||||||
|
|
||||||
@DbContext
|
@DbContext
|
||||||
public class AppDbContext {
|
public class AppDbContext {
|
||||||
prop users: DbSet<User>
|
let users: DbSet<User>
|
||||||
prop orders: DbSet<Order>
|
let orders: DbSet<Order>
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
@@ -190,8 +190,8 @@ import simcu::orm.macros.*
|
|||||||
|
|
||||||
@DbContext
|
@DbContext
|
||||||
public class MyDbContext {
|
public class MyDbContext {
|
||||||
prop users: DbSet<User>
|
let users: DbSet<User>
|
||||||
prop orders: DbSet<Order>
|
let orders: DbSet<Order>
|
||||||
}
|
}
|
||||||
|
|
||||||
main(args: Array<String>) {
|
main(args: Array<String>) {
|
||||||
@@ -324,7 +324,7 @@ orm-cj/
|
|||||||
|
|
||||||
### 3. DbContext / DbSet
|
### 3. DbContext / DbSet
|
||||||
|
|
||||||
**推荐:`@DbContext` 宏**。把 `DbSet` 声明为 `prop`,宏自动补继承/构造/注入/`migrations()`:
|
**推荐:`@DbContext` 宏**。把 `DbSet` 声明为 **`let` 前缀**(`let xxx: DbSet<T>`),宏只处理这种形式:改写成 `public prop xxx: DbSet<T> { get() { this.set<T>() } }`(public getter 保证反射可枚举);`var` / `prop` 前缀一律不改动。同时自动补继承/构造/`migrations()`:
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simcu::orm.*
|
import simcu::orm.*
|
||||||
@@ -334,8 +334,8 @@ import std.collection.* // migrations() 签名需要 ArrayList
|
|||||||
|
|
||||||
@DbContext
|
@DbContext
|
||||||
public class AppDbContext {
|
public class AppDbContext {
|
||||||
prop users: DbSet<User>
|
let users: DbSet<User>
|
||||||
prop orders: DbSet<Order>
|
let orders: DbSet<Order>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用法(宏生成 (driverName, connStr) 构造)
|
// 用法(宏生成 (driverName, connStr) 构造)
|
||||||
@@ -354,8 +354,12 @@ if (!db.databaseExists() || db.hasPendingMigrations()) { db.migrate() }
|
|||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
public class AppDbContext <: DbContext {
|
public class AppDbContext <: DbContext {
|
||||||
public let users: DbSet<User>
|
public prop users: DbSet<User> {
|
||||||
public let orders: DbSet<Order>
|
get() { this.set<User>() }
|
||||||
|
}
|
||||||
|
public prop orders: DbSet<Order> {
|
||||||
|
get() { this.set<Order>() }
|
||||||
|
}
|
||||||
|
|
||||||
public init(driverName: String, connStr: String) { super(driverName, connStr) }
|
public init(driverName: String, connStr: String) { super(driverName, connStr) }
|
||||||
public init(datasource: Datasource) { super(datasource) }
|
public init(datasource: Datasource) { super(datasource) }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ cjc-version = "1.1.3"
|
|||||||
name = "orm"
|
name = "orm"
|
||||||
organization = "simcu"
|
organization = "simcu"
|
||||||
description = "SimApi 数据访问层 ORM:数据模型映射 + 增删改查 + 数据库迁移;多方言架构,内置 openGauss/PostgreSQL 方言"
|
description = "SimApi 数据访问层 ORM:数据模型映射 + 增删改查 + 数据库迁移;多方言架构,内置 openGauss/PostgreSQL 方言"
|
||||||
version = "1.4.0"
|
version = "1.4.1"
|
||||||
target-dir = ""
|
target-dir = ""
|
||||||
output-type = "static"
|
output-type = "static"
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
* 用法(对齐 EF Core 声明式体验):
|
* 用法(对齐 EF Core 声明式体验):
|
||||||
* @DbContext
|
* @DbContext
|
||||||
* public class DataContext {
|
* public class DataContext {
|
||||||
* public var users: DbSet<User>
|
* let users: DbSet<User>
|
||||||
* public var bots: DbSet<Bot>
|
* let bots: DbSet<Bot>
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* 展开行为:
|
* 展开行为:
|
||||||
* - 自动补 `<: DbContext`(标注的类不能自带继承)
|
* - 自动补 `<: DbContext`(标注的类不能自带继承)
|
||||||
* - 每个 DbSet<T> 字段/无 getter prop -> `public prop name: DbSet<T> { get() { this.set<T>() } }`
|
* - 只处理「let 前缀」的 DbSet<T> 字段 -> `public prop name: DbSet<T> { get() { this.set<T>() } }`;
|
||||||
|
* var / prop 一律不改动(交给用户自行管理)
|
||||||
* - 自动生成 `init(driverName: String, connStr: String)` 调用 super
|
* - 自动生成 `init(driverName: String, connStr: String)` 调用 super
|
||||||
* - 自动生成 `migrations()` override,返回 orm-cj 全局注册表 allMigrations()
|
* - 自动生成 `migrations()` override,返回 orm-cj 全局注册表 allMigrations()
|
||||||
* (CLI 生成的 src/migrations/MigrationRegistry.cj 在模块加载时自动注册;
|
* (CLI 生成的 src/migrations/MigrationRegistry.cj 在模块加载时自动注册;
|
||||||
@@ -44,24 +45,21 @@ public macro DbContext(input: Tokens): Tokens {
|
|||||||
cd.superTypes.add(RefType(cangjieLex("DbContext")))
|
cd.superTypes.add(RefType(cangjieLex("DbContext")))
|
||||||
cd.upperBound = Token(TokenKind.UPPERBOUND)
|
cd.upperBound = Token(TokenKind.UPPERBOUND)
|
||||||
|
|
||||||
// 2. DbSet<T> 字段/无 getter prop -> public prop { get() { this.set<T>() } }
|
// 2. 只处理「let 前缀」的 DbSet<T> 字段 -> `public prop { get() { this.set<T>() } }`;
|
||||||
|
// var / prop 一律不改动(交给用户自行管理)
|
||||||
let newDecls = ArrayList<Decl>()
|
let newDecls = ArrayList<Decl>()
|
||||||
for (d in cd.body.decls) {
|
for (d in cd.body.decls) {
|
||||||
match (d) {
|
match (d) {
|
||||||
case vd: VarDecl =>
|
case vd: VarDecl =>
|
||||||
let typeName = vd.declType.toTokens().toString()
|
let typeName = vd.declType.toTokens().toString()
|
||||||
if (isDbSetType(typeName)) {
|
if (isDbSetType(typeName) && declIsLet(vd.toTokens().toString())) {
|
||||||
newDecls.add(buildSetProp(vd.identifier.value, typeName))
|
newDecls.add(buildSetProp(vd.identifier.value, typeName))
|
||||||
} else {
|
} else {
|
||||||
newDecls.add(d)
|
newDecls.add(d)
|
||||||
}
|
}
|
||||||
case pd: PropDecl =>
|
case pd: PropDecl =>
|
||||||
let typeName = pd.declType.toTokens().toString()
|
// prop 一律不改动
|
||||||
if (isDbSetType(typeName)) {
|
newDecls.add(d)
|
||||||
newDecls.add(buildSetProp(pd.identifier.value, typeName))
|
|
||||||
} else {
|
|
||||||
newDecls.add(d)
|
|
||||||
}
|
|
||||||
case _ => newDecls.add(d)
|
case _ => newDecls.add(d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +84,22 @@ private func isDbSetType(t: String): Bool {
|
|||||||
t.indexOf("DbSet") == Some(0) && t.indexOf("<").getOrThrow() > 0
|
t.indexOf("DbSet") == Some(0) && t.indexOf("<").getOrThrow() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 判断声明关键字是否为 let(保留字 let/var 只会独立出现在声明词位置,
|
||||||
|
/// 按词边界匹配避免误伤类型名/泛型实参里的子串)。
|
||||||
|
/// prop 是 PropDecl(非 VarDecl),根本不会走到这里。
|
||||||
|
private func declIsLet(t: String): Bool {
|
||||||
|
hasWord(t, "let") && !hasWord(t, "var")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判断字符串中 w 是否作为独立词出现(textified 输出用单个空格分隔词)
|
||||||
|
private func hasWord(s: String, w: String): Bool {
|
||||||
|
let padded = " " + s + " "
|
||||||
|
match (padded.indexOf(" " + w + " ")) {
|
||||||
|
case Some(_) => true
|
||||||
|
case None => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 生成 `public prop name: DbSet<T> { get() { this.set<T>() } }`
|
/// 生成 `public prop name: DbSet<T> { get() { this.set<T>() } }`
|
||||||
private func buildSetProp(name: String, typeName: String): Decl {
|
private func buildSetProp(name: String, typeName: String): Decl {
|
||||||
let inner = extractInnerType(typeName)
|
let inner = extractInnerType(typeName)
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* @DbContext 宏回归测试:只处理「let 前缀」的 DbSet<T> 字段,var/prop 一律不改动。
|
||||||
|
*
|
||||||
|
* 通过注册一个 FakeDriver,让宏生成的 `init(driverName, connStr)` 走通
|
||||||
|
* `super(...) -> DatasourceFactory.create(connStr, driverName)` 这条真实路径,
|
||||||
|
* 从而验证:let 字段被改写为 `public prop { get() { this.set<T>() } }`(public 可被
|
||||||
|
* std.reflect 枚举),getModels() 能反射采集到模型;手写 prop DbSet 保持原样不被改写。
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::orm.tests
|
||||||
|
|
||||||
|
import std.collection.*
|
||||||
|
import std.database.sql.*
|
||||||
|
import std.time.*
|
||||||
|
import std.unittest.*
|
||||||
|
import std.unittest.testmacro.*
|
||||||
|
import simcu::orm.*
|
||||||
|
import simcu::orm.macros.*
|
||||||
|
|
||||||
|
// ---------- 复用 sim_orm_test.cj 的实体 ----------
|
||||||
|
// User / Order 已在同包定义,这里直接引用。
|
||||||
|
|
||||||
|
// ---------- 假驱动 ----------
|
||||||
|
class FakeDriver <: Driver {
|
||||||
|
public prop name: String {
|
||||||
|
get() { "fake" }
|
||||||
|
}
|
||||||
|
public prop version: String {
|
||||||
|
get() { "0.0.1" }
|
||||||
|
}
|
||||||
|
public prop preferredPooling: Bool {
|
||||||
|
get() { false }
|
||||||
|
}
|
||||||
|
public func open(_: String, _: Array<(String, String)>): Datasource {
|
||||||
|
FakeDatasource()
|
||||||
|
}
|
||||||
|
public func getDriverStartTime(): DateTime {
|
||||||
|
DateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = {
|
||||||
|
=>
|
||||||
|
DriverManager.register("fake", FakeDriver())
|
||||||
|
}()
|
||||||
|
|
||||||
|
// ---------- 宏应用:仅含 let 字段 ----------
|
||||||
|
@DbContext
|
||||||
|
class MacroCtx {
|
||||||
|
let users: DbSet<User>
|
||||||
|
let orders: DbSet<Order>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 宏应用:let 与手写 prop 混用 ----------
|
||||||
|
// let 字段被宏改写为 public prop;手写 prop(含 getter)保持原样、不改写。
|
||||||
|
@DbContext
|
||||||
|
class MixedCtx {
|
||||||
|
let users: DbSet<User>
|
||||||
|
prop todos: DbSet<Order> {
|
||||||
|
get() { this.set<Order>() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public class DbContextMacroTests {
|
||||||
|
@TestCase
|
||||||
|
public func testMacroGeneratesInitAndDbSets(): Unit {
|
||||||
|
let ctx = MacroCtx("fake", "postgres://localhost/db")
|
||||||
|
// 宏生成的 init 已把 let 字段初始化为 set<T>(),可直接使用
|
||||||
|
let users = ctx.users
|
||||||
|
let orders = ctx.orders
|
||||||
|
@Expect(users is DbSet<User>, true)
|
||||||
|
@Expect(orders is DbSet<Order>, true)
|
||||||
|
// DbSet 连通到 context 的变更队列
|
||||||
|
users.add(User())
|
||||||
|
@Expect(ctx.pendingCount(), 1)
|
||||||
|
orders.add(Order())
|
||||||
|
@Expect(ctx.pendingCount(), 2)
|
||||||
|
// 默认方言为 openGauss(继承 PG)
|
||||||
|
@Expect(ctx.getDialect().quoteName("users"), "\"users\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
public func testMacroModelsCollectedFromLetFields(): Unit {
|
||||||
|
let ctx = MacroCtx("fake", "postgres://localhost/db")
|
||||||
|
// 宏把 let DbSet 字段统一提升为 public,getModels() 反射可采集到模型
|
||||||
|
let models = ctx.getModels()
|
||||||
|
@Expect(models.size, 2)
|
||||||
|
// 模型采集自 let 字段(users -> User, orders -> Order)
|
||||||
|
var gotUser = false
|
||||||
|
var gotOrder = false
|
||||||
|
for (m in models) {
|
||||||
|
if (m.tableName == "users") {
|
||||||
|
gotUser = true
|
||||||
|
}
|
||||||
|
if (m.tableName == "orders") {
|
||||||
|
gotOrder = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Expect(gotUser, true)
|
||||||
|
@Expect(gotOrder, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
public func testPropDbSetIsNotRewritten(): Unit {
|
||||||
|
let ctx = MixedCtx("fake", "postgres://localhost/db")
|
||||||
|
// let 字段被改写为 public prop,反射可采集 -> 1 个模型(users)
|
||||||
|
let models = ctx.getModels()
|
||||||
|
@Expect(models.size, 1)
|
||||||
|
// 手写 prop(internal,含 getter)保持原样:可正常使用,
|
||||||
|
// 但不会被宏改写成 public prop,因而不会重复进入模型采集
|
||||||
|
let todos = ctx.todos
|
||||||
|
@Expect(todos is DbSet<Order>, true)
|
||||||
|
todos.add(Order())
|
||||||
|
@Expect(ctx.pendingCount(), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user