Java EE 7 Database Migrations with Liquibase and WildFly
July 31st, 2014 by Micha KopsI have written about other database migration frameworks before but in this article I’d like to cover the Liquibase framework in combination with WildFly as Java EE 7 compatible application server.
In the following tutorial, we’re going to write a full Java EE 7 book store application with a few steps and with Liquibase on board to create the database structure and insert example data into the database.
Thanks to the WildFly Maven Plug-in we even do not need to download and configure the application server but let Maven and the plug-in do the work for us.
Contents
Java EE 7 Webproject with Maven
The easiest way to create the project is using the archetype org.codehaus.mojo.archetypes:webapp-javaee7:
mvn archetype:generate -Dfilter=webapp-javaee7 [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building Maven Stub Project (No POM) 1 [INFO] ------------------------------------------------------------------------ [..] Choose archetype: 1: remote -> org.codehaus.mojo.archetypes:webapp-javaee7 (Archetype for a web application using Java EE 7.) Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 1
Or using your IDE of choice with Maven support e.g. Eclipse IDE:
In addition, we need the dependencies for Liquibase (liquibase.core and a logging adapter for my logging framework of choice here, SLF4J), so the dependencies node in our final pom.xml should look like this one:
<dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> <version>3.2.2</version> </dependency> <dependency> <groupId>com.mattbertolini</groupId> <artifactId>liquibase-slf4j</artifactId> <version>1.2.1</version> </dependency> </dependencies>
Last but not least we need to add the WildFly Maven Plugin because we’re too lazy to download, install, configure and start the application server by hand:
<plugin> <groupId>org.wildfly.plugins</groupId> <artifactId>wildfly-maven-plugin</artifactId> <version>1.0.2.Final</version> </plugin>
Creating our Application
We’re going to build a simple book store application with a RESTful webservice to persist new books to the database and to enlist available, persisted books.
The database structure is created using the Liquibase Database Migration framework initialized in an EJB.
Bootstrap Loader
We’re using a singleton session bean annotated with javax.ejb.Startup to initiate our database migrations on application startup.
Since every Java EE Application server must supply a database, we’re free to use the H2 database embedded with our WildFly server.
We’re obtaining a JDBC datasource by @Resource injection and we’re getting a reference to the default resource .. in WildFly it is “jboss/datasources/ExampleDS“.
Now that we’ve got a connection to the database we simply initialize Liquibase, tell the migration framework where to find our migration, we’re setting the context to “development” and finally we’re performing the database migration’s upgrade tasks.
package com.hascode.tutorial.setup; import java.sql.Connection; import java.sql.SQLException; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.ejb.Singleton; import javax.ejb.Startup; import javax.ejb.TransactionManagement; import javax.ejb.TransactionManagementType; import javax.sql.DataSource; import liquibase.Liquibase; import liquibase.database.Database; import liquibase.database.DatabaseFactory; import liquibase.database.jvm.JdbcConnection; import liquibase.exception.LiquibaseException; import liquibase.resource.ClassLoaderResourceAccessor; import liquibase.resource.ResourceAccessor; @Startup @Singleton @TransactionManagement(TransactionManagementType.BEAN) public class InitializerBean { private static final String STAGE = "development"; private static final String CHANGELOG_FILE = "liquibase/db.changelog.xml"; @Resource private DataSource ds; @PostConstruct protected void bootstrap() { ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader()); try (Connection connection = ds.getConnection()) { JdbcConnection jdbcConnection = new JdbcConnection(connection); Database db = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcConnection); Liquibase liquiBase = new Liquibase(CHANGELOG_FILE, resourceAccessor, db); liquiBase.update(STAGE); } catch (SQLException | LiquibaseException e) { } } }
Persistence Configuration
This is our persistence configuration file persistence.xml in src/main/resources/META-INF.
We’re just adding a persistence unit referencing the existing default database:
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="default"> </persistence-unit> </persistence>
Database Migration
The following database migration creates the structure for or book table and adds two books to the database.
I won’t go into the details of the Liquibase syntax but there exists an excellent documentation on the Liquibase website here.
The referenced XML schema allows us to use auto-completion in most IDE’s (I like that!).
This is our db.changelog.xml in src/main/resources/liquibase:
<?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.2.xsd"> <changeSet author="micha_kops" id="2014-07-29_1440-create-book-table" context="development"> <createTable tableName="book"> <column name="id" type="CHAR(36)"> <constraints nullable="false" /> </column> <column name="title" type="VARCHAR(255)" /> <column name="published" type="DATETIME" /> </createTable> </changeSet> <changeSet author="micha_kops" id="2014-07-29_1450-add-a-book" context="development"> <insert tableName="book"> <column name="id" value="3B79F749-5B96-46CD-AAF8-4E75D663E6D4"/> <column name="title" value="Some Book"/> <column name="published" value="2014-01-11 12:00:01"/> </insert> </changeSet> <changeSet author="micha_kops" id="2014-07-29_1455-add-another-book" context="development"> <insert tableName="book"> <column name="id" value="6FE7A1E5-B172-4D7C-97C7-88667862B264"/> <column name="title" value="Another book"/> <column name="published" value="2014-01-11 12:20:00"/> </insert> </changeSet> </databaseChangeLog>
Book Entity
Our book entity simply has and id and a title and in addition we’re adding a named query to return all existing books from the database ordered by their title in ascending order.
package com.hascode.tutorial.entity; import java.util.Date; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Temporal; import javax.persistence.TemporalType; @Entity @NamedQueries(@NamedQuery(name = Book.Query.FIND_ALL, query = "SELECT b FROM Book b ORDER BY b.title")) public class Book { public static class Query { public static final String FIND_ALL = "Book.findAll"; } @Id private String id; private String title; @Temporal(TemporalType.TIMESTAMP) private Date published; // getter, setter, toString omitted }
Stateless Book Bean
The responsibility of our stateless book bean is to persist and fetch book entities to/from the database within the stateless session bean’s transaction context.
package com.hascode.tutorial.service; import java.util.List; import java.util.UUID; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import com.hascode.tutorial.entity.Book; @Stateless public class BookBean { @PersistenceContext private EntityManager em; public List<Book> findAll() { return em.createNamedQuery(Book.Query.FIND_ALL, Book.class).getResultList(); } public void create(final Book book) { book.setId(UUID.randomUUID().toString().toUpperCase()); em.persist(book); } }
RESTful Book Webservice
Our simple webservice supports two operations: fetching available books and storing a new book.
Both tasks are delegated to the BookBean injected into the webservice, nothing special here..
package com.hascode.tutorial.service; import javax.ejb.Stateless; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.hascode.tutorial.entity.Book; @Stateless @Path("/book") public class BookWebservice { @Inject private BookBean bookBean; @GET @Produces(MediaType.APPLICATION_JSON) public Response getBooks() { return Response.ok(bookBean.findAll()).build(); } @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response create(final Book book) { bookBean.create(book); return Response.ok().build(); } }
Project Structure
Our project structure should look similar to this one by now so that Maven is able to package our application as a war-file.
$ tree
.
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── hascode
│ └── tutorial
│ ├── entity
│ │ └── Book.java
│ ├── service
│ │ ├── BookBean.java
│ │ ├── BookWebservice.java
│ │ └── RSConfiguration.java
│ └── setup
│ └── InitializerBean.java
├── resources
│ ├── liquibase
│ │ └── db.changelog.xml
│ └── META-INF
│ └── persistence.xml
└── webapp
Running WildFly and the Liquibase Migrations
Now that we’ve put together everything we need, the last step is to get a WildFly application server running, build, package and deploy our application and verify that the database migrations are applied on deployment without an error.
Luckily, the WildFly Maven Plugin does all this work for us: Running the Maven goal wildfly:run downloads the application server, copies it into the project’s target directory and deploys our application.
This is the excerpt from the Maven output that displays the database migrations applied and the success of these operations:
$ mvn clean package wildfly:run [..] 19:43:03,944 INFO [liquibase] (ServerService Thread Pool -- 11) Successfully acquired change log lock 19:43:04,489 INFO [liquibase] (ServerService Thread Pool -- 11) Creating database history table with name: PUBLIC.DATABASECHANGELOG 19:43:04,493 INFO [liquibase] (ServerService Thread Pool -- 11) Reading from PUBLIC.DATABASECHANGELOG 19:43:04,498 INFO [liquibase] (ServerService Thread Pool -- 11) liquibase/db.changelog.xml: 2014-07-29_1440-create-book-table::micha_kops: Table book created 19:43:04,499 INFO [liquibase] (ServerService Thread Pool -- 11) liquibase/db.changelog.xml: 2014-07-29_1440-create-book-table::micha_kops: ChangeSet liquibase/db.changelog.xml::2014-07-29_1440-create-book-table::micha_kops ran successfully in 1ms 19:43:04,507 INFO [liquibase] (ServerService Thread Pool -- 11) liquibase/db.changelog.xml: 2014-07-29_1450-add-a-book::micha_kops: New row inserted into book 19:43:04,507 INFO [liquibase] (ServerService Thread Pool -- 11) liquibase/db.changelog.xml: 2014-07-29_1450-add-a-book::micha_kops: ChangeSet liquibase/db.changelog.xml::2014-07-29_1450-add-a-book::micha_kops ran successfully in 1ms 19:43:04,511 INFO [liquibase] (ServerService Thread Pool -- 11) liquibase/db.changelog.xml: 2014-07-29_1455-add-another-book::micha_kops: New row inserted into book 19:43:04,511 INFO [liquibase] (ServerService Thread Pool -- 11) liquibase/db.changelog.xml: 2014-07-29_1455-add-another-book::micha_kops: ChangeSet liquibase/db.changelog.xml::2014-07-29_1455-add-another-book::micha_kops ran successfully in 1ms 19:43:04,517 INFO [liquibase] (ServerService Thread Pool -- 11) Successfully released change log lock [..]
Using the RESTful Webservice with cURL
This is nearly offtopic, but if we’d wish to test our persistence solution using our RESTful webservice, this may be achieved with a view steps using cURL (or a similar tool of choice).
Fetching available books
We’re fetching available books from the server and we’re able to see that the books added by the database migration are reflected in the server’s JSON response.
$ curl -XGET http://localhost:8080/javaee7-wildfly-liquibase-tutorial-1.0.0/rs/book [{"id":"6FE7A1E5-B172-4D7C-97C7-88667862B264","title":"Another book","published":1389439200000},{"id":"3B79F749-5B96-46CD-AAF8-4E75D663E6D4","title":"Some Book","published":1389438001000}]
Persisting a new book
Since we’ve specified that our webservice accepts the content-type application/json, we need to add this header to our request.
$ curl -H "Content-Type: application/json" -XPOST -d '{"id":null, "title":"New book", "published":"2009-11-15T14:12:12"}' http://localhost:8080/javaee7-wildfly-liquibase-tutorial-1.0.0/rs/book
Fetching again to verify
We’re able to see that now there’s one more book in the list of books fetched from the server:
$ curl -XGET http://localhost:8080/javaee7-wildfly-liquibase-tutorial-1.0.0/rs/book [{"id":"6FE7A1E5-B172-4D7C-97C7-88667862B264","title":"Another book","published":1389439200000},{"id":"5F939EC7-549A-4D37-809C-BB605269E955","title":"New book","published":1258294332000},{"id":"3B79F749-5B96-46CD-AAF8-4E75D663E6D4","title":"Some Book","published":1389438001000}]
Tutorial Sources
Please feel free to download the tutorial sources from my Bitbucket repository, fork it there or clone it using Git:
git clone https://bitbucket.org/hascode/javaee7-wildfly-liquibase-migrations.git
Resources
- JSR 342: Java EE 7 Specification
- WildFly Homepage
- WildFly Maven Plugin Documentation
- Liquibase Project Website
- cURL Website
Troubleshooting
Though using Liquibase is really easy-to-use and straightforward, you might encounter one of the following situations:
Classloader Issues
Using the latest (to the moment of writing) version of liquibase-core may produce the following warning that might be irritating when viewing the log files – but besides the classloader errors here the migrations are handled without a problem
22:00:50,381 WARN [org.jboss.modules] (ServerService Thread Pool -- 23) Failed to define class liquibase.sdk.watch.WatchCommand$DynamicContentHandler in Module "deployment.javaee7-wildfly-liquibase-tutorial-1.0.0.war:main" from Service Module Loader: java.lang.LinkageError: Failed to link liquibase/sdk/watch/WatchCommand$DynamicContentHandler (Module "deployment.javaee7-wildfly-liquibase-tutorial-1.0.0.war:main" from Service Module Loader) at org.jboss.as.ee.component.ComponentStartService$1.run(ComponentStartService.java:54) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) [rt.jar:1.7.0_40-ea] at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334) [rt.jar:1.7.0_40-ea] at java.util.concurrent.FutureTask.run(FutureTask.java:166) [rt.jar:1.7.0_40-ea] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [rt.jar:1.7.0_40-ea] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [rt.jar:1.7.0_40-ea] at java.lang.Thread.run(Thread.java:722) [rt.jar:1.7.0_40-ea] at org.jboss.threads.JBossThread.run(JBossThread.java:122) Caused by: java.lang.NoClassDefFoundError: org/eclipse/jetty/server/handler/AbstractHandler at java.lang.ClassLoader.defineClass1(Native Method) [rt.jar:1.7.0_40-ea] at java.lang.ClassLoader.defineClass(ClassLoader.java:791) [rt.jar:1.7.0_40-ea] at org.jboss.modules.ModuleClassLoader.doDefineOrLoadClass(ModuleClassLoader.java:361) [jboss-modules.jar:1.3.3.Final] at org.jboss.modules.ModuleClassLoader.defineClass(ModuleClassLoader.java:482) [jboss-modules.jar:1.3.3.Final] ... 75 more Caused by: java.lang.ClassNotFoundException: org.eclipse.jetty.server.handler.AbstractHandler from [Module "deployment.javaee7-wildfly-liquibase-tutorial-1.0.0.war:main" from Service Module Loader] at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:213) [jboss-modules.jar:1.3.3.Final] at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:459) [jboss-modules.jar:1.3.3.Final] at org.jboss.modules.ConcurrentClassLoader.performLoadClassChecked(ConcurrentClassLoader.java:408) [jboss-modules.jar:1.3.3.Final] at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:389) [jboss-modules.jar:1.3.3.Final] at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:134) [jboss-modules.jar:1.3.3.Final] ... 79 more
Switching to an older version helps to avoid this output, e.g. by using version 3.1.1:
<dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> <version>3.1.1</version> </dependency>
Liquibase logging to STDERR
Running on JBoss AS or WildFly and without a logger configured, Liquibase logs to STDERR and the Application Server logs such output as an error – the output might look like this one:
20:59:55,188 ERROR [stderr] (ServerService Thread Pool -- 14) INFO 7/29/14 8:59 PM: liquibase: Successfully acquired change log lock
The solution is to add a logging adapter for Liquibase e.g. for SLF4J:
<dependency> <groupId>com.mattbertolini</groupId> <artifactId>liquibase-slf4j</artifactId> <version>1.2.1</version> </dependency>
Alternatives: GlassFish and Flyway
I have written an article about database migrations in a Java EE enviroment using an alterenative setup with a GlassFish as application server and Flyway as database migration library: “Easy Database Migrations using Flyway, Java EE 6 and GlassFish“.
Article Updates
2014-08-01: Unnecessary JDNI references to JBoss/WildFly resources removed, thanks @john_waterwood for the remark!
Tags: application server, as, curl, db, dbmigrate, ejb, entitymanager, h2, javaee, javaee7, jax-rs, jaxrs, jee, jpa, json, liquibase, migration, persistence, rdbms, rest, wildfly
August 1st, 2014 at 8:33 am
Great article, but why use the JBoss specific name for the default data source? There’s a standard name in Java EE 7 for that.
And if you omit the data source in persistence.xml you’ll get the default one as well.
August 1st, 2014 at 9:24 am
Hi John,
thanks for your remarks! I’ve updated the article.
Cheers,
Micha
November 23rd, 2015 at 7:27 am
Hi,
Thanks for this great article.
I have trying to integrate the Liquibase with my application at my work place.
We are using Tomcat with openEjb. In the openejb.xml we specifiy multiple data sources.
I am not able to automate this scenario. Can you please provide any help in this case.
Thanks in advance,
Sanjeev