# ProductionDataBaseSync_DataMacro Access → SQL Server 单向增量同步(数据宏驱动)。把各 Access 库的业务数据周期性同步到 SQL Server 镜像表,作为 Access → SQL Server 迁移期的过渡数据层。 设计详见 `docs/superpowers/specs/2026-07-14-access-datamacro-sync-design.md`。 ## 背景 生产数据实际承载在网络共享下的多个 Access `.accdb`(按车间/年份分库)。旧机制靠客户端前端 VBA 写变更日志,但 VBA 只在特定表单事件触发,批量改表、直接改表等路径会绕过 → 漏数据。 改用 Access **数据宏**(表级引擎触发器,任何写路径必触发):每张业务表挂 `After Insert/Update/Delete`,变更写入各库本地 `TableChangeLog`。本程序就是「读各库日志 → 增量同步到 SQL」的搬运器,理论上 100% 捕获、对客户端零侵入。 ## 架构 每个 Access 库通过数据宏把变更写入本地 `TableChangeLog`;同步服务周期性把这些变更搬运到 SQL Server 镜像表。单库一轮分三段: | 阶段 | 动作 | 说明 | | --- | --- | --- | | Capture | `SELECT` 读取 `TableChangeLog` 最旧的 N 条,I/U 按 `RecordID` 回读整行 | 只读 Access | | Apply | 调用 `dbo.usp_SyncApply` 写入 SQL 镜像表 | 按 `ID` 精确落库,保序「最后操作胜」 | | Cleanup | `DELETE` 已应用的 `TableChangeLog` 行 | 按 `ID` 列表精确删除,遇锁自动退避重试 | 关键设计点: - **无水位线表**——Access 日志「应用成功即删」,日志本身就是待处理队列;`dbo.SyncQueue` 的唯一索引 `(SourceFile, SourceTable, SourceLogID)` 兜底去重,重复捕获幂等。 - **保序「最后操作胜」**——同一 `RecordID` 多次操作(先 Insert 后 Delete 等)按日志顺序取最后一条,保证最终态与 Access 一致。 - **每表一个事务**——单表失败只回滚该表,失败行标 `error` 重试,超限标 `dead` 待人工。 - **`SyncQueue` 长期保留**作审计/重试日志;`applied` 行清理后标 `cleaned`,超保留期再 purge,控制表增长。 ## 环境要求 - **Python 3.10+**(实测 3.13)。代码用 `X | None` 等新语法。 - **ODBC 驱动**(系统级,非 pip 安装,需预先装好): - `Microsoft Access Driver (*.accdb, *.mdb)`(ACE Redist 2016) - `ODBC Driver 17 for SQL Server` - **SQL Server ≥ 2017**(存储过程用 `STRING_AGG ... WITHIN GROUP`)。 - 执行账号需对目标表有 `ALTER` 权限(`SET IDENTITY_INSERT` 要求)。 ## 安装 ```bash python -m venv .venv .venv/Scripts/python.exe -m pip install --upgrade pip .venv/Scripts/python.exe -m pip install -r requirements.txt ``` 依赖(`requirements.txt`):`pyodbc`、`PyYAML`、`pydantic`、`pytest`。 ## SQL 端部署 首次需在目标库建好暂存表与 apply 存储过程(两个脚本都幂等,可重复执行): ```bash sqlcmd -S ,1433 -U -P -d -C -N o -i sql/01_sync_queue.sql sqlcmd -S ,1433 -U -P -d -C -N o -i sql/02_sync_apply.sql ``` - `sql/01_sync_queue.sql`:建 `dbo.SyncQueue` + 去重/清理索引 + `CleanedAt` 列。 - `sql/02_sync_apply.sql`:`dbo.usp_SyncApply` 集合化 apply 存储过程。 > 连接串/凭据以 `config.yaml` 为准,README 不硬编码。 ## 配置 编辑 `config.yaml`(从 `config.example.yaml` 复制并填入真实凭据;该文件 gitignored)。关键段: - **`sql_server`**:`conn_str`(ODBC 连接串)与 `sync_queue_table`(默认 `dbo.SyncQueue`)。 - **`access`**:`driver`(ACE 驱动名)与 `roots`(年份→根目录映射,如 `2026: "\\\\srv\\生产进度表\\2026年数据"`)。 - **`runtime`**:`poll_interval_seconds`(轮询间隔)、`capture_batch_size`/`apply_batch_size`/`cleanup_batch_size`(各段批大小)、`max_retries`/`retry_backoff_seconds`(重试)、`cleanup_lock_retries`(Access 锁重试次数)、`cleaned_retention_hours`(`cleaned` 行保留多久后 purge)。 - **`files`**:每个 Access 文件一条映射: - `file` / `root`(对应 `access.roots` 的 key)/ `schema`(SQL 目标 schema)。 - `year_suffix`:拼到表名后(`2026年数据` 用 `_YEAR2026`,`2025年数据`/合同表用 `""`)。 - `exclude_tables` / `include_tables`:排除/包含规则,**exclude 优先于 include**。`TableChangeLog` 必须排除。 ## 命令行 统一入口 `main.py`(仓库根目录),三个功能块都用它调用。配置固定读取同目录的 `config.yaml`,不在命令中指定: ```bash .venv/Scripts/python.exe main.py fullsync [--db FILE] [--table NAME] [--clear-change-log] .venv/Scripts/python.exe main.py incremental [--loop] [--poll-interval N] .venv/Scripts/python.exe main.py compare [--granularity count|ids] [--db FILE] [--table NAME] [--report PATH] ``` | 子命令 | 说明 | 退出码 | | --- | --- | --- | | `fullsync` | 一次性全量同步:TRUNCATE + 批量 INSERT,绕开增量队列。 | 0 | | `incremental` | 增量同步一轮(capture→apply→cleanup)。`--loop` 切持续轮询(服务模式)。 | 0 | | `compare` | 数据一致性核对:默认行数总量,`--granularity ids` 精确到 ID 集合差异。 | 全一致 0 / 有不一致 1 | > 三个块共用同一份 `config.yaml`,目标表集合完全一致(由 `sync.targets` 统一解析)。`main.py` 在根目录、自行把 `src/` 加入 `sys.path`,无需 `-m`、无需设环境变量;控制台强制 UTF-8,中文表名不乱码。 > > 旧入口 `-m sync.service` / `-m sync.fullsync` 保留为兼容,行为不变。 ## 全量同步 用于从零重建镜像表或修复 Access 与 SQL 之间的漂移。会**清空目标表再全量写入**,绕开增量队列: ```bash .venv/Scripts/python.exe main.py fullsync # 全部库、全部表 .venv/Scripts/python.exe main.py fullsync --db OEM.accdb # 仅单个库 .venv/Scripts/python.exe main.py fullsync --table 表壳焊接记录 # 仅单表(作用于所有库) .venv/Scripts/python.exe main.py fullsync --clear-change-log # 同步后同时清空 TableChangeLog(谨慎) ``` - `year_suffix` 通过 `FileMapping` 拼到表名后(如 `表壳焊接记录` → `表壳焊接记录_YEAR2026`)。 - 写入时 `SET IDENTITY_INSERT ON`,保留 Access 原 ID,保证后续增量的 `RecordID` 匹配不错位。 - 无镜像表的目标表按设计跳过(`target table missing`),不报错。 ## 增量同步 以 Access 数据宏日志为唯一变更源,每轮跑一遍 capture → apply → cleanup(三段说明见上面「架构」)。这是**主用模式**,生产上常驻运行。两种调用方式: ```bash .venv/Scripts/python.exe main.py incremental # 跑一轮就退出(手动/按需补跑) .venv/Scripts/python.exe main.py incremental --loop # 持续轮询(服务模式,不退出) .venv/Scripts/python.exe main.py incremental --loop --poll-interval 30 # 覆盖 runtime.poll_interval_seconds ``` - 生产环境以 nssm 服务 `DataMacroSync` 常驻(即 `--loop` 模式),见下文「NSSM 服务」;手动单轮适合验证或临时补跑积压。 - `--loop` 持续轮询直到进程被停(`nssm stop` 或 Ctrl+C);默认单轮跑完即退出。 - 单轮一次最多处理每库 `capture_batch_size` 条日志;积压多时连续跑几轮或用 `--loop` 直到清空。 - 每个文件的 capture/cleanup 独立隔离,单文件失败不影响其它;apply 失败不阻塞 cleanup;失败行 `error` 下轮重试、超 `max_retries` 标 `dead` 待人工。 - 幂等:`SyncQueue` 唯一索引去重,重复 capture、中断续跑都不会重写或漏写。 - 与全量同步共用同一份 `config.yaml` 和 `sync.targets`,目标表集合完全一致;全量是「从零重建」的补充手段,不替代增量。 ## 数据对比 核对 Access 源表与 SQL 镜像表是否一致。两种粒度: - **行数总量**(默认):逐表比对 `COUNT(*)`。 - **ID 集合**(`--granularity ids`):逐表比对两边 `ID` 集合,报告「Access 有 / SQL 无」与「SQL 有 / Access 无」的 ID(每表前 50 个 + 总数)。 ```bash .venv/Scripts/python.exe main.py compare # 全部库、全部表,行数总量 .venv/Scripts/python.exe main.py compare --db 氩弧焊.accdb # 仅单个库 .venv/Scripts/python.exe main.py compare --granularity ids --table 表壳焊接记录 # 单表 ID 级 .venv/Scripts/python.exe main.py compare --report report.txt # 同时写入报告文件(UTF-8) ``` - 无镜像表按设计跳过(`[SKIPPED no mirror]`),不计为不一致——这类表多半是该排除却没排除(如 `*_停` 停用表、`USysApplicationLog`),可作为配置清理的线索。 - 任一表不一致时退出码 `1`(便于脚本化);全部一致为 `0`。 - 实时增量同步存在秒级延迟窗口,刚写入 Access 的行可能尚未到 SQL,属正常(稍后再核或对照 `SyncQueue` 的 pending 行)。 ## NSSM 服务(114) 增量同步在 host 114 上以 nssm 服务 `DataMacroSync` 常驻运行。常用操作(经 `ssh 114`): ```bash ssh 114 "nssm status DataMacroSync" # 查状态(SERVICE_RUNNING / SERVICE_STOPPED) ssh 114 "nssm stop DataMacroSync" # 停 ssh 114 "nssm start DataMacroSync" # 起 ssh 114 "nssm restart DataMacroSync" # 重启 ssh 114 "nssm list" # 列出所有 nssm 服务 ``` ## 测试 ```bash .venv/Scripts/python.exe -m pytest # 仅单元测试(默认) RUN_INTEGRATION=1 .venv/Scripts/python.exe -m pytest # 含集成测试(需能连真实 Access + SQL Server) ``` - 单元测试用 mock,不依赖数据库;集成测试(`@pytest.mark.integration`)连 `config.yaml` 里的真实库,且自带清理。 - `pyproject.toml` 仅用于配置 pytest(`pythonpath = ["src", "."]`、`testpaths`、`integration` 标记)。 ## 项目结构 ``` main.py 统一命令行入口(fullsync / incremental / compare) config.yaml 真实配置(gitignored);config.example.yaml 是模板 requirements.txt 依赖 pyproject.toml pytest 配置 sql/ 01_sync_queue.sql dbo.SyncQueue 建表 + 索引(幂等) 02_sync_apply.sql dbo.usp_SyncApply 存储过程 src/sync/ config.py Pydantic 配置模型 + load_config targets.py 共享目标表解析(exclude/include,全量/增量/对比共用) serialize.py Access 值 → JSON 可序列化 access_reader.py 读 Access(日志/整行/计数/ID/删除日志) sql_writer.py 写 SQL(SyncQueue/apply/计数/ID/全量灌表) capture.py 增量编排:读日志→回读整行→入队 cleanup.py 清理编排:回删已应用日志 service.py 主循环 cycle() / run() fullsync.py 一次性全量同步 compare.py 数据一致性对比(count / ids) logging_setup.py 日志配置(滚动文件 + 控制台) tests/ 单元 + 集成测试 docs/superpowers/ 设计文档与实现计划 ``` ## 常见问题 - **cleanup 报 `-1102 无法更新;当前被锁定`**:Access 是文件型数据库,cleanup 反写 `DELETE` 与生产客户端数据宏写日志争用页级锁。服务已对锁冲突自动退避重试(`access_reader.delete_log_ids` 捕获 `pyodbc.Error` 并判断 `-1102`/「被锁定」)。偶发属正常,持续刷错再排查。 - **`No module named 'pydantic_core'` / `pyodbc`**:venv 解释器与轮子 ABI 不匹配(常见于 Python 3.13 装到 cp310 轮子)。修复:`.venv/Scripts/python.exe -m pip install --force-reinstall --no-cache-dir pyodbc pydantic`。 - **compare/fullsync 报 `[SKIPPED no mirror]` / `target table missing`**:该表在 Access 里但 SQL 端没有镜像表(多为 `*_停` 停用表、`USysApplicationLog` 等系统表,或尚未建镜像的新表)。若是该停用的表,加进对应 `exclude_tables`;若该同步,先在 SQL 建表再 fullsync。 - **`SyncQueue` 出现 `error`/`dead` 行**:`error` 会在下轮自动重试(未超 `max_retries`);`dead` 是超限放弃,需人工看 `ErrorMsg` 排查后处理。 - **`UserWarning: Field name "schema" ... shadows ... BaseModel`**:`FileMapping.schema` 字段名与 Pydantic 基类属性重名,仅告警、不影响功能。 - **控制台中文乱码**:`main.py` 已强制 stdout/stderr 为 UTF-8;若仍乱码,设环境变量 `PYTHONIOENCODING=utf-8`,或用 `compare --report` 输出 UTF-8 文件。