/* * Copyright (c) 2025 SimcuTeam. All rights reserved. * 数据库上下文:连接管理 + 变更提交 + 查询物化。 * * 对齐 EF Core 的用法: * class AppDbContext <: DbContext { * public init(datasource: Datasource) { super(datasource) } * } * let db = AppDbContext(ds) * db.set().add(user) * db.saveChanges() * * 语义说明(v1,操作队列模式):add/update/remove 只入队, * saveChanges 开启事务按入队顺序逐条执行;执行后清空队列。 */ package simcu::orm.db import std.collection.* import std.database.sql.* import std.fs.* import std.reflect.* import simcu::orm.cli.{CliContext, MigrationCli} import simcu::orm.model.* import simcu::orm.query.* import simcu::orm.sql.* import simcu::orm.tracking.* import simcu::orm.migrations.{Migrator, Migration} /** * 数据库上下文。 * * 基于实例的 CLI 入口(对齐 EF Core): * main(args: Array) { * let db = DataContext("pgsql", "Host=..;Database=..;Username=..;Password=..") * if (db.cli(args)) { return } // `app orm <命令>` 时执行 CLI 并退出 * // 正常启动 * } * cli() 的模型来自实例的 DbSet 属性(反射收集);迁移列表来自子类 * override migrations();连接/驱动来自构造参数。 */ public open class DbContext <: CliContext { internal let datasource: Datasource internal let dialect: SqlDialect internal let tracker = ChangeTracker() private let _sets = HashMap() private var _driverName: String private var _connString: String public init(datasource: Datasource, dialect: SqlDialect) { this.datasource = datasource this.dialect = dialect this._driverName = "" this._connString = "" } public init(datasource: Datasource) { this(datasource, OpenGaussDialect()) } /// 便捷构造:由连接字符串创建 Datasource(驱动构建收敛在 DatasourceFactory) public init(connStr: String, dialect: SqlDialect) { this(DatasourceFactory.create(connStr), dialect) this._driverName = DatasourceFactory.detectDriver(connStr, DatasourceFactory.DEFAULT_DRIVER) this._connString = connStr } public init(connStr: String) { this(connStr, OpenGaussDialect()) } /// 便捷构造:显式指定驱动类型(如 "pgsql")+ 连接字符串 public init(driverName: String, connStr: String, dialect: SqlDialect) { this(DatasourceFactory.create(connStr, driverName), dialect) this._driverName = DatasourceFactory.normalizeDriverName(driverName) this._connString = connStr } public init(driverName: String, connStr: String) { this(driverName, connStr, OpenGaussDialect()) } /// 应用提供的迁移列表(子类 override;由 CLI 维护的注册文件提供实例)。 /// 默认空——add 生成迁移不依赖它,update/list/create 需要。 public open func migrations(): ArrayList { ArrayList() } /// CLI 入口:` orm <命令>` 时执行迁移 CLI 并返回 true;否则返回 false(正常启动)。 /// 模型从本实例的 DbSet 属性反射收集,连接/驱动/方言来自构造参数。 public func cli(args: Array): Bool { if (args.size == 0 || args[0] != "orm") { return false } let list = ArrayList() for (i in 1..args.size) { list.add(args[i]) } MigrationCli.runOn(this, list) true } /// 反射收集本实例所有 DbSet 的模型(DataContext 的 DbSet 字段/prop 均可) public func getModels(): ArrayList { let result = ArrayList() let ti = TypeInfo.of(this) // prop(getter 形式)与 var 字段形式都要支持 for (p in ti.instanceProperties) { if (p.typeInfo.name.startsWith("DbSet")) { let v = p.getValue(this) match (v as DbSetBase) { case Some(ds) => result.add(ds.getModel()) case None => () } } } for (v in ti.instanceVariables) { if (v.typeInfo.name.startsWith("DbSet")) { let value = v.getValue(this) match (value as DbSetBase) { case Some(ds) => result.add(ds.getModel()) case None => () } } } result } /// 驱动注册名(构造时归一化;Datasource 构造无连接串时为空) public func getDriverName(): String { _driverName } /// 原始连接字符串(Datasource 构造时为 "") public func getConnString(): String { _connString } /// CliContext:主数据源 public func getDatasource(): Datasource { datasource } /// CliContext:迁移列表(委托子类的 migrations()) public func getMigrations(): ArrayList { migrations() } /// CliContext:seed 运行上下文 = 本 DataContext 实例(seed 的 run(ctx) 用它操作 ORM) public func getSeedContext(): Any { this } /// 系统库数据源(数据库存在性检查用)。 /// 由连接串 + 驱动派生:Database=xxx 换成系统库名(pgsql → postgres,mysql → mysql)。 /// 无连接串或驱动不支持(sqlite 无系统库)时返回 None。 public func getAdminDatasource(): ?Datasource { if (_connString.isEmpty() || _driverName.isEmpty()) { return None } if (_driverName == "sqlite") { return None } let adminDb = if (_driverName == "mysql") { "mysql" } else { "postgres" } var result = "" for (seg in _connString.split(";")) { let t = seg.trimAscii() if (t.isEmpty()) { continue } if (t.toAsciiLower().startsWith("database=")) { result += "Database=${adminDb};" } else { result += t result += ";" } } Some(DatasourceFactory.create(result, _driverName)) } /// CliContext:应用所有迁移(委托 migrate) public func migrateAll(migrations: ArrayList): Int64 { migrate(migrations) } /// 获取实体的 DbSet(同一实体类型复用同一个 DbSet 实例) public func set(): DbSet { let key = TypeInfo.of().toString() if (let Some(existing) <- _sets.get(key)) { return (existing as DbSet).getOrThrow() } let ds = DbSet(this, ModelCache.get()) _sets[key] = ds ds } /// 待提交的变更条数 public func pendingCount(): Int64 { tracker.count() } /// 当前方言(子类/应用可读取) public func getDialect(): SqlDialect { dialect } /// 应用迁移(对齐 EF Core Database.Migrate),返回本次应用的数量 public func migrate(migrations: ArrayList): Int64 { Migrator(datasource, dialect).migrate(migrations) } /// 无参便捷版:应用子类 migrations() 提供的全部迁移(`db.migrate()` 即运行 update) public func migrate(): Int64 { migrate(migrations()) } /// 确认当前连接串指向的数据库是否存在(连系统库查询,不建库)。 /// 需带连接串构造(_connString 非空);sqlite 文件库返回文件是否存在。 public func databaseExists(): Bool { if (_connString.isEmpty()) { return false } if (_driverName == "sqlite") { return exists(Path(sqliteFilePath())) } let dbName = databaseNameFromConnString() if (dbName.isEmpty()) { return false } match (getAdminDatasource()) { case None => false case Some(admin) => let conn = admin.connect() try { let stmt = conn.prepareStatement(dialect.databaseExistsSql()) try { stmt.set(0, dbName) let rs = stmt.query() rs.next() } finally { stmt.close() } } finally { conn.close() } } } /// 是否存在未应用的迁移(库未创建或历史表不存在视为有待应用迁移) public func hasPendingMigrations(): Bool { let list = migrations() if (list.size == 0) { return false } try { let migrator = Migrator(datasource, dialect) let applied = migrator.appliedMigrationIds() for (m in list) { if (!applied.contains(m.migrationId)) { return true } } false } catch (_: Exception) { // 数据库/历史表不存在 → 有待应用迁移 true } } /// 提交全部待处理变更(事务内按入队顺序执行),返回影响的实体数 public func saveChanges(): Int64 { var count: Int64 = 0 if (tracker.count() == 0) { return 0 } let conn = datasource.connect() let tx = conn.createTransaction() tx.begin() try { for (entry in tracker.getEntries()) { match (entry.state) { case EntityState.Added => count += insertOne(conn, entry) case EntityState.Modified => count += updateOne(conn, entry) case EntityState.Deleted => count += deleteOne(conn, entry) case _ => () } } tx.commit() tracker.clear() count } catch (e: Exception) { try { tx.rollback() } catch (_) { () } throw e } finally { conn.close() } } internal func getTracker(): ChangeTracker { tracker } /// 执行查询并物化实体(query 层回调) internal func query(model: EntityModel, sql: String, params: ArrayList): ArrayList { let conn = datasource.connect() try { let stmt = conn.prepareStatement(sql) try { ParamBinder.bind(stmt, params) let rs = stmt.query() let list = ArrayList() while (rs.next()) { let instance = materialize(rs, model) list.add((instance as T).getOrThrow()) } list } finally { stmt.close() } } finally { conn.close() } } /// 执行 COUNT 查询(query 层回调) internal func count(sql: String, params: ArrayList): Int64 { let conn = datasource.connect() try { let stmt = conn.prepareStatement(sql) try { ParamBinder.bind(stmt, params) let rs = stmt.query() if (rs.next()) { return rs.getOrNull(0).getOrThrow() } 0 } finally { stmt.close() } } finally { conn.close() } } /// 结果集 → 实体实例(列顺序与 model.properties 一致) internal func materialize(rs: QueryResult, model: EntityModel): Any { let instance = model.createInstance() var i = 0 for (p in model.properties) { let v = ValueReader.read(rs, i, p.fullTypeName()) i += 1 if (let Some(val) <- v) { model.setValue(instance, p, val) } } instance } private func insertOne(conn: Connection, entry: EntityEntry): Int64 { let model = modelOf(entry.entity) let insertCols = ArrayList() let values = ArrayList() for (p in model.properties) { if (p.autoIncrement) { continue } var v = model.getValue(entry.entity, p) // 客户端生成主键(String):为空则生成唯一串 if (p.clientGenerated && isBlankString(v)) { v = GuidUtil.generate() model.setValue(entry.entity, p, v) } insertCols.add(p.columnName) values.add(v) } let kp = model.keyProperty.getOrThrow() let returning = kp.autoIncrement let sql = dialect.buildInsert(model.tableName, insertCols, kp.columnName, returning) let stmt = conn.prepareStatement(sql) try { ParamBinder.bind(stmt, values) if (returning) { let rs = stmt.query() if (rs.next()) { let v = ValueReader.read(rs, 0, kp.fullTypeName()) if (let Some(kv) <- v) { model.setKeyValue(entry.entity, kv) } } } else { stmt.update() } 1 } finally { stmt.close() } } private func updateOne(conn: Connection, entry: EntityEntry): Int64 { let model = modelOf(entry.entity) let setCols = ArrayList() let values = ArrayList() for (p in model.properties) { if (p.isKey) { continue } setCols.add(p.columnName) values.add(model.getValue(entry.entity, p)) } let kp = model.keyProperty.getOrThrow() values.add(model.getKeyValue(entry.entity)) let sql = dialect.buildUpdate(model.tableName, setCols, kp.columnName) let stmt = conn.prepareStatement(sql) try { ParamBinder.bind(stmt, values) stmt.update() 1 } finally { stmt.close() } } private func deleteOne(conn: Connection, entry: EntityEntry): Int64 { let model = modelOf(entry.entity) let kp = model.keyProperty.getOrThrow() let sql = dialect.buildDelete(model.tableName, kp.columnName) let stmt = conn.prepareStatement(sql) try { stmt.set(0, model.getKeyValue(entry.entity)) stmt.update() 1 } finally { stmt.close() } } private func modelOf(entity: Any): EntityModel { ModelCache.getFor(TypeInfo.of(entity)) } private static func isBlankString(v: Any): Bool { if (let s: String <- v) { return s.isEmpty() } false } /// 从连接串解析数据库名: /// URL 格式(postgres://user:pass@host:port/dbname?params)→ 路径段(去 query); /// ADO 风格(Host=..;Database=xxx;..)→ Database 值。 /// 解析不到返回 ""。 private func databaseNameFromConnString(): String { if (_connString.contains("://")) { let afterScheme = _connString.indexOf("://").getOrThrow() + 3 let rest = _connString[afterScheme..] if (let Some(slash) <- rest.indexOf("/")) { var path = rest[slash + 1..] if (let Some(q) <- path.indexOf("?")) { path = path[..q] } return path } return "" } for (seg in _connString.split(";")) { let t = seg.trimAscii() if (t.toAsciiLower().startsWith("database=")) { return t[t.indexOf("=").getOrThrow() + 1..] } } "" } /// sqlite 连接串 → 数据库文件路径(去 sqlite:// 前缀与 query 参数) private func sqliteFilePath(): String { var p = _connString if (p.startsWith("sqlite://")) { p = p[9..] } else if (p.startsWith("sqlite:")) { p = p[7..] } if (let Some(q) <- p.indexOf("?")) { p = p[..q] } p } } /** * DbSet 的非泛型访问基类:供 ORM CLI 通过反射拿到模型(EntityModel)。 */ public abstract class DbSetBase { public func getModel(): EntityModel } /** * 实体的数据集(对齐 EF Core DbSet)。 */ public class DbSet <: DbSetBase { private let _context: DbContext private let _model: EntityModel private let _tracker: ChangeTracker public init(context: DbContext, model: EntityModel) { this._context = context this._model = model this._tracker = context.getTracker() } public override func getModel(): EntityModel { _model } /// 入队新增 public func add(entity: T): Unit { _tracker.add(entity, EntityState.Added) } /// 入队修改(UPDATE 全部非主键列) public func update(entity: T): Unit { _tracker.add(entity, EntityState.Modified) } /// 入队删除 public func remove(entity: T): Unit { _tracker.add(entity, EntityState.Deleted) } /// 构建查询 public func query(): QueryBuilder { QueryBuilder(_model, _context.getDialect(), { sql, params => _context.query(_model, sql, params) }, { sql, params => _context.count(sql, params) }) } /// 按主键查询(无结果返回 None) public func find(key: Any): ?T { let kp = _model.keyProperty.getOrThrow() let list = query().filter(kp.columnName, "=", key).take(1).toList() if (list.size > 0) { return Some(list[0]) } None } /// 全表查询 public func toList(): ArrayList { query().toList() } /// 全表计数 public func count(): Int64 { query().count() } }