precondition失效时changeset默认不执行而抛异常中断流程;onfail="mark_ran"标记已执行,onfail="continue"跳过当前并继续,onerror="continue"才允许流程延续。

precondition 失效时 changeset 仍执行?检查 onFail 和 onError 行为
默认情况下,precondition 失败不会自动跳过 changeset,而是抛出异常并中断整个更新流程——但很多人误以为它会“静默跳过”。关键看你怎么配置失败策略。
-
onFail="MARK_RAN":预检失败,Liquibase 仍标记该changeset为已执行(不重试),适合“这个变更只在环境满足时才该运行,否则就当没这回事” -
onFail="CONTINUE":预检失败,继续执行后续changeset,当前被跳过(但数据库变更日志里不记录) -
onError="HALT"(默认):遇到任何错误(包括 precondition 报错)立即停止;换成onError="CONTINUE"才可能让流程走下去 - 注意:
onFail只对precondition生效,onError控制所有异常,二者要配合用
用 dbms precondition 判断数据库类型,别信 databaseChangeLog 的顶层配置
Liquibase 不会根据 databaseChangeLog 的 dbms 属性自动过滤 changeset;必须显式写 <precondition class="liquibase.preconditions.core.DbmsPrecondition"><dbms type="postgresql"></dbms></precondition>,否则 MySQL 环境跑 PostgreSQL 专属 SQL 会直接报错。
-
type值必须小写,且严格匹配 Liquibase 内置标识符:postgresql、mysql、oracle、sqlserver(不是mssql或sql-server) - 多个类型用逗号分隔:
<dbms type="postgresql,mysql"></dbms> - 别依赖 JDBC URL 推断——
jdbc:postgresql://...不等于dbms="postgresql"自动生效,precondition 必须存在且通过
sqlCheck 预检返回非 0 或 NULL 就算失败,但要注意空结果集
<sqlcheck expectedresult="0">SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'my_table'</sqlcheck> 这类写法很常见,但容易栽在“查不到表时返回空结果集”,而非 0 —— 这会导致 precondition 认为执行失败(因为没返回预期值),哪怕逻辑上你想表达“表不存在,可以建”。
- 安全写法是用聚合确保总有值:
SELECT COALESCE(COUNT(*), 0) FROM ... -
expectedResult是字符串比对,不是数值比较:expectedResult="1"不等于expectedResult=" 1 " - PostgreSQL 中
pg_tables比information_schema.tables更快,但跨库兼容性差,慎选
changeset 的 id + author + file path 三元组决定是否重复执行,和 precondition 无关
哪怕你改了 precondition 逻辑、删了旧条件、加了新判断,只要 id、author、文件路径没变,Liquibase 就认为这是同一个 changeset,不会重新执行——即使数据库状态已变。这是最常被忽略的“变更不生效”根源。
- 修改 precondition 后想强制重跑?要么改
id(推荐加版本后缀,如create_user_table_v2),要么用mvn liquibase:dropAll清库(仅开发) -
logicalFilePath属性可覆盖实际路径,用于合并多个 changelog 文件时避免冲突,但一旦设了就必须保持稳定 - 不要用时间戳或随机数生成
id,CI/CD 中会导致每次构建都认为是新 changeset,引发重复执行
precondition 不是魔法开关,它只控制单次执行流;真正决定“这个变更要不要进生产”的,是你怎么设计 id/author/路径组合,以及是否理解 onFail 和底层 SQL 返回值的语义边界。










