560 lines
18 KiB
Plaintext
560 lines
18 KiB
Plaintext
/*
|
||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||
* 数据库上下文:连接管理 + 变更提交 + 查询物化。
|
||
*
|
||
* 对齐 EF Core 的用法:
|
||
* class AppDbContext <: DbContext {
|
||
* public init(datasource: Datasource) { super(datasource) }
|
||
* }
|
||
* let db = AppDbContext(ds)
|
||
* db.set<User>().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<String>) {
|
||
* 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<String, Any>()
|
||
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<Migration> {
|
||
ArrayList()
|
||
}
|
||
|
||
/// CLI 入口:`<app> orm <命令>` 时执行迁移 CLI 并返回 true;否则返回 false(正常启动)。
|
||
/// 模型从本实例的 DbSet 属性反射收集,连接/驱动/方言来自构造参数。
|
||
public func cli(args: Array<String>): Bool {
|
||
if (args.size == 0 || args[0] != "orm") {
|
||
return false
|
||
}
|
||
let list = ArrayList<String>()
|
||
for (i in 1..args.size) {
|
||
list.add(args[i])
|
||
}
|
||
MigrationCli.runOn(this, list)
|
||
true
|
||
}
|
||
|
||
/// 反射收集本实例所有 DbSet<T> 的模型(DataContext 的 DbSet 字段/prop 均可)
|
||
public func getModels(): ArrayList<EntityModel> {
|
||
let result = ArrayList<EntityModel>()
|
||
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<Migration> {
|
||
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<Migration>): Int64 {
|
||
migrate(migrations)
|
||
}
|
||
|
||
/// 获取实体的 DbSet(同一实体类型复用同一个 DbSet 实例)
|
||
public func set<T>(): DbSet<T> {
|
||
let key = TypeInfo.of<T>().toString()
|
||
if (let Some(existing) <- _sets.get(key)) {
|
||
return (existing as DbSet<T>).getOrThrow()
|
||
}
|
||
let ds = DbSet<T>(this, ModelCache.get<T>())
|
||
_sets[key] = ds
|
||
ds
|
||
}
|
||
|
||
/// 待提交的变更条数
|
||
public func pendingCount(): Int64 {
|
||
tracker.count()
|
||
}
|
||
|
||
/// 当前方言(子类/应用可读取)
|
||
public func getDialect(): SqlDialect {
|
||
dialect
|
||
}
|
||
|
||
/// 应用迁移(对齐 EF Core Database.Migrate),返回本次应用的数量
|
||
public func migrate(migrations: ArrayList<Migration>): 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<Any>(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<T>(model: EntityModel, sql: String, params: ArrayList<Any>): ArrayList<T> {
|
||
let conn = datasource.connect()
|
||
try {
|
||
let stmt = conn.prepareStatement(sql)
|
||
try {
|
||
ParamBinder.bind(stmt, params)
|
||
let rs = stmt.query()
|
||
let list = ArrayList<T>()
|
||
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<Any>): 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<Int64>(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<String>()
|
||
let values = ArrayList<Any>()
|
||
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<String>()
|
||
let values = ArrayList<Any>()
|
||
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<Any>(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<T> <: 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<T> {
|
||
QueryBuilder<T>(_model, _context.getDialect(),
|
||
{ sql, params => _context.query<T>(_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<T> {
|
||
query().toList()
|
||
}
|
||
|
||
/// 全表计数
|
||
public func count(): Int64 {
|
||
query().count()
|
||
}
|
||
}
|