In his great Liquibase introductory blog Michael Wenz described how to get Liquibase and schema evolution up and running. I want to tackle a very specific DB upgrade issue today in which developers will nearly always run when using JPA as an underlying framework. Let’s look at a sample scenario:
- You develop an app, create entities, have JPA create the tables
- You deploy the app
- Sometime later you change the data model
- If you create a new entity, JPA will handle this and create the table automatically
- If you modify an existing entity, JPA cannot handle this, you need a Liquibase refactoring script
- You redeploy the app, the script is executed and everything is fine
Working this way will get you very far (and there might be even use cases where it is totally sufficient), just incrementally doing the small changes in Liquibase and let JPA handle the big new table definitions. Where is the issue?
What will happen if you either move your application to a different account or if you are lucky and don’t have to work on your project alone anymore and another developer is going to run it locally? Yes, at start-up Liquibase will try to run the scripts against the not-yet-existing database and will fail. It seems like a hen-egg problem. Let’s look at some ways to solve this.
1. Deactivate Liquibase Locally
An obvious solution is to simply deactivate Liquibase locally adding “-Dliquibase.should.run=false” to your launch configuration. This way, you will always have JPA create the database for you and if you make some entity changes and your setup allows it you simply delete the local database and have it recreated by JPA. You track the changes to existing entities in a Liquibase script.
- You have to manually set the -D argument everytime you run the app somewhere locally.
- You have to set liquibase.onerror.fail in your web.xml to false upon deployment to a new cloud space and to true for future updates.
2. Make Liquibase The First-Class-Citizen
Another solution is to do everything through Liquibase and set the EclipseLink property “eclipselink.ddl-generation” in your persistence.xml from “create-tables” to “none”. Now, for every entity you create you write a Liquibase script with the table definition. Everybody can now update his database and updates will go through easily. Liquibase offers a command to create such a script and also upcoming diffs automatically.
- You need quite some effort to create the table definition scripts
- They can become very large
- For databases which Liquibase does not support you need to write DB dependent SQL which makes it even bigger
- Liquibase diff is not supporting JPA, only Hibernate
3. Make a Happy Couple
To me, the second way seems OK but one looses lots of simplicity and beauty. Instead of easy to understand JPA entities we duplicate this definition to a totally different and really lengthy script format. Interestingly enough there actually is a way to unite these technologies and get the best of both worlds:
If we analyze the problem scenario we realize that actually all we need in most cases is a check if the app is started for the first time or not. If it is, we want JPA to create everything, if it is not, we want to execute the Liquibase scripts. Easy enough. Liquibase offers conditional checks called preconditions which can be put into changesets and which define when the changeset should be executed.
<changeSet id="3" author="rw"> <preConditions onFail="MARK_RAN"> <tableExists tableName="USERS" /> </preConditions> <sql>ALTER TABLE FIXTURES ADD (EXTID VARCHAR(255))</sql> </changeSet>
In the example above I check for a well known table (users). If this table does not exist, it will ignore the other commands inside the changeset and mark it as “ran” which is the intended outcome since JPA will create everything for us. If I execute this now locally for the first time, the following output appears in the console:
INFO 04.12.12 09:35:liquibase: Marking ChangeSet: db/db.changelog-20120924-Fixtures.xml::3::rw:: (Checksum: 3:b94a1d5a11fae972bd39032ea93dd396) ran despite precondition failure due to onFail='MARK_RAN': db/db.changelog.xml : Table USERS does not exist