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!

8 comments:

  1. Hi George,

    Nice writeup, Could you please publish a sample maven project with this config? May be on github? Lot of people will appreciate it!

    ReplyDelete
  2. George, thanks for this very helpful post. I was trying to use the hibernate3-maven-plugin and was running into all these problems, and your post solved them all. You are a JPA god.

    ReplyDelete
  3. Thanks, guys! Nice to hear about the post helped you. Unfortunately there is no sample project with that stuff as it is a small excerpt from a big maven project, but it is pretty straightforward to create one (and I would definetely do it if not the lack of time :().

    ReplyDelete
  4. Hi...
    Do you have simple steps for publishing Maven + (spring + hibernate) in GAE? Which is the most used framework for PHP? struts? CodeIgnitor?

    ReplyDelete
  5. Hi,

    very helpful post. It saved my day a couple of weeks ago. I just did a simple maven plugin based on your solution:

    https://github.com/abendt/hibernate4-maven-plugin

    best regards, Alphonse Bendt

    ReplyDelete
  6. Alphonse, this is great! I'll look at it and put the link into the post if you don't mind.

    ReplyDelete
  7. Hi George,

    I have written a maven-plugin, that can replace the hibernate3-maven-plugin:. It's called hibernate4-maven-plugin and is available on the central maven repository:

    http://juplo.de/hibernate4-maven-plugin/


    Best regards,
    Kai Moritz

    ReplyDelete
  8. jpa god where can i find a church to pray?

    ReplyDelete