
1. Overview
Liquibase is a powerful database management tool that provides an easy way to track and deploy database changes. It also allows to describe the behavior of changesets at the time of their deployment through what we call metadata.
Preconditions are a form of metadata commonly used to control the execution of a changelog or individual changesets. They ensure that certain requirements are met before continuing deployment.
In this tutorial, we’ll explore preconditions and learn to define conditional logic and error handling to optimize changeset control during their deployment.
2. Understanding Preconditions
Preconditions are an essential feature of Liquibase. They let us define specific conditions that must be satisfied before applying a changeset or changes. So, we can think of preconditions as guardians of database changes.
Before deploying a changeset, Liquibase systematically evaluates these conditions. If the conditions aren’t met, Liquibase prevents the changeset from being executed. Depending on the precondition behavior, it can throw failures or warnings or skip the changeset. This mechanism ensures that changes apply only when the database state meets the required criteria. In this way, we can ensure rigorous control of deployments while minimizing the risk of problems or errors.
Let’s explore a few practical scenarios where Liquibase preconditions can be useful:
- We can restrict changes to a specific database type, a group of database types, or even a particular version to ensure compatibility.
- We can check whether an object in the database already exists before creating or modifying it, thus avoiding unexpected errors.
- It also makes sense to check existing data before performing irreversible changes. For example, we can prevent the deletion of a table (dropTable) if it still contains data.
- Finally, we can validate the presence or absence of specific data before making any changes. For instance, if a migration involves inserting a new row into a configuration table, we can ensure this row doesn’t already exist, to avoid duplication or inconsistency.
These practices help us to secure our deployments and better anticipate potential problems.
3. Defining Preconditions Syntax
Preconditions are tags that we add directly to the changelog file to control the execution of changes. Let’s talk about their syntax.
3.1. Basic Syntax of Preconditions
Let’s start by using a simple example:
<preConditions>
<dbms type="mysql,oracle" />
</preConditions>
Here we use the dbms precondition. This ensures that the database is either MySQL or Oracle.
Liquibase offers several precondition tags that fit our needs. Here are some examples of preconditions:
- dbms checks the database type
- runningAs verifies the database username
- tableExists checks whether a table exists
- columnExists ensures the presence of a specific column
- foreignKeyExists checks whether a foreign key exists
- sqlCheck executes a SQL query and compares it with the expected result
We can define these preconditions in various changelog file formats such as XML, YAML, or JSON. Prior to Liquibase 4.27.0, only sqlCheck was compatible with the formatted SQL. As of version 4.27.0, other preconditions like tableExists and viewExists also support SQL changelogs:
--precondition-table-exists table:users
This is a valuable improvement for users who prefer to work with SQL changelogs.
We can always refer to the official documentation for a complete list of available preconditions and their attributes.
3.2. Nested Preconditions
Liquibase gives us the flexibility to nest or wrap multiple preconditions using logical tags like <and>, <or> and <not>. This makes it possible to create more complex conditions.
Let’s look at an example:
<preConditions>
<or>
<and>
<dbms type="oracle" />
<runningAs username="baeldung" />
</and>
<and>
<dbms type="mysql" />
<runningAs username="baeldung" />
</and>
</or>
</preConditions>
In this example, we check whether the database is Oracle and the database user is baeldung or whether the database is MySQL with the same user.
If no logical operator is specified, Liquibase applies AND logic by default. Let’s take another example:
<preConditions>
<tableExists tableName="users"/>
<columnExists tableName="users" columnName="email"/>
</preConditions>
In this case, both conditions must be met: the users table must exist and it must contain an email column.
Liquibase uses lazy evaluation for preconditions. This means that if the first condition in an <and> fails, the next one isn’t even checked. Similarly, if the first condition in an <or> fails, it skips the remaining conditions.
4. Global vs Local Preconditions
We can use preconditions at two levels: globally in the changelog, and locally in each changeset.
Global or changelog preconditions are defined at the top of the changelog, prior to any changesets. They apply to all changesets, and are useful for general checks, such as ensuring that the database is compatible:
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<preConditions>
<dbms type="mysql"/>
<runningAs username="baeldung"/>
<sqlCheck expectedResult="9.1.0">
SELECT @@version;
</sqlCheck>
</preConditions>
<changeSet id="BAEL-1000" author="baeldung">
<addColumn tableName="users">
<column name="country" type="varchar(25)"/>
</addColumn>
</changeSet>
<changeSet id="BAEL-1001" author="baeldung">
<addColumn tableName="tutorials">
<column name="code" type="varchar(5)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
In this example, we checked that the database is MySQL and that the database user deploying the changesets is baeldung. We also checked the database version is 9.1.0 using sqlCheck. Therefore, if any of these conditions aren’t met, none of the changesets will be executed.
Local preconditions, on the other hand, are defined at the top of a changeset and apply only to that specific changeset. They enable targeted checks, such as ensuring that a table or column exists before applying a change.
Let’s take an example:
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="BAEL-1000" author="baeldung">
<addColumn tableName="users">
<column name="country" type="varchar(25)"/>
</addColumn>
</changeSet>
<changeSet id="BAEL-1002" author="baeldung">
<preConditions>
<tableExists tableName="users"/>
<columnExists tableName="users" columnName="last_visit"/>
</preConditions>
<dropColumn tableName="users" columnName="last_visit"/>
</changeSet>
</databaseChangeLog>
This precondition only applies to the changeset with the id BAEL-1002. It ensures that the users table and the last_visit column exist before allowing the column to be dropped.
5. Handling Preconditions Failures and Errors
Liquibase distinguishes between a precondition failure and an error during its execution:
- A failure happens when a precondition runs successfully but doesn’t meet the expected criteria.
- An error occurs when technical issues, like a syntax error, prevent the precondition from running.
We use the onFail and onError attributes to specify how to handle failures or errors during precondition checks.
5.1. The onFail and onError Attributes
Let’s look at a changelog example with a changeset that includes a precondition:
<changeSet id="BAEL-1003" author="baeldung">
<preConditions onFail="HALT" onError="HALT">
<not>
<columnExists tableName="users" columnName="verified"/>
</not>
</preConditions>
<addColumn tableName="users">
<column name="verified" type="boolean" defaultValue="false"/>
</addColumn>
</changeSet>
We can run this changelog using the mvn liquibase:update command.
If the verified column already exists, the precondition fails with the following message:
Not precondition failed
For instance, if the users table doesn’t exist, an error occurs:
liquibase.exception.DatabaseException: Table 'baeldung_liquibase.users' doesn't exist
In both cases, the HALT value immediately stops execution when there’s a failure or error.
5.2. Values for onFail and onError Attributes
We can configure different behaviors for handling preconditions:
- HALT immediately stops the changelog execution. This is the default behavior.
- WARN logs a warning but continues execution
- MARK_RAN marks the changeSet as executed in DATABASECHANGELOG table and continues
- CONTINUE skips the changeSet and proceeds with execution
The table below summarizes the possible values:
onFail Attribute Values | onError Attribute Values | |
---|---|---|
Global Precondition | HALT, WARN | HALT, WARN |
Local Precondition | HALT, CONTINUE, MARK_RAN, WARN | HALT, CONTINUE, MARK_RAN, WARN |
Additional attributes, like onErrorMessage and onFailMessage, enable customized error or failure messages:
<preConditions onFail="WARN" onFailMessage="Column verified already exists">
<not>
<columnExists tableName="users" columnName="verified"/>
</not>
</preConditions>
These attributes allow flexibility and provide custom behavior when handling preconditions.
6. Conclusion
In this article, we explored how Liquibase preconditions improve control and precision during database updates. They ensure the database meets specific conditions before applying changes. This approach reduces risks and avoids potential errors.
By effectively utilizing preconditions, we can streamline deployments and maintain database integrity.