AzerothCore 自定义 SQL 目录指南:data/sql/custom 的正确用法与可重放原则 📅 发布时间:2026/9/16 20:38:21 👁 浏览次数: AzerothCore 自定义 SQL 目录指南data/sql/custom 的正确用法与可重放原则【免费下载链接】azerothcore-wotlkComplete Open Source and Modular solution for MMO项目地址: https://gitcode.com/GitHub_Trending/az/azerothcore-wotlkAzerothCore 的data/sql/custom/目录专用于存放可重复执行的re-applicable自定义 SQL 脚本是服主长期维护数据库、叠加自定义改动而不破坏官方更新的关键机制。本文以 data/sql/custom/README.md 为骨架结合 AzerothCore 数据库更新器DBUpdater源码与updates_include表结构系统讲解该目录的设计意图、写入规范、执行机制与注意事项帮助读者写出安全、可重放、可维护的自定义 SQL。目录结构三大数据库的平行布局data/sql/custom/下与 AzerothCore 三库体系一一对应共三个子目录当前仓库中均以.dummy占位文件占位等待开发者放入真实脚本data/sql/custom/db_auth/认证库如账号、权限相关自定义data/sql/custom/db_characters/角色库如角色、公会、排行榜相关自定义data/sql/custom/db_world/世界库如生物、物品、任务、技能等玩法内容自定义data/sql/ ├── archive/db_* # 已归档的旧更新ARCHIVED ├── base/db_* # 基准数据 ├── custom/db_* # ← 自定义可重放 SQL 的唯一合法位置 ├── old/db_* # 历史旧数据 └── updates/db_* # 官方增量更新RELEASED / PENDING从数据库侧看三张基准表updates_include都预置了指向custom目录的注册行例如 data/sql/base/db_world/updates_include.sqlINSERT INTO updates_include VALUES ($/data/sql/archive/db_world,ARCHIVED), ($/data/sql/custom/db_world,CUSTOM), -- 本文主角 ($/data/sql/updates/db_world,RELEASED), ($/data/sql/updates/pending_db_world,PENDING);$前缀表示相对于服务器源码目录的路径state枚举值决定了该目录中脚本被数据库更新器如何处理。目录设计意图为何需要可重放SQLREADME 开宗明义This folder should contains only re-applicable sql——本目录只应存放可重复应用的 SQL。这与data/sql/updates/db_*中官方增量更新一次性、单向、有日期命名形成鲜明对照维度updates/db_worldcustom/db_world内容官方提交的增量变更服主/模块的自定义内容执行次数原则上只执行一次可反复执行且每次执行都必须安全幂等性不强求强制要求命名日期式如2026_01_01_00.sql无强制要求建议语义化命名失败影响单次失败会中断更新流程同样会中断但要求设计上尽量不失败为什么官方更新不能直接放进custom因为updates表会记录每个已应用文件的 SHA1 哈希官方更新文件若被改动哪怕一个空格更新器会检测到哈希变化并重新应用见下文源码分析而官方增量 SQL 大多不是幂等的重复执行会报错或产生脏数据。custom目录的存在就是给需要长期存在、可反复施加的自定义改动一个专门的、语义正确的家。可重放 SQL 的四种标准写法README 明确列举了四类推荐模式以下逐一展开并给出可直接复制的实战示例1.CREATE TABLE IF NOT EXISTS—— 建表先行创建自定义表如排行榜、自定义成就、捐献记录时必须使用IF NOT EXISTS-- 幂等建表重复执行不会报错 CREATE TABLE IF NOT EXISTS custom_boss_kills ( entry INT UNSIGNED NOT NULL, guild_id INT UNSIGNED NOT NULL, kill_count INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (entry, guild_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;若不加IF NOT EXISTS第二次执行会直接抛Table already exists错误整个更新流程中断。2.REPLACE INTO—— 整体覆盖式写入需要以当前脚本内容为准、无论库里原来是什么都覆盖时使用REPLACE INTO。它先删除冲突行再插入新行天然幂等-- 定义/修正某个自定义 NPC 的阵营与等级 REPLACE INTO creature_template (entry, faction, level_min, level_max, name) VALUES (900001, 35, 80, 80, Custom Guard);注意REPLACE INTO依赖唯一键/主键判定冲突且会触发删除-重建若目标表有外键引用需谨慎。3.DELETE INSERT—— 先清后写当自定义数据需要整组替换而非逐行覆盖时先按条件删除再插入完整集合-- 重写某 NPC 的全部售卖物品整组替换 DELETE FROM npc_vendor WHERE entry 900001; INSERT INTO npc_vendor (entry, item, maxcount, incrtime) VALUES (900001, 12345, 0, 0), (900001, 67890, 0, 0);删除条件必须足够精确本例锁定entry避免误删官方或其他自定义数据。4. 带固定值的UPDATE—— 定向修正用固定值修正某行数据且重复执行结果不变-- 把某任务奖励固定修正为指定物品 UPDATE quest_template SET RewardItem1 43214 WHERE ID 24579;这类UPDATE必须满足再执行一次结果完全一样的幂等条件——如果写成SET x x 1之类的相对变更第二次执行就会叠加绝不允许出现在本目录。反模式什么绝不能放进来INSERT INTO不带任何幂等保护重复执行会插入重复行UPDATE ... SET x x 1等增量运算重复执行会无限叠加依赖执行顺序的临时脚本如一次性数据迁移应放updates/走正常增量流程DROP TABLE破坏性操作会导致后续自定义脚本因缺表而失败。执行机制源码解析CUSTOM 状态如何被处理custom目录的脚本之所以可反复应用且内容可改根源于 AzerothCore 数据库更新器对updates_include的解析逻辑。核心实现在 src/server/database/Updater/UpdateFetcher.cpp。状态机定义更新器从updates_include表读取(path, state)对UpdateFetcher.cpp#L129-L155其中CUSTOM是与RELEASED、ARCHIVED、PENDING并列的状态。状态枚举与字符串转换定义在 src/server/database/Updater/UpdateFetcher.h#L74 附近CUSTOM, // 对应 updates_include.state CUSTOM两轮遍历官方更新在前自定义在后更新器将可用文件列表分成两批依次处理UpdateFetcher.cpp#L394-L406// 第一轮仅应用官方 RELEASED 更新跳过 PENDING/CUSTOM/MODULE for (auto const availableQuery : available) if (availableQuery.second ! PENDING availableQuery.second ! CUSTOM availableQuery.second ! MODULE) ApplyUpdateFile(availableQuery); // 第二轮应用 PENDING / CUSTOM / MODULE 更新 for (auto const availableQuery : available) if (availableQuery.second PENDING || availableQuery.second CUSTOM || availableQuery.second MODULE) ApplyUpdateFile(availableQuery);即官方更新先执行、自定义 SQL 后执行保证自定义脚本永远构建在最新的官方数据之上。哈希比对与重放逻辑每个待应用文件都会计算 SHA1 哈希UpdateFetcher.cpp#L297与updates表中记录的哈希比对决定执行动作UpdateFetcher.cpp#L277-L372文件未在updates表中 → 全新文件执行MODE_APPLY应用已在表中且哈希一致 → 跳过已应用已在表中但哈希不同 →检测到内容变更重新应用已在表中且哈希为空 →MODE_REHASH仅补写哈希不重跑。这正是可重放语义的落点一个custom脚本只要你保持幂等写法无论执行多少次都安全而你修改脚本内容后重启服务器更新器会自动识别哈希变化并重放新版本——无需手动清updates表记录。应用成功的文件会被写入updates表REPLACE INTO updates ...见 UpdateFetcher.cpp#L462-L469state记为CUSTOM。文件名唯一性约束更新器按文件名而非路径去重并排序UpdateFetcher.cpp#L92-L103若两个不同目录下出现同名.sql会触发LOG_FATAL并抛出UpdateException中止更新以保持排序确定性。因此custom/db_world/内的文件名必须全局唯一不能与其他目录如模块data/sql/中的文件名撞车。这也是updates/db_*采用日期式命名的原因之一。挂载与生效前提custom目录并非放进去就生效生效链路依赖两处注册缺一不可目录必须存在于源码树中更新器扫描时会检查路径是否存在不存在的 include 目录仅产生 WARN 并跳过UpdateFetcher.cpp#L144-L148不会报错但也不会执行任何脚本updates_include表中必须有对应行三张基准库的 updates_include.sql 已预置$/data/sql/custom/db_*行stateCUSTOM正常初始化数据库后即自动生效若曾手动删改该表需自行补回。此外目录会被递归扫描最大深度 10 层见 UpdateFetcher.cpp#L74-L84因此允许在custom/db_world/下按业务分子目录如custom/db_world/events/、custom/db_world/items/组织文件只要文件名全局唯一即可。与模块Modules自定义 SQL 的关系AzerothCore 模块同样可以携带自己的 SQL 目录modules/mod/data/sql/db_world/由更新器在_modulesList非空时以MODULE状态注册UpdateFetcher.cpp#L157-L187。二者对比如下对比项data/sql/custommodules/*/data/sql归属服务器本体自定义模块私有数据stateCUSTOMMODULE生命周期跟随主仓库跟随模块启用/移除幂等要求必须可重放同样建议幂等实践中随模块分发的脚本应放在模块目录卸载模块即清理数据而服务器运营层面的私有定制则应放在custom。值得注意的是更新器清理孤儿记录orphaned entries时会跳过MODULE状态UpdateFetcher.cpp#L414-L428避免误删模块已注册的历史。调试与排障查看已应用记录登录对应数据库执行SELECT * FROM updates WHERE state CUSTOM;可列出所有已应用的 custom 脚本及其哈希、执行耗时speed列强制重放修改后的脚本正常流程下直接修改custom内文件重启服务器或重跑数据库更新即可自动重放哈希变化触发若需绕过哈希机制强制重跑可手动删除updates表中对应行后重新更新脚本未生效按上述挂载前提检查目录路径与updates_include行是否齐备并查看日志中sql.updates相关输出文件名冲突报错日志出现Duplicate filename ... occurred时重命名冲突文件保证全局唯一UpdateFetcher.cpp#L92-L103。小结data/sql/custom/是 AzerothCore 为服主自定义内容预留的可重放 SQL 专属区域结构上与db_auth/db_characters/db_world三库一一对应内容上强制要求幂等写法CREATE TABLE IF NOT EXISTS、REPLACE INTO、DELETE INSERT、固定值UPDATE机制上由数据库更新器以CUSTOM状态在官方更新之后统一调度、以 SHA1 哈希驱动增量重放。遵循本文规范编写自定义 SQL即可在不干扰官方更新体系的前提下安全、可追溯、可长期维护地扩展服务器玩法数据。【免费下载链接】azerothcore-wotlkComplete Open Source and Modular solution for MMO项目地址: https://gitcode.com/GitHub_Trending/az/azerothcore-wotlk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考