Wednesday, October 28, 2009

How To Generate DDL Scripts from Hibernate

One of the nice things about using Hibernate in your persistence layer is that it can automatically make updates to your database schema for you. This is nice in development, but oftentimes you need to have the ddl script file. Lucky for us, Hibernate ships with the SchemaExport class. This is what hibernate uses to make updates to your database. I'll show you how we can hijack it and use it for our own purposes as well.

Here we go. As it seems a lot of developers are migrating towards Hibernate annotations and EJB JPA, the following code assumes your classes are configured with annotations. This simple class takes the name of the package where you have your domain objects stored and generates ddl for mysql, oracle and hsql.

package com.jandrewthompson;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.tool.hbm2ddl.SchemaExport;

/**
* @author john.thompson
*
*/
public class SchemaGenerator
{
private AnnotationConfiguration cfg;

public SchemaGenerator(String packageName) throws Exception
{
cfg = new AnnotationConfiguration();
cfg.setProperty("hibernate.hbm2ddl.auto","create");

for(Class<Object> clazz : getClasses(packageName))
{
cfg.addAnnotatedClass(clazz);
}
}

/**
* Method that actually creates the file.
* @param dbDialect to use
*/
private void generate(Dialect dialect)
{
cfg.setProperty("hibernate.dialect", dialect.getDialectClass());

SchemaExport export = new SchemaExport(cfg);
export.setDelimiter(";");
export.setOutputFile("ddl_" + dialect.name().toLowerCase() + ".sql");
export.execute(true, false, false, false);
}

/**
* @param args
*/
public static void main(String[] args) throws Exception
{
SchemaGenerator gen = new SchemaGenerator("org.jthompson.myapp.domain");
gen.generate(Dialect.MYSQL);
gen.generate(Dialect.ORACLE);
gen.generate(Dialect.HSQL);
}

/**
* Utility method used to fetch Class list based on a package name.
* @param packageName (should be the package containing your annotated beans.
*/
private List getClasses(String packageName) throws Exception
{
List classes = new ArrayList();
File directory = null;
try
{
ClassLoader cld = Thread.currentThread().getContextClassLoader();
if (cld == null) {
throw new ClassNotFoundException("Can't get class loader.");
}
String path = packageName.replace('.', '/');
URL resource = cld.getResource(path);
if (resource == null) {
throw new ClassNotFoundException("No resource for " + path);
}
directory = new File(resource.getFile());
} catch (NullPointerException x) {
throw new ClassNotFoundException(packageName + " (" + directory
+ ") does not appear to be a valid package");
}
if (directory.exists()) {
String[] files = directory.list();
for (int i = 0; i < files.length; i++) {
if (files[i].endsWith(".class")) {
// removes the .class extension
classes.add(Class.forName(packageName + '.'
+ files[i].substring(0, files[i].length() - 6)));
}
}
} else {
throw new ClassNotFoundException(packageName
+ " is not a valid package");
}

return classes;
}

/**
* Holds the classnames of hibernate dialects for easy reference.
*/
private static enum Dialect
{
ORACLE("org.hibernate.dialect.Oracle10gDialect"),
MYSQL("org.hibernate.dialect.MySQLDialect"),
HSQL("org.hibernate.dialect.HSQLDialect");

private String dialectClass;
private Dialect(String dialectClass)
{
this.dialectClass = dialectClass;
}
public String getDialectClass()
{
return dialectClass;
}
}
}


It should be pretty easy to modify this code to suit your needs. If you are using hibernate xml configuration, just swap out the AnnotationConfiguration class for org.hibernate.cfg.Configuration.

21 comments:

  1. Hi Andrew, I find your code great, but i'm experience some issues width it. I call the generator new SchemaGenerator("entities") - thats my package - but it doesn't find the classes within the package or I get "ClassNotFoundException, entities it's not a valid package".
    I don't have packages names like 'com.something.other', my package is a sub dir under src directory (src/entities) in my NetBeans project.
    So, what I'm doing wrong?

    ReplyDelete
  2. Hi Gabriel, I know I'm way late chiming in here (I thought google was supposed to notify me when someone posts a comment?)... Did you put SchemaGenerator in the same project as your entities?

    Assuming you did, it might be an issue with the way netbeans is setting up your classpath (I'm an Eclipse user, so I can't really help you here). Basically, the code is going to try to locate your 'entities' package using SchemaGenerator's classloader. So, if the classloader doesn't have visibility to 'entities', you will get that ClassNotFoundException.

    Hope this helps shed some light. -- andrew

    ReplyDelete
  3. If you've got entities in a dependency, this alternative constructor lets you generate the DDL for a given class.

    public SchemaGenerator( Class classToGenerate ) throws Exception {
    cfg = new AnnotationConfiguration();
    cfg.setProperty( "hibernate.hbm2ddl.auto", "create" );
    cfg.addAnnotatedClass( classToGenerate );
    }

    ReplyDelete
  4. Why not to use SchemaExport class? See http://docs.jboss.org/hibernate/core/3.3/reference/en/html/toolsetguide.html

    ReplyDelete
  5. Ondrej, my code IS using the SchemaExport class. As is usual with JBoss, things are not quite as simple as RedHat would like you to believe. First of all, the jboss documentation only mentions passing in a list of hibernate mapping files. It gives no examples of how to accomplish this task if your project uses annotations. To get any real mileage out of the SchemaExport class (don't get me wrong, I'm glad JBoss provides this tool) requires some supporting code to go along with it. I just wanted to show some folks how I utilize it in my projects.

    Cheers -- andrew

    ReplyDelete
  6. Thanks for sharing, saved a lot of time :)

    ReplyDelete
  7. Thank you very much!

    If you are using eclipse on windows the default workspace location is in c:\Documents and Settings\workspace. The spaces in the path will require URL decoding of the getFile() like so:

    directory = new File(URLDecoder.decode(resource.getFile(), "UTF-8"));

    Also the lowercase C's in class caused me some grief before I realized the needed to be uppercase.

    ReplyDelete
  8. Good article and nicely explained. Thanks.

    ReplyDelete
  9. Hi Andrew,

    really thank you for this code ..for me its working like an normal application..but I am facing errors when I run this in JBOSS after including all hibernate dependencies also..and its throwing following error.

    org.hibernate.AnnotationException: java.lang.NoSuchMethodException: org.hibernate.validator.ClassValidator

    Can you help me to sort it out and thanks in Advance.

    Regards,
    Raja.

    ReplyDelete
  10. @raja

    Usually, NoSuchMethodExceptions are thrown when you have conflicting versions of the same jar in your classpath. JBoss, if I'm not mistaken, ships with some hibernate jars. The jboss versions may have slightly different signatures than what you're expecting. Your options are to either attempt to upgrade the hibernate jars in Jboss, or to rework the code above to work with jboss's provided libs.

    Hope this helps -- andrew

    ReplyDelete
  11. Hi Andrew,

    thanks for the answer..after upgrading the jars in JBOSS..its throwing the following error..java.lang.ClassCastException: org.hibernate.ejb.HibernatePersistence cannot be c
    ast to javax.persistence.spi.PersistenceProvider

    I am using annotations.jar and entitymanager.jar dependencies

    can you please tell me how to remove these errors.

    ReplyDelete
  12. Hi,

    actually how can I run this program in JBOSS?..is it possible to deploy this program in JBOSS..can you help me how can I make it.

    Thanks in Advance,

    Best Regards,
    Raja.

    ReplyDelete
  13. @raja
    I'm sure it could be run in Jboss, but honestly, that is not really the intent of the application. Ideally, generating the schema for your application would be done before you deploy to your application server.

    I have changed the hibernate version in Jboss before, but it is not for the faint of heart. There are lots of dependencies that need to work together, and it is too much to try to help in this forum.

    If you really want to use this in JBoss, I think it would be easier to port this code than to change Jboss.

    Regards, and good luck! -- andrew

    ReplyDelete
  14. Hi Andrew,

    nice work. Helped us a lot until someone created a directory for test classes for some package to be scanned. We had:
    src/main/java/de/foo/bar/beans/ and
    src/test/java/de/foo/bar/beans/

    Your getClasses only scans the latter directory. Therefore our annotated beans were not found anymore.

    We fixed it with the following method:
    private List> getClasses(String packageName) throws Exception
    {
    List classes = new ArrayList();
    File directory = null;


    ClassLoader cld = Thread.currentThread().getContextClassLoader();
    if (cld == null) {
    throw new ClassNotFoundException("Can't get class loader.");
    }

    String path = packageName.replace('.', '/');
    Enumeration resources = cld.getResources(path);

    while (resources.hasMoreElements()) {
    URL resource = resources.nextElement();

    try {
    directory = new File( resource.toURI().getPath() );
    } catch (NullPointerException x) {
    throw new ClassNotFoundException(packageName + " (" + directory
    + ") does not appear to be a valid package");
    }
    if (directory.exists()) {
    String[] files = directory.list();
    for (int i = 0; i < files.length; i++) {
    if (files[i].endsWith(".class")) {
    // removes the .class extension
    classes.add(Class.forName(packageName + '.'
    + files[i].substring(0, files[i].length() - 6)));
    }
    }
    }
    }

    return classes;
    }

    ReplyDelete
  15. Here's a reworked function that works if your classes are contained in a .jar as well as simply in the filesystem


    private List> getClasses(String packageName) throws Exception
    {
    final String classExt = ".class";
    List files = FileUtils.listFilesInPackage(packageName);
    List> classes = new ArrayList>(files.size());

    for (String file : files) {
    if (file.endsWith(classExt)) {
    // removes the .class extension, and replaces '/' with '.'
    String className = (file.substring(0, file.length() - classExt.length())).replace(File.separatorChar, '.');
    classes.add(Class.forName(className));
    }
    }

    return classes;
    }

    public static List listFilesInPackage(String packageName) throws ClassNotFoundException, IOException {
    List classNames = new LinkedList();
    File directory = null;
    try
    {
    ClassLoader cld = Thread.currentThread().getContextClassLoader();
    if (cld == null) {
    throw new ClassNotFoundException("Can't get class loader.");
    }
    String path = packageName.replace('.', '/');
    URL resource = cld.getResource(path);
    if (resource == null) {
    throw new ClassNotFoundException("No resource for " + path);
    }
    directory = new File(resource.getFile());
    } catch (NullPointerException x) {
    throw new ClassNotFoundException(packageName + " (" + directory
    + ") does not appear to be a valid package");
    }
    if (directory.exists()) { // Deal with file-system case
    String[] files = directory.list();
    for (int i = 0; i < files.length; i++) {
    classNames.add(packageName.replace('.', File.separatorChar) + '.' + files[i]);
    }
    }
    else { // Deal with case where files are within a JAR
    final String[] parts = directory.getPath().split(".jar!\\\\");
    if (parts.length == 2) {
    String jarFilename = parts[0].substring(6) + ".jar";
    String relativePath = parts[1].replace(File.separatorChar, '/');
    JarFile jarFile = new JarFile(jarFilename);
    final Enumeration entries = jarFile.entries();
    while (entries.hasMoreElements()) {
    final JarEntry entry = entries.nextElement();
    final String entryName = entry.getName();
    if ((entryName.length() > relativePath.length()) && entryName.startsWith(relativePath)) {
    classNames.add(entryName.replace('/', File.separatorChar));
    }
    }
    }
    else {
    throw new ClassNotFoundException(packageName
    + " is not a valid package");
    }
    }

    return classNames;

    }

    ReplyDelete
  16. For me, I made a change 'cause all my entities are into subpackages. So I recursively call the getClasses method ==>

    if (directory.exists()) {
    File[] files = directory.listFiles();
    for (int i = 0; i < files.length; i++) {
    File file = files[i];
    if (file.isFile() && file.getName().endsWith(".class")) {
    Class aClass = Class.forName(packageName + '.'
    + file.getName().substring(0, file.getName().length() - 6));
    classes.add(aClass);
    } else if (file.isDirectory()) {
    getClasses(packageName + "." + file.getName());
    }

    }
    } else {
    throw new ClassNotFoundException(packageName
    + " is not a valid package");
    }

    ReplyDelete
  17. What would be the change to use the persistence.xml to list the classes?

    Thanks!

    Jeff

    ReplyDelete
  18. Jeff, I just created some code that does what you seem to need:

    List classes() throws Exception {
    File f = new File("src/test/resources/META-INF/persistence-test.xml");
    String cname = "http://java.sun.com/xml/ns/persistence";
    Namespace ns = Namespace.getNamespace(cname);
    Document build = new org.jdom2.input.SAXBuilder().build(f);

    Element pu = build.getRootElement().getChild("persistence-unit", ns);
    List classes = new ArrayList<>();
    for (Object clz : pu.getChildren("class", ns)) {
    classes.add(Class.forName(((Element) clz).getText()));
    }
    return classes;
    }

    Crappy code and hardcoded reference to persistence file, but hey it works :)

    Requires JDOM, tested with 2.0.2.

    ReplyDelete
  19. hi Andrew,
    I am using entities as dependency in pom.xml as :

    com.org
    model
    1.2


    All entities come in com.org.entity package under src/main/java folder in the library whose dependency I am using to get entities. It is compiled. no error in compilation.
    I am giving com.org.entity package name to SchemaGenerator but I am getting Exception as :
    file:/Users/user1/.m2/repository/com/org/entity/model/1.2/model-1.2.jar!/com/org
    Exception in thread "main" java.lang.ClassNotFoundException: com.org.entity is not a valid package.
    Can you suggest me what I am doing wrong.
    Thanks

    ReplyDelete