Skip to content

数据库 ​

签到插件要把积分落盘、封禁插件要记操作日志——只要你开始在插件里存数据,就会碰到建表、查询和事务。默认的 SQLite 零配置即可用;场景放大后再切 MySQL,上层接口不变。

ZCBOT 支持 SQLite(默认,零配置)与 MySQL,上层使用同一套接口, 插件基本不需要感知当前是哪种数据库。

选型边界:SQLite 仅适合小环境与开发环境(个人/小群、 单写多读、单文件存储);大环境不适合 SQLite——多群、高并发、多进程 部署、长连接保活、水平扩展等场景请使用 MySQL。方言适配只解决 SQL 能不能跑,解决不了 SQLite 的并发与扩展上限。

  • 数据库封装:framework/database/db.py 的 Database 类;
  • SQL 方言翻译:framework/database/dialect.py(纯函数);
  • 自动建表/迁移:framework/database/schema.py + framework/database/init_db.py 的 auto_init_database(db);
  • 调试模式引擎:framework/database/sql_sim.py 的 SqlSimEngine(无数据库时本地 SQL 模拟);
  • 配置见 配置系统。

存储降级兜底(数据库不可用) ​

数据库存储不再构成框架的硬依赖:初始化失败(缺 MySQL 驱动、连接失败、 SQLite 路径不可写等)时框架不会因此无法启动,而是自动降级为本地 SQL 模拟引擎 (SqlSimEngine,与 database.type: debug 同款实现,data/db/sql/ 下 JSON 行集落盘,查询有语义且重启不丢):

  • 降级兜底仅供"数据库没了框架还能起来",数据以本地 JSON 行集存储,性能低、无真实事务; 正常部署请配置 SQLite 或 MySQL;
  • 显式调试模式配置 database.type: debug 走同一套 SqlSimEngine(见下节), 适合无数据库的开发/联调环境;
  • 旧版 database.type: file(降级文件存储)已移除:该模式 SQL 调用一律返回空值、 无实际持久化能力,现已不可用;若配置中仍写 type: file,框架启动将明确报错提示改用 sqlite / mysql / debug;
  • 框架通过 framework.db_debug_mode(True 表示调试/降级兜底模式)与 framework.storage_mode(sqlite/mysql/debug)感知当前后端;连接池状态可用 ctx.db_pool_status 查看。

调试模式(低性能模式 / 本地模拟 SQL) ​

在没有 SQLite/MySQL 依赖的开发、联调环境(如 CI 冒烟、最小复现、 无数据库沙箱),可通过抽象层开启调试模式:database: { type: debug }。

  • 存储抽象层:framework/database/storage.py 的 create_storage(config) 按 database.type 选后端 —— sqlite/mysql → Database、 debug → SqlSimEngine;上层访问接口完全同构, 插件与内核无需改动;
  • 模拟引擎:framework/database/sql_sim.py 的 SqlSimEngine,在本机 用 JSON 行集文件"假扮"数据库(data/db/sql/<表>.json,原子写、重启不丢), 查询返回符合 SQL 语义的结果(非空值兜底):
    • SELECT:* / 列 / COUNT(*) / DISTINCT / DATE(col) AS d、 WHERE(= <> != > < >= <= AND OR IN NOT INIS NULL LIKE,支持 %s/? 参数与字面量)、GROUP BY、 ORDER BY、LIMIT / OFFSET;
    • INSERT [IGNORE]、UPDATE(NOW()、col=col+N 自增)、DELETE、CREATE TABLE;
    • 不支持的语法(JOIN/子查询/复杂表达式/ALTER)安全兜底:查询返回 []、 写返回 0,绝不抛异常,保证框架可启动;
  • 调试模式适合无数据库的开发/联调环境,可真实验证查询/写入逻辑; 生产或常规部署请使用 SQLite / MySQL(性能、事务、并发能力均更强)。 但它性能低、无真实事务与并发保证,仅限开发调试,生产必须用 sqlite/mysql;
  • 行集内存缓冲:行集常驻内存,读写不再逐次全量解析/重写 JSON 文件; 写入先进缓冲,按 database.debug_flush_ms(毫秒,默认 1000,0=逐次 同步落盘)由后台线程合并落盘,正常关停/进程退出兜底全量落盘(进程被强杀 最多丢最近一个刷盘间隔的写入);外部/手工改动 JSON 文件按 mtime 自动重载;
  • 框架感知:framework.storage_mode(sqlite/mysql/debug)与 framework.db_debug_mode(True 表示调试模式)暴露当前后端。
yaml
# config.yaml —— 调试模式(低性能模式:本地模拟 SQL)
database:
  type: debug         # 无需任何数据库依赖,行集落盘 data/db/sql/
  debug_flush_ms: 1000  # 内存缓冲刷盘间隔(毫秒),0 = 每次写同步落盘

自动初始化 ​

框架启动时根据 config.yaml → database 建立连接并自动建表:

  • SQLite:按 database.path(默认 data/zcbot.db)创建文件库;
  • MySQL:自动探测版本与字符集,适配 DDL 后建库建表。

插件不要自己去改框架表结构;自己的业务表用 ctx.create_table() 创建。

建表之后的字段/索引演进走版本化迁移(账本表 schema_migrations,只执行未应用过的版本,失败会记账并在下次启动重试):

  • 新增字段要三处同步:sql/init.sql + sql/init_mysql55.sql + MIGRATIONS 里追加一版;
  • 已发布的迁移不可改(账本有校验和,改了只会报漂移、不会重跑);
  • 详见 数据库迁移。

插件建表:统一写 MySQL 方言 ​

python
def register(ctx):
    ctx.create_table("""
        CREATE TABLE IF NOT EXISTS my_data (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL,
            score INTEGER DEFAULT 0,
            memo TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)

框架自动完成方言适配,插件无需判断数据库类型:

  • SQLite:AUTO_INCREMENT → AUTOINCREMENT、ENUM → TEXT、移除不支持的内联 INDEX 等;
  • MySQL:AUTOINCREMENT → AUTO_INCREMENT,对长文本列上的索引自动改成 前缀索引 col(191),规避 MySQL 的 1170/1064 错误。

建议在 register(ctx) 里建表(幂等,IF NOT EXISTS),每次注册都会确保表存在。

查询接口 ​

同步 ​

python
rows = ctx.db_query("SELECT * FROM users WHERE group_id=%s", (group_id,))     # list[dict]
row  = ctx.db_query_one("SELECT * FROM users WHERE user_id=%s", (user_id,))  # dict / None
n    = ctx.db_execute("UPDATE users SET score=score+%s WHERE id=%s", (1, id_))  # 受影响行数
new_id = ctx.db_insert("INSERT INTO logs (msg) VALUES (%s)", ("hello",))     # 自增 ID
ctx.db_execute_many("INSERT INTO t (v) VALUES (%s)", [(1,), (2,)])           # 批量

异步(async handler 推荐) ​

异步方法在数据库专用线程池执行,DB 繁忙也不会卡住消息事件循环:

python
rows = await ctx.db_query_async(sql, params)
row  = await ctx.db_query_one_async(sql, params)
n    = await ctx.db_execute_async(sql, params)
new_id = await ctx.db_insert_async(sql, params)
await ctx.db_execute_many_async(sql, params_list)

查询结果统一为 dict(单条)或 list[dict](多条),列名即键。

占位符:统一用 %s ​

无论 SQLite 还是 MySQL,插件 SQL 一律写 %s 占位符,框架在 SQLite 下 自动转换成 ?。不要拼接字符串,避免 SQL 注入与方言问题:

python
# 正确:参数化
ctx.db_execute("UPDATE t SET v=%s WHERE id=%s", (v, id_))
# 错误:字符串拼接
ctx.db_execute(f"UPDATE t SET v={v} WHERE id={id_}")

事务 ​

多条必须一起成功/失败的写操作,用 db_connection() 取连接手动提交/回滚。 连接池模式下 close() 是归还连接,不是断开:

python
conn = ctx.db_connection()
try:
    cur = conn.cursor()
    cur.execute("UPDATE account SET balance=balance-%s WHERE id=%s", (100, from_id))
    cur.execute("UPDATE account SET balance=balance+%s WHERE id=%s", (100, to_id))
    conn.commit()
except Exception:
    conn.rollback()
    raise
finally:
    cur.close()
    conn.close()

连接池状态 ​

python
ctx.db_pool_status     # dict:连接池占用/空闲等状态,便于排障

切换到 MySQL 的完整写法 ​

把 config.yaml 的 database 段改成下面这样(所有键与 type 平级):

yaml
database:
  type: mysql
  host: 127.0.0.1
  port: 3306
  user: zcbot
  password: "your-password"    # 数字密码请加引号
  database: zcbot              # 库名,需提前建好
  charset: utf8mb4
  pool_size: 10                # 连接池上限
  pool_wait_timeout: 30        # 池满时最多等多久(秒)

库和账号要先建好,框架只负责建表:

sql
CREATE DATABASE zcbot DEFAULT CHARSET utf8mb4;
CREATE USER 'zcbot'@'127.0.0.1' IDENTIFIED BY 'your-password';
GRANT ALL PRIVILEGES ON zcbot.* TO 'zcbot'@'127.0.0.1';
FLUSH PRIVILEGES;

三种常见写错,症状都是启动卡住而不是报配置错:

yaml
# ✗ 错 1:整段写成单行 flow mapping,粘贴到 database: 下面就成了嵌套
database:
  database: { type: mysql, host: 127.0.0.1, ... }

# ✗ 错 2:多嵌一层 mysql:
database:
  type: mysql
  mysql: { host: 127.0.0.1, ... }

# ✓ 对:平铺,与 type 同级
database:
  type: mysql
  host: 127.0.0.1

写错时框架会直接指出具体键名(如「database.host 应为字符串,实际是 dict」), 不会再把一次参数错误重试成 30 秒假死。

权限说明:迁移前的备份会尝试读 binlog 位点,需要 REPLICATION CLIENT (或 MySQL 8 的 BACKUP_ADMIN)权限。普通业务账号没有该权限时会打印 v2 执行前备份未完成 — 无法读取 binlog 位点 并继续执行迁移——请自行确认 已有实例级备份,或给账号补上该权限。

SQLite vs MySQL 对比 ​

特性SQLiteMySQL
配置零配置,单文件需 host/port/user/password/database
并发单写多读,适合轻量场景支持高并发
占位符插件写 %s,运行时转 ?原生 %s
自增主键INTEGER PRIMARY KEY AUTOINCREMENTINT ... AUTO_INCREMENT PRIMARY KEY
适合规模仅小环境与开发环境(个人/小群、本地调试)大环境(多群、高并发、多进程部署)

结论照抄即可:

  • 本地开发、个人号、小群试点 → database.type: sqlite(默认,零配置);
  • 生产上线、多群、消息量大、双进程/多 worker → database.type: mysql, 不要用 SQLite 顶大环境(写锁、单文件、无网络拓扑,扩容到头就是换库)。
  • 从 SQLite 迁到 MySQL:改 config.yaml 后重启,框架会按 sql/init.sql 自动建表;业务数据需自行导出导入(结构为 MySQL 方言,双方言通用)。

字段元信息系统 ​

框架为业务表维护字段描述等元信息,配套提供(通过 ctx/框架能力):

  • 描述字段、更新字段描述、列出全表字段描述;
  • 群级动态列等高级能力。

插件自定义表只要遵循统一建表入口,即可被这些能力识别。

实践建议 ​

  1. 表名加插件前缀(如 sign_records),避免不同插件撞表;
  2. async def handler 里一律用 *_async 接口;
  3. 建表 DDL 保持幂等(IF NOT EXISTS),在 register(ctx) 调用;
  4. 高频写库考虑批量接口 db_execute_many;
  5. 用户可改的配置走 _conf_schema.json + ctx.get_config,不要自己建配置表。

基于 MIT + Apache 2.0 双协议发布