5/30/12

Schema generation with Hibernate 4, JPA and Maven

How you would do it in the good old times

It should be easy, really easy, just as easy as fry an egg. Just take Maven and maven-hibernate3-plugin and follow a receipt described here. Once you run it you may notice that it has Hibernate 3.3.1 in its dependency list. Not a big surprise really, after all it's Hibernate3 plugin. But that's a real problem if you are using JPA 2.0 annotations in your mappings as JPA 2.0 isn't supported in Hibernate 3.3.1. For example maven-hibernate3-plugin has troubles with the following construction:
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "PROLONGATION_DATES", joinColumns = 
 @JoinColumn(name = "PARENT_ID", referencedColumnName = "ID"))
@Temporal(TemporalType.DATE)
@Column(name = "PROLONGATION_DATE")
private List<Date> prolongationDates;

It complains about @Temporal annotation, then about List and so on. Actually, you can make it work. Just override some dependencies by adding the following fragment to the plugin configuration shown in the reference above:
<dependencies>
 <dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-tools</artifactId>
  <version>3.2.4.GA</version>
 </dependency>
 <dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-entitymanager</artifactId>
  <version>3.6.10.Final</version>
 </dependency>
 <dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-core</artifactId>
  <version>3.6.10.Final</version>
 </dependency>
 <dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.14</version>
 </dependency>
 <dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.1.1</version>
 </dependency>
</dependencies>
This way you'll be using the latest hibernate-tools available at the moment and the final versions of Hibernate3 branch libraries. Unfortunately you can't make it work with Hibernate4 libraries. If you try to change versions to for instance 4.1.3.Final, you'll get interface incompatibility errors. So if you really need a schema generated by Hibernate4 you'll need another solution. And there is one.

Custom schema exporter

Actually, it's easier than you may think, just take a look at the org.hibernate.tool.hbm2ddl.SchemaExport class. It has a main method so you can run it using exec-maven-plugin. But once you try to do it you'll realize that there is no way to point SchemaExport out to use JPA configuration. That's a pity, but if you look at the code you'll see that it uses Hibernate Configuration object to produce schema export script (lines 188-189):
this.dropSQL = configuration.generateDropSchemaScript( dialect );
this.createSQL = configuration.generateSchemaCreationScript( dialect );
It means that we need to pass Ejb3Configuration to the SchemaExport instance somehow to make it work with JPA. Unfortunately SchemaExport creates Configuration object just inside its main method, so there is no chance to pass it this way. All we can do is to create our own CustomSchemaExport class, create Ejb3Configuration inside of it and either pass it to the SchemaExport constructor or mimic SchemaExport behavior by pulling all the relevant logic out of it. I preferred the second way as it promised to be more customizable (and it was worth it as I was able to generate grants and synonyms creation scripts). Actually the amount of logic that makes sense to us is fairly small. Here is a whole and fully functional example of a custom schema exporter:

package com.blogspot.doingenterprise;

import org.hibernate.cfg.Configuration;
import org.hibernate.dialect.Dialect;
import org.hibernate.ejb.Ejb3Configuration;
import org.hibernate.engine.jdbc.internal.FormatStyle;
import org.hibernate.engine.jdbc.internal.Formatter;

import java.io.*;

public class SchemaExport {

 public static void main(String[] args) {
  boolean drop = false;
  boolean create = false;
  String outFile = null;
  String delimiter = "";
  String unitName = null;

  for (int i = 0; i < args.length; i++) {
   if (args[i].startsWith("--")) {
    if (args[i].equals("--drop")) {
     drop = true;
    } else if (args[i].equals("--create")) {
     create = true;
    } else if (args[i].startsWith("--output=")) {
     outFile = args[i].substring(9);
    } else if (args[i].startsWith("--delimiter=")) {
     delimiter = args[i].substring(12);
    }
   } else {
    unitName = args[i];
   }
  }

  Formatter formatter = FormatStyle.DDL.getFormatter();

  Ejb3Configuration jpaConfiguration = new Ejb3Configuration().configure(unitName, null);
  Configuration hibernateConfiguration = jpaConfiguration.getHibernateConfiguration();

  String[] createSQL = hibernateConfiguration.generateSchemaCreationScript(
    Dialect.getDialect(hibernateConfiguration.getProperties()));
  String[] dropSQL = hibernateConfiguration.generateDropSchemaScript(
    Dialect.getDialect(hibernateConfiguration.getProperties()));

  if (create)
   export(outFile, delimiter, formatter, createSQL);
  if (drop)
   export(outFile, delimiter, formatter, dropSQL);
 }

 private static void export(String outFile, String delimiter, Formatter formatter, String[] createSQL) {
  PrintWriter writer = null;
  try {
   writer = new PrintWriter(outFile);
   for (String string : createSQL) {
    writer.print(formatter.format(string) + "\n" + delimiter + "\n");
   }
  } catch (FileNotFoundException e) {
   System.err.println(e);
  } finally {
   if (writer != null)
    writer.close();
  }
 }
}
As it was already mentioned above you can run this shiny new exporter via the maven-exec-plugin. Just put these lines to your pom.xml file:
<build>
<plugins>
.........
<plugin>
 <groupId>org.codehaus.mojo</groupId>
 <artifactId>exec-maven-plugin</artifactId>
 <version>1.1.1</version>
 <executions>
  <execution>
   <id>generate-create-schema-ddl</id>
   <phase>process-classes</phase>
   <goals>
    <goal>java</goal>
   </goals>
   <configuration>
    <mainClass>com.blogspot.doingenterprise.SchemaExport</mainClass>
    <arguments>
     <argument>--create</argument>
     <argument>--delimiter=/</argument>
     <argument>--output=${project.basedir}/${project.build.finalName}-create-tables.sql</argument>
     <argument>${project.build.finalName}</argument>
    </arguments>
   </configuration>
  </execution>
 </executions>
 <dependencies>
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-entitymanager</artifactId>
   <version>4.1.3.Final</version>
  </dependency>
 </dependencies>
</plugin>
.........
</plugins>
</build>
That's it for today. Enjoy!