Files
server-core/app/training/db.py
T

202 lines
5.9 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""培训子应用数据层(同步 helper,经 asyncio.to_thread 调用)。
统一走平台 DATABASE_URLconfig.DATABASE_URL,可配 SQLite / MySQL),
由 infrastructure.db.make_sync_engine 映射同步驱动:
sqlite+aiosqlite → sqlitemysql+asyncmy → mysql+pymysql。
历史遗留:本层最初为原生 ``sqlite3`` 直连(``?`` 占位符),为不改动 20+ 处
调用点,``get_conn()`` 返回一个占位符兼容的连接 shim——SQL 用 ``?`` 书写,
执行前翻译为 SQLAlchemy 命名参数,对 SQLite / MySQL 双方言通用。
表结构统一由 alembic 迁移管理(本文件的 SCHEMA 常量仅存档,运行时不再建表)。
"""
import random
import threading
import time
from sqlalchemy import text
from ..infrastructure.db import make_sync_engine
BASE_DIR_KEY = "serverdata" # 兼容旧注释:数据目录统一由 config.SERVERDATA_DIR 承载
_engine = None
_engine_lock = threading.Lock()
def _get_engine():
global _engine
if _engine is None:
with _engine_lock:
if _engine is None:
_engine = make_sync_engine()
return _engine
def reset_engine() -> None:
"""测试/切库后重置共享引擎(下次使用时按当前 DATABASE_URL 重建)。"""
global _engine
with _engine_lock:
if _engine is not None:
_engine.dispose()
_engine = None
def gen_id(prefix):
ts = time.time()
s = int(ts)
# base36 大写,模拟 JS genId
def b36(n):
if n == 0:
return "0"
d = "0123456789abcdefghijklmnopqrstuvwxyz"
out = ""
while n:
out = d[n % 36] + out
n //= 36
return out
return prefix + b36(s).upper() + b36(random.randint(0, 35)).upper()
def now_iso():
from datetime import datetime, timezone
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
SCHEMA = """
-- (存档,运行时不再执行)历史 SQLite SCHEMA;表结构现由 alembic 迁移
-- 0003_training_tables 及后续版本管理,MySQL 下类型由 dialect 钩子映射。
"""
# ---------------- 占位符兼容连接 shim ----------------
def _translate(sql: str, params) -> tuple[str, dict]:
"""sqlite 风格 ``?`` 占位符 → SQLAlchemy 命名参数(:p0, :p1, ...)。
本层 SQL 均为受控字面量(无字符串内嵌 ``?``),直接顺序替换安全;
dict 参数原样透传。列表/元组包装为 ``(x,)`` 的写法均兼容。
"""
if isinstance(params, dict):
return sql, params
if params is None:
params = ()
if not isinstance(params, (list, tuple)):
params = (params,)
out, n = [], 0
for ch in sql:
if ch == "?":
out.append(f":p{n}")
n += 1
else:
out.append(ch)
if n != len(params):
raise ValueError(f"占位符数量不匹配: {n} 占位符 vs {len(params)} 参数 | {sql}")
return "".join(out), {f"p{i}": v for i, v in enumerate(params)}
class _Cursor:
"""轻量结果游标:fetchone/fetchall 返回 dict(兼容 row["col"] 与 dict(row))。"""
def __init__(self, result):
self._result = result
def _to_dict(self, row):
return None if row is None else dict(row._mapping)
def fetchone(self):
return self._to_dict(self._result.fetchone())
def fetchall(self):
return [dict(r._mapping) for r in self._result.fetchall()]
def __iter__(self):
return iter(self.fetchall())
class _Conn:
"""sqlite3.Connection 兼容 shimexecute/executemany/commit/close)。"""
def __init__(self, conn):
self._conn = conn
def execute(self, sql, params=()):
q, p = _translate(sql, params)
return _Cursor(self._conn.execute(text(q), p))
def executemany(self, sql, seq_of_params):
cur = None
for params in seq_of_params:
cur = self.execute(sql, params)
return cur
def commit(self):
self._conn.commit()
def rollback(self):
self._conn.rollback()
def close(self):
self._conn.close()
@property
def in_transaction(self):
return self._conn.in_transaction()
def get_conn():
"""取一条连接(shim)。用完必须 close()(归还连接池)。"""
return _Conn(_get_engine().connect())
# ---------------- 通用读写 helpers ----------------
def rows_to_list(rows):
return [dict(r) for r in rows]
def fetch_one(table, **kw):
where = " AND ".join([f"`{k}`=:p{i}" for i, k in enumerate(kw)])
sql = f"SELECT * FROM `{table}`"
if where:
sql += f" WHERE {where}"
with _get_engine().connect() as conn:
row = conn.execute(text(sql), {f"p{i}": v for i, v in enumerate(kw.values())}).fetchone()
return dict(row._mapping) if row else None
def fetch_by_id(table, rid):
return fetch_one(table, id=rid)
def list_all(table, order_by=None):
sql = f"SELECT * FROM `{table}`"
if order_by:
sql += f" ORDER BY {order_by}"
with _get_engine().connect() as conn:
return [dict(r._mapping) for r in conn.execute(text(sql)).fetchall()]
def insert(table, data):
keys = list(data.keys())
cols = ",".join(f"`{k}`" for k in keys)
marks = ",".join(f":p{i}" for i in range(len(keys)))
sql = f"INSERT INTO `{table}` ({cols}) VALUES ({marks})"
with _get_engine().begin() as conn:
conn.execute(text(sql), {f"p{i}": data[k] for i, k in enumerate(keys)})
def update_row(table, rid, patch):
keys = list(patch.keys())
sets = ",".join(f"`{k}`=:p{i}" for i, k in enumerate(keys))
sql = f"UPDATE `{table}` SET {sets} WHERE `id`=:pid"
params = {f"p{i}": patch[k] for i, k in enumerate(keys)}
params["pid"] = rid
with _get_engine().begin() as conn:
conn.execute(text(sql), params)
def delete_row(table, rid):
with _get_engine().begin() as conn:
conn.execute(text(f"DELETE FROM `{table}` WHERE `id`=:pid"), {"pid": rid})