Programmatic SchemaExport / SchemaUpdate with Hibernate 5 and Spring 4
Asked Answered
D

4

22

With Spring 4 and Hibernate 4, I was able to use Reflection to get the Hibernate Configuration object from the current environment, using this code:

@Autowired LocalContainerEntityManagerFactoryBean lcemfb;

EntityManagerFactoryImpl emf = (EntityManagerFactoryImpl) lcemfb.getNativeEntityManagerFactory();
SessionFactoryImpl sf = emf.getSessionFactory();
SessionFactoryServiceRegistryImpl serviceRegistry = (SessionFactoryServiceRegistryImpl) sf.getServiceRegistry();
Configuration cfg = null;

try {
    Field field = SessionFactoryServiceRegistryImpl.class.getDeclaredField("configuration");
    field.setAccessible(true);
    cfg = (Configuration) field.get(serviceRegistry);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
    e.printStackTrace();
}

SchemaUpdate update = new SchemaUpdate(serviceRegistry, cfg);

With Hibernate 5, I must use some MetadataImplementor, which doesn't seems to be available from any of those objects. I also tried to use MetadataSources with the serviceRegistry. But it did say that it's the wrong kind of ServiceRegistry.

Is there any other way to get this working?

Derm answered 5/1, 2016 at 12:43 Comment(3)
Related: #32178541Bedlamite
I find the persistence.xml parser from this post pretty handy: #23311117Pyxidium
The ServiceRegistry error you faced was due to the fact that MetadataSources expects 'StandardServiceRegistry' whereas the serviceRegistry from SessionFactoryImpl is not of the above type. Look into my answer for the details.Phenformin
P
8

I would like to add up on Aviad's answer to make it complete as per OP's request.

The internals:

In order to get an instance of MetadataImplementor, the workaround is to register an instance of SessionFactoryBuilderFactory through Java's ServiceLoader facility. This registered service's getSessionFactoryBuilder method is then invoked by MetadataImplementor with an instance of itself, when hibernate is bootstrapped. The code references are below:

  1. Service Loading
  2. Invocation of getSessionFactoryBuilder

So, ultimately to get an instance of MetadataImplementor, you have to implement SessionFactoryBuilderFactory and register so ServiceLoader can recognize this service:

An implementation of SessionFactoryBuilderFactory:

public class MetadataProvider implements SessionFactoryBuilderFactory {

    private static MetadataImplementor metadata;

    @Override
    public SessionFactoryBuilder getSessionFactoryBuilder(MetadataImplementor metadata, SessionFactoryBuilderImplementor defaultBuilder) {
        this.metadata = metadata;
        return defaultBuilder; //Just return the one provided in the argument itself. All we care about is the metadata :)
    }

    public static MetadataImplementor getMetadata() {
        return metadata;
    }
}

In order to register the above, create simple text file in the following path(assuming it's a maven project, ultimately we need the 'META-INF' folder to be available in the classpath):

src/main/resources/META-INF/services/org.hibernate.boot.spi.SessionFactoryBuilderFactory

And the content of the text file should be a single line(can even be multiple lines if you need to register multiple instances) stating the fully qualified class path of your implementation of SessionFactoryBuilderFactory. For example, for the above class, if your package name is 'com.yourcompany.prj', the following should be the content of the file.

com.yourcompany.prj.MetadataProvider

And that's it, if you run your application, spring app or standalone hibernate, you will have an instance of MetadataImplementor available through a static method once hibernate is bootstraped.

Update 1:

There is no way it can be injected via Spring. I digged into Hibernate's source code and the metadata object is not stored anywhere in SessionFactory(which is what we get from Spring). So, it's not possible to inject it. But there are two options if you want it in Spring's way:

  1. Extend existing classes and customize all the way from

LocalSessionFactoryBean -> MetadataSources -> MetadataBuilder

LocalSessionFactoryBean is what you configure in Spring and it has an object of MetadataSources. MetadataSources creates MetadataBuilder which in turn creates MetadataImplementor. All the above operations don't store anything, they just create object on the fly and return. If you want to have an instance of MetaData, you should extend and modify the above classes so that they store a local copy of respective objects before they return. That way you can have a reference to MetadataImplementor. But I wouldn't really recommend this unless it's really needed, because the APIs might change over time.

  1. On the other hand, if you don't mind building a MetaDataImplemetor from SessionFactory, the following code will help you:

    EntityManagerFactoryImpl emf=(EntityManagerFactoryImpl)lcemfb.getNativeEntityManagerFactory();
    SessionFactoryImpl sf=emf.getSessionFactory();
    StandardServiceRegistry serviceRegistry = sf.getSessionFactoryOptions().getServiceRegistry();
    MetadataSources metadataSources = new MetadataSources(new BootstrapServiceRegistryBuilder().build());
    Metadata metadata = metadataSources.buildMetadata(serviceRegistry);
    SchemaUpdate update=new SchemaUpdate(serviceRegistry,metadata); //To create SchemaUpdate
    
    // You can either create SchemaExport from the above details, or you can get the existing one as follows:
    try {
        Field field = SessionFactoryImpl.class.getDeclaredField("schemaExport");
        field.setAccessible(true);
        SchemaExport schemaExport = (SchemaExport) field.get(serviceRegistry);
    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
        e.printStackTrace();
    }
    
Phenformin answered 8/1, 2016 at 18:32 Comment(6)
Is there no other way to inject it via Spring? I don't like those text files, that I must keep track of when renaming or refactoring.Derm
I just updated my answer with a way to use it in Spring, just like how you used it previously with Hibernate 4Phenformin
"new SchemaUpdate(serviceRegistry,metadata)" is giving me an error that this constructor isn't defined, using Hibernate 5.2. Has this moved somewhere else now?Cb
@PeriataBreatta, yes, there is now only a no-arg constructor: docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/tool/… It makes it easier to use: blog.exxeta.com/2016/10/14/…Conlan
This example generally works. Just want to mention that LocalSessionFactoryBean has method setHibernateIntegrators where you can set the integrator without creating text file and pass metadata to SchemaExport. It worked for me.Berlyn
is it possible, with Spring, apply BeanValidation constraints to Schema generation?Otolaryngology
C
10

Basic idea for this problem is:

implementation of org.hibernate.integrator.spi.Integrator which stores required data to some holder. Register implementation as a service and use it where you need.

Work example you can find here https://github.com/valery-barysok/spring4-hibernate5-stackoverflow-34612019


create org.hibernate.integrator.api.integrator.Integrator class

import hello.HibernateInfoHolder;
import org.hibernate.boot.Metadata;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;

public class Integrator implements org.hibernate.integrator.spi.Integrator {

    @Override
    public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
        HibernateInfoHolder.setMetadata(metadata);
        HibernateInfoHolder.setSessionFactory(sessionFactory);
        HibernateInfoHolder.setServiceRegistry(serviceRegistry);
    }

    @Override
    public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
    }
}

create META-INF/services/org.hibernate.integrator.spi.Integrator file

org.hibernate.integrator.api.integrator.Integrator

import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        new SchemaExport((MetadataImplementor) HibernateInfoHolder.getMetadata()).create(true, true);
        new SchemaUpdate(HibernateInfoHolder.getServiceRegistry(), (MetadataImplementor) HibernateInfoHolder.getMetadata()).execute(true, true);
    }
}
Conn answered 10/1, 2016 at 2:29 Comment(1)
This is a very good example: vladmihalcea.com/…Male
P
8

I would like to add up on Aviad's answer to make it complete as per OP's request.

The internals:

In order to get an instance of MetadataImplementor, the workaround is to register an instance of SessionFactoryBuilderFactory through Java's ServiceLoader facility. This registered service's getSessionFactoryBuilder method is then invoked by MetadataImplementor with an instance of itself, when hibernate is bootstrapped. The code references are below:

  1. Service Loading
  2. Invocation of getSessionFactoryBuilder

So, ultimately to get an instance of MetadataImplementor, you have to implement SessionFactoryBuilderFactory and register so ServiceLoader can recognize this service:

An implementation of SessionFactoryBuilderFactory:

public class MetadataProvider implements SessionFactoryBuilderFactory {

    private static MetadataImplementor metadata;

    @Override
    public SessionFactoryBuilder getSessionFactoryBuilder(MetadataImplementor metadata, SessionFactoryBuilderImplementor defaultBuilder) {
        this.metadata = metadata;
        return defaultBuilder; //Just return the one provided in the argument itself. All we care about is the metadata :)
    }

    public static MetadataImplementor getMetadata() {
        return metadata;
    }
}

In order to register the above, create simple text file in the following path(assuming it's a maven project, ultimately we need the 'META-INF' folder to be available in the classpath):

src/main/resources/META-INF/services/org.hibernate.boot.spi.SessionFactoryBuilderFactory

And the content of the text file should be a single line(can even be multiple lines if you need to register multiple instances) stating the fully qualified class path of your implementation of SessionFactoryBuilderFactory. For example, for the above class, if your package name is 'com.yourcompany.prj', the following should be the content of the file.

com.yourcompany.prj.MetadataProvider

And that's it, if you run your application, spring app or standalone hibernate, you will have an instance of MetadataImplementor available through a static method once hibernate is bootstraped.

Update 1:

There is no way it can be injected via Spring. I digged into Hibernate's source code and the metadata object is not stored anywhere in SessionFactory(which is what we get from Spring). So, it's not possible to inject it. But there are two options if you want it in Spring's way:

  1. Extend existing classes and customize all the way from

LocalSessionFactoryBean -> MetadataSources -> MetadataBuilder

LocalSessionFactoryBean is what you configure in Spring and it has an object of MetadataSources. MetadataSources creates MetadataBuilder which in turn creates MetadataImplementor. All the above operations don't store anything, they just create object on the fly and return. If you want to have an instance of MetaData, you should extend and modify the above classes so that they store a local copy of respective objects before they return. That way you can have a reference to MetadataImplementor. But I wouldn't really recommend this unless it's really needed, because the APIs might change over time.

  1. On the other hand, if you don't mind building a MetaDataImplemetor from SessionFactory, the following code will help you:

    EntityManagerFactoryImpl emf=(EntityManagerFactoryImpl)lcemfb.getNativeEntityManagerFactory();
    SessionFactoryImpl sf=emf.getSessionFactory();
    StandardServiceRegistry serviceRegistry = sf.getSessionFactoryOptions().getServiceRegistry();
    MetadataSources metadataSources = new MetadataSources(new BootstrapServiceRegistryBuilder().build());
    Metadata metadata = metadataSources.buildMetadata(serviceRegistry);
    SchemaUpdate update=new SchemaUpdate(serviceRegistry,metadata); //To create SchemaUpdate
    
    // You can either create SchemaExport from the above details, or you can get the existing one as follows:
    try {
        Field field = SessionFactoryImpl.class.getDeclaredField("schemaExport");
        field.setAccessible(true);
        SchemaExport schemaExport = (SchemaExport) field.get(serviceRegistry);
    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
        e.printStackTrace();
    }
    
Phenformin answered 8/1, 2016 at 18:32 Comment(6)
Is there no other way to inject it via Spring? I don't like those text files, that I must keep track of when renaming or refactoring.Derm
I just updated my answer with a way to use it in Spring, just like how you used it previously with Hibernate 4Phenformin
"new SchemaUpdate(serviceRegistry,metadata)" is giving me an error that this constructor isn't defined, using Hibernate 5.2. Has this moved somewhere else now?Cb
@PeriataBreatta, yes, there is now only a no-arg constructor: docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/tool/… It makes it easier to use: blog.exxeta.com/2016/10/14/…Conlan
This example generally works. Just want to mention that LocalSessionFactoryBean has method setHibernateIntegrators where you can set the integrator without creating text file and pass metadata to SchemaExport. It worked for me.Berlyn
is it possible, with Spring, apply BeanValidation constraints to Schema generation?Otolaryngology
W
1

Take a look on this one:

public class EntityMetaData implements SessionFactoryBuilderFactory {

    private static final ThreadLocal<MetadataImplementor> meta = new ThreadLocal<>();

    @Override
    public SessionFactoryBuilder getSessionFactoryBuilder(MetadataImplementor metadata, SessionFactoryBuilderImplementor defaultBuilder) {
        meta.set(metadata);
        return defaultBuilder;
    }

    public static MetadataImplementor getMeta() {
        return meta.get();
    }
}

Take a look on This Thread which seems to answer your needs

Wilscam answered 7/1, 2016 at 21:14 Comment(4)
Thank you. I'll try this later. And report back.Derm
How to integrate this with Spring? The class won't get called and thus getMeta() returns null.Derm
Well, this require change in METADATA folder. Why do you need this? What is the purpose?Wilscam
Yes..getMeta() returning null because of getSessionFactoryBuilder method not getting called.Enjoin
P
0

Well, my go to on this:

public class SchemaTranslator {
    public static void main(String[] args) throws Exception {
        new SchemaTranslator().run();
    }
    private void run() throws Exception {    
        String packageName[] = { "model"};    
        generate(packageName);
    }   
    private List<Class<?>> getClasses(String packageName) throws Exception {
        File directory = null;
        try {
            ClassLoader cld = getClassLoader();
            URL resource = getResource(packageName, cld);
            directory = new File(resource.getFile());
        } catch (NullPointerException ex) {
            throw new ClassNotFoundException(packageName + " (" + directory + ") does not appear to be a valid package");
        }
        return collectClasses(packageName, directory);
    }
    private ClassLoader getClassLoader() throws ClassNotFoundException {
        ClassLoader cld = Thread.currentThread().getContextClassLoader();
        if (cld == null) {
            throw new ClassNotFoundException("Can't get class loader.");
        }
        return cld;
    }
    private URL getResource(String packageName, ClassLoader cld) throws ClassNotFoundException {
        String path = packageName.replace('.', '/');
        URL resource = cld.getResource(path);
        if (resource == null) {
            throw new ClassNotFoundException("No resource for " + path);
        }
        return resource;
    }
    private List<Class<?>> collectClasses(String packageName, File directory) throws ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();
        if (directory.exists()) {
            String[] files = directory.list();
            for (String file : files) {
                if (file.endsWith(".class")) {
                    // removes the .class extension
                    classes.add(Class.forName(packageName + '.' + file.substring(0, file.length() - 6)));
                }
            }
        } else {
            throw new ClassNotFoundException(packageName + " is not a valid package");
        }
        return classes;
    }
    private void generate(String[] packagesName) throws Exception {
        Map<String, String> settings = new HashMap<String, String>();
        settings.put("hibernate.hbm2ddl.auto", "drop-create");
        settings.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL94Dialect");
        MetadataSources metadata = new MetadataSources(
                new StandardServiceRegistryBuilder()
                        .applySettings(settings)
                        .build());    
        for (String packageName : packagesName) {
            System.out.println("packageName: " + packageName);
            for (Class<?> clazz : getClasses(packageName)) {
                System.out.println("Class: " + clazz);
                metadata.addAnnotatedClass(clazz);
            }
        }
        SchemaExport export = new SchemaExport(
                (MetadataImplementor) metadata.buildMetadata()
        );
        export.setDelimiter(";");
        export.setOutputFile("db-schema.sql");
        export.setFormat(true);
        export.execute(true, false, false, false);
    }
}
Pyxidium answered 31/10, 2017 at 19:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.