Liquibase Preconditions Example
Liquibase is a popular open-source database schema change management tool that allows developers to track, manage, and deploy database changes in a controlled and versioned manner. One of its powerful features is Preconditions, which help ensure that certain conditions are met before a changeSet is executed. Let us delve into understanding Liquibase preconditions.
1. What is Liquibase?
Liquibase enables teams to manage database changes efficiently. It provides features like version control, rollback capabilities, and cross-database compatibility. It is widely used in DevOps and CI/CD pipelines to automate schema management.
1.1 Key Features of Liquibase
Liquibase is a powerful database version control tool that helps manage and automate database changes efficiently. It provides several key features that make it a preferred choice for developers and database administrators:
- Database Agnostic: Works with multiple databases like MySQL, PostgreSQL, Oracle, and SQL Server.
- Rollback Support: Allows reverting database changes.
- Change Tracking: Uses a changelog file to track modifications.
- Flexible Formats: Supports XML, YAML, JSON, and SQL changelog formats.
- Extensibility: Supports custom extensions and integrations with CI/CD tools.
2. Guide to Understanding Liquibase Preconditions
Preconditions act as validation rules that must be met before a changeSet is executed. They help enforce constraints, validate schema states, and prevent execution errors.
2.1 Why Use Preconditions?
Liquibase preconditions play a crucial role in maintaining database integrity by enforcing rules before executing changeSets. They help prevent unintended modifications, ensure the database is in the correct state, and minimize errors in production environments. By using preconditions, developers can apply schema changes conditionally, reducing the risk of failures and ensuring compatibility with different database types or user roles. Below are some key benefits of using preconditions in Liquibase:
- Preventing duplicate table or column creation.
- Ensuring a database is in a valid state before applying changes.
- Avoiding errors in production by verifying system conditions.
- Applying changes conditionally based on database type or user role.
2.2 Preconditions Syntax
Liquibase preconditions are defined within the <preConditions>
tag and can be applied at a global or local level.
1 2 3 4 5 | < preConditions onFail = "HALT" > < dbms type = "mysql" /> < runningAs username = "admin" /> < tableExists tableName = "employees" /> </ preConditions > |
The given Liquibase preconditions ensure that certain conditions are met before executing a database change. The <preConditions>
tag, with onFail="HALT"
, specifies that if any condition fails, the execution should stop immediately. Inside it, <dbms type="mysql" />
ensures the script runs only on a MySQL database, <runningAs username="admin" />
checks that the script is executed by the “admin” user, and <tableExists tableName="employees" />
verifies the existence of the “employees” table before proceeding with the changes.
3. Comparing Global and Local Liquibase Preconditions
3.1 Global Preconditions
Global preconditions are defined at the beginning of the changelog and apply to all changeSets within it. In the example below, the <preConditions>
block is placed inside <databaseChangeLog>
with onFail="MARK_RAN"
, meaning that if the specified condition fails, Liquibase will mark the changeSet as executed but will not run it. The condition checks if the “customers” table does not exist using <not><tableExists tableName="customers"/></not>
. If the table is absent, the subsequent <changeSet>
creates it with an “id” column as the primary key and a “name” column. Since global preconditions affect all changeSets, they serve as a safeguard to ensure database constraints before any modifications are executed.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | < databaseChangeLog > < preConditions onFail = "MARK_RAN" > < not > < tableExists tableName = "customers" /> </ not > </ preConditions > < changeSet id = "1" author = "yatin" > < createTable tableName = "customers" > < column name = "id" type = "int" autoIncrement = "true" primaryKey = "true" /> < column name = "name" type = "varchar(255)" /> </ createTable > </ changeSet > </ databaseChangeLog > |
3.2 Local Preconditions
Local preconditions are defined within individual changeSets, applying only to that specific change. In the example below, the <preConditions>
block is inside a <changeSet>
, ensuring that the precondition is evaluated before executing that particular change. The condition checks if the “customers” table has a column named “email” using <columnExists tableName="customers" columnName="email"/>
. The onFail="HALT"
attribute ensures that if this condition is not met, Liquibase will stop execution immediately, preventing unintended changes. If the condition passes, a new “email” column is added to the “customers” table. Local preconditions are useful for verifying specific database states before applying incremental updates.
1 2 3 4 5 6 7 8 9 | < changeSet id = "2" author = "yatin" > < preConditions onFail = "HALT" > < columnExists tableName = "customers" columnName = "email" /> </ preConditions > < addColumn tableName = "customers" > < column name = "email" type = "varchar(255)" /> </ addColumn > </ changeSet > |
4. Managing Liquibase Preconditions Failures and Errors
4.1 Failure Handling Strategies
Liquibase preconditions come with different onFail
strategies that determine how execution should proceed when a condition is not met. These strategies help control the behavior of database migrations, ensuring that failures are handled appropriately based on the project’s requirements. Below are the different onFail
options with explanations and examples:
HALT
: Stops execution if a condition fails (default).123<
preConditions
onFail
=
"HALT"
>
<
tableExists
tableName
=
"employees"
/>
</
preConditions
>
If the “employees” table is missing, Liquibase will immediately halt execution to prevent further operations.
WARN
: Logs a warning and continues execution. E.g.: If the “users” table does not exist, Liquibase will log a warning but proceed with the changes.123<
preConditions
onFail
=
"WARN"
>
<
tableExists
tableName
=
"users"
/>
</
preConditions
>
Instead of stopping execution, Liquibase will only issue a warning, allowing subsequent changes to execute.
MARK_RAN
: Marks the changeSet as executed without running it. Fog e.g.: If the “customers” table is missing, Liquibase will mark the changeSet as executed without making any actual changes.123<
preConditions
onFail
=
"MARK_RAN"
>
<
tableExists
tableName
=
"customers"
/>
</
preConditions
>
This is useful when the change is optional, and you want to ensure it’s recorded as executed without applying modifications.
CONTINUE
: Ignores the failure and proceeds. E.g.: If the “orders” table does not exist, Liquibase will simply move on to the next changeset.123<
preConditions
onFail
=
"CONTINUE"
>
<
tableExists
tableName
=
"orders"
/>
</
preConditions
>
This setting allows Liquibase to continue execution without logging warnings or marking the changeSet in any special way.
4.2 Using Nested Preconditions
Nested preconditions in Liquibase allow combining multiple conditions using logical operators like <and>
, <or>
, and <not>
. This ensures that multiple checks are evaluated together before executing a changeSet. By using nested preconditions, you can define complex rules that must be met before Liquibase applies database modifications.
In the example below, the <preConditions>
block contains an <and>
condition, which ensures that both conditions inside it must be true for Liquibase to proceed.
1 2 3 4 5 6 | < preConditions > < and > < dbms type = "postgresql" /> < tableExists tableName = "orders" /> </ and > </ preConditions > |
4.2.1 Code Explanation
The given Liquibase preconditions use an <and>
condition to ensure that multiple checks are met before executing a changeSet. The <dbms type="postgresql"/>
condition restricts execution to only PostgreSQL databases, while <tableExists tableName="orders"/>
verifies that the “orders” table is present. Since both conditions are nested within <and>
, Liquibase will only proceed if both are true; otherwise, it will halt or handle the failure based on the defined onFail
strategy. This approach ensures that database changes are applied only in the appropriate environment, preventing errors due to missing tables or incompatible database types.
This approach is useful in scenarios where database updates should only be applied under specific conditions, such as ensuring compatibility with the database type or verifying the presence of required tables before making schema changes.
5. Best Practices for Using Preconditions
Liquibase preconditions help ensure database integrity by validating specific conditions before executing changeSets. They prevent errors, enforce constraints, and ensure that schema changes are applied only when predefined conditions are met. By strategically using preconditions, developers can create robust database migration scripts that minimize deployment risks. Below are some best practices for using preconditions effectively:
- Use preconditions to validate critical database constraints.
- Apply global preconditions for overarching rules and local preconditions for specific changeSets.
- Leverage logical operators like
and
,or
, andnot
for complex conditions. - Use
onFail="WARN"
for non-critical checks to prevent unnecessary failures. - Regularly test preconditions in staging environments before applying them in production.
6. Conclusion
Liquibase preconditions provide a robust way to enforce database constraints and prevent unintended changes. By using preconditions effectively, developers can ensure that database migrations are executed safely and predictably.