Files
orm-cj/src/db/db_context.cj
T

560 lines
18 KiB
Plaintext
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.
/*
* 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()
}
}