I have an existing spring boot project and a database for the same. Now, I would like to add liquibase to handle further database migration. What are the correct steps to do this?
I have followed this article to add liquibase and generate changelog. Most articles I've found talk about using liquibase in a project starting from scratch or are not too detailed about the implementation. So far, I've done the following:
Added the dependencies and plugin in pom.xml
<dependencies>
//..other dependencies
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>3.6.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<propertyFile>src/main/resources/liquibase.properties</propertyFile>
</configuration>
</plugin>
</plugins>
</build>
Added the liquibase.properties file under src/main/resources
url=jdbc:mysql://localhost:3306/demodb
username=root
password=root
driver=com.mysql.jdbc.Driver
outputChangeLogFile=src/main/resources/db/changelog/changes/demodb-changelog.xml
Updated the application.properties file under src/main/resources to handle changelogs
#Hibernate
spring.datasource.url=jdbc:mysql://localhost:3306/demodb
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
#Jpa
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
#Liquibase
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml
Created the db.changelog-master.xml file under src/main/resources/db/changelog
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://www.liquibase.org/xml/ns/dbchangelog/1.9
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.9.xsd">
</databaseChangeLog>
Ran the spring boot app so that two new tables are created in the database - DATABASECHANGELOG (which is empty at the moment) and DATABASECHANGELOGLOCK (which has a single null entry at the moment)
Generated the demodb-changelog.xml file from the terminal to create the changelog for the current state of the database
mvn liquibase:generateChangeLog
Then to sync the current changelogs as executed, in liquibase.properties added:
changeLogFile=src/main/resources/db/changelog/changes/demodb-changelog.xml
And then from the terminal ran:
mvn liquibase:changelogSync
Now, the DATABASECHANGELOG table has entries for the changelogs as executed.
Next in the db.changelog-master.xml file, added the generated file:
<include file="db/changelog/changes/demodb-changelog.xml"/>
Now, when I run the app, I get the exception:
Caused by: liquibase.exception.MigrationFailedException:
Migration failed for change set db/changelog/changes/demodb-changelog.xml
Reason: liquibase.exception.DatabaseException: Table 'abc' already exists
So, this is trying to run the changelog files again. How do I configure to run only those changesets that have not yet been run? I thought that the function of the DATABASECHANGELOG was to handle the changesets that have been executed, but I guess I'm wrong here.
I could run the application without the include tag in db.changelog-master.xml, but I guess all the changelog files need to be listed here as I would need all the changelog files if I were to run this app in a different machine to create the entire database from scratch.
So how to configure liquibase to run only changelogs that have not yet been executed?
The problem here is filename field stored in the DATABASECHANGELOG table.
Liquibase determines identity of the changeSet not just by id or by id and author, but instead it determines identity of the changeSet based on value of all three fields: id, author, filename.
When you run mvn liquibase:update or mvn liquibase:changelogSync, the filename of your changelog is src/main/resources/db/changelog/changes/demodb-changelog.xml. This is value that stored by liquibase into DATABASECHANGELOG.FILENAME field in each table row.
But, when you start your spring-boot application, the filename of your changelog is classpath:db/changelog/db.changelog-master.xml, and filename of included into it changelog is db/changelog/changes/demodb-changelog.xml.
Then, Liquibase checks what changesets are already applied.
It checks the id, author and filename of each changeset from DATABASECHANGELOG table and each changeset in the db/changelog/changes/demodb-changelog.xml file, and it found that nothing matched, because filename field differs. In the DB it's value is src/main/resources/db/changelog/changes/demodb-changelog.xml.
src/main/resources/db/changelog/changes/demodb-changelog.xml != db/changelog/changes/demodb-changelog.xml
So, Liquibase thinks these are different changesets and tries to apply them. It fails because table already exist. But if you change your changesets to something that can be applied multiple times, you will see that Liqibase created a new set for rows in the DATABASECHANGELOG table with same id, same author but different filename...
Honestly, I don't know how to solve this problem. It looks like a fundamental problem of how Liquibase in Spring Boot works. When changelog files are read on spring-boot application start, they MUST be in classpath, especially if default fat-jar used. But when you run Liquibase as maven plugin, they are usually on local file system (it's not true, see my "Update 2").
Maybe some workaround exist, please add it in comment and I'll update the answer.
Update
One workaround that I found is to use logicalFilePath attribute of <databaseChangeLog> tag.
Use to override the file name and path when creating the unique identifier of change sets. Required when moving or renaming change logs.
If you carefully set it in all your changelogs/changesets, it should work.
An example for your db/changelog/changes/demodb-changelog.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<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-3.8.xsd"
logicalFilePath="db/changelog/changes/demodb-changelog.xml">
</databaseChangeLog>
But take care of <include> tag. It supports including of raw *.sql files. And you can't set logicalFilePath for them. And logicalFilePath that is set on <databaseChangeLog> tag is also not inherited.
Update 2
I found a more robust solution.
Just don't specify the prefix src/main/resources in changeLogFile property of maven plugin. This way maven will search changelog in classpath, not in filesystem. So, you must do mvn process-resources first, before executing liquibase maven plugin.
So, your changeLogFile property of liquibase maven plugin (liquibase.properties file) should look like this:
changeLogFile=db/changelog/changes/demodb-changelog.xml
Then, when you run
mvn process-resources liquibase:changelogSync
Liquibase will create records in DATABASECHANGELOG table with FILENAME field equal to db/changelog/changes/demodb-changelog.xml, which is equal to filename, used by spring-boot when it runs migrations on startup (classpath: prefix automatically stripped by Liquibase when it compares filenames). And finally this makes Liquibase to understand, that these are the same set of changesets, and they are already applied.
MySQL will maintain the log for liquibase in table,
Liquibase uses the DATABASECHANGELOG table to track which changeSets have been ran.
Whenever the changelog runs, for each change log one row will be added the table, Based on the ids we can know what Change logs are ran,
There are some flags like runAlways, runOnChange, these will help us to execute/ not execute.
you can refer to:
For understanding flags:
https://www.liquibase.org/documentation/changeset.html
For understanding DATABASECHANGELOG Table:
https://www.liquibase.org/documentation/databasechangelog_table.html
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With