/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.hbm; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.inject.Provider; import javax.inject.Singleton; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.boot.Metadata; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.SessionFactoryBuilder; import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl; import org.hibernate.boot.registry.BootstrapServiceRegistry; import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cfg.AvailableSettings; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EventType; import org.hibernate.jpa.event.spi.JpaIntegrator; import org.hibernate.service.spi.ServiceRegistryImplementor; import org.jooby.Env; import org.jooby.Env.ServiceKey; import org.jooby.Registry; import org.jooby.Route; import org.jooby.internal.hbm.GuiceBeanManager; import org.jooby.internal.hbm.OpenSessionInView; import org.jooby.internal.hbm.ScanEnvImpl; import org.jooby.internal.hbm.SessionProvider; import org.jooby.internal.hbm.UnitOfWorkProvider; import org.jooby.jdbc.Jdbc; import com.google.inject.Binder; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import javaslang.concurrent.Promise; /** * <h1>hibernate</h1> * <p> * <a href="http://hibernate.org/orm">Hibernate ORM</a> enables developers to more easily write * applications whose data outlives the application process. As an Object/Relational Mapping (ORM) * framework, Hibernate is concerned with data persistence as it applies to relational databases. * </p> * <p> * This module setup and configure <a href="http://hibernate.org/orm">Hibernate ORM</a> and * <code>JPA Provider</code>. * </p> * * <p> * This module depends on {@link Jdbc} module, make sure you read the doc of the {@link Jdbc} * module. * </p> * * <h2>exports</h2> * <ul> * <li>SessionFactory / EntityManagerFactory</li> * <li>Session / EntityManager</li> * <li>UnitOfWork</li> * </ul> * * <h2>usage</h2> * * <pre>{@code * { * use(new Hbm("jdbc:mysql://localhost/mydb") * .classes(Beer.class) * ); * * get("/api/beer/", req -> { * return require(UnitOfWork.class).apply(em -> { * return em.createQuery("from Beer").getResultList(); * }); * }); * } * }</pre> * * <h2>unit of work</h2> * <p> * We provide an {@link UnitOfWork} to simplify the amount of code required to interact within the * database. * </p> * <p> * For example the next line: * </p> * * <pre>{@code * { * require(UnitOfWork.class).apply(em -> { * return em.createQuery("from Beer").getResultList(); * }); * } * }</pre> * * <p> * Is the same as: * </p> * * <pre>{@code * { * Session session = require(SessionFactory.class).openSession(); * Transaction trx = session.getTransaction(); * try { * trx.begin(); * List<Beer> beers = em.createQuery("from Beer").getResultList(); * trx.commit(); * } catch (Exception ex) { * trx.rollback(); * } finally { * session.close(); * } * } * }</pre> * * <p> * An {@link UnitOfWork} takes care of transactions and session life-cycle. It's worth to mention * too that a first requested {@link UnitOfWork} bind the Session to the current request. If later * in the execution flow an {@link UnitOfWork}, {@link Session} and/or {@link EntityManager} is * required then the one that belong to the current request (first requested) will be provided it. * </p> * * <h2>open session in view</h2> * <p> * We provide an advanced and recommended <a href= * "https://developer.jboss.org/wiki/OpenSessionInView#jive_content_id_Can_I_use_two_transactions_in_one_Session" * >Open Session in View</a> pattern, which basically keep the {@link Session} opened until the view * is rendered, but it uses two database transactions: * </p> * * <ol> * <li>first transaction is committed before rendering the view and then</li> * <li>a read only transaction is opened for rendering the view</li> * </ol> * * <p> * Here is an example on how to setup the open session in view filter: * </p> * * <pre>{@code * { * use(new Hbm()); * * use("*", Hbm.openSessionInView()); * } * }</pre> * * <h2>event listeners</h2> * <p> * JPA event listeners are provided by Guice, which means you can inject dependencies into your * event * listeners: * </p> * * <pre>{@code * * @Entity * @EntityListeners({BeerListener.class}) * public class Beer { * * } * * public class BeerListener { * @Inject * public BeerListener(DependencyA depA) { * this.depA = depA; * } * * @PostLoad * public void postLoad(Beer beer) { * this.depA.complementBeer(beer); * } * } * * }</pre> * * <p> * Hibernate event listeners are supported too via {@link #onEvent(EventType, Class)}: * </p> * * <pre>{@code * { * use(new Hbm() * .onEvent(EventType.POST_LOAD, MyPostLoadListener.class)); * } * }</pre> * * <p> * Again, <code>MyPostLoadListener</code> will be provided by Guice. * </p> * * <h2>persistent classes</h2> * <p> * Persistent classes must be provided at application startup time via * {@link #classes(Class...)}: * </p> * * <pre>{@code * { * use(new Hbm() * .classes(Entity1.class, Entity2.class, ..., ) * ); * } * }</pre> * * <p> * Or via {@link #scan()}: * </p> * * <pre>{@code * { * use(new Hbm() * .scan() * ); * } * }</pre> * * <p> * Which <code>scan</code> the application package, or you can provide where to look: * <p> * * <pre>{@code * { * use(new Hbm() * .scan("foo.bar", "x.y.z") * ); * } * }</pre> * * <h2>advanced configuration</h2> * <p> * Advanced configuration is provided via {@link #doWith(Consumer)} callbacks: * </p> * <pre>{@code * { * use(new Hbm() * .doWith((BootstrapServiceRegistryBuilder bsrb) -> { * // do with bsrb * }) * .doWith((StandardServiceRegistryBuilder ssrb) -> { * // do with ssrb * }) * ); * } * }</pre> * * <p> * Or via <code>hibernate.*</code> property from your <code>.conf</code> file: * </p> * * <pre>{@code * hibernate.hbm2ddl.auto = update * }</pre> * * * <h2>life-cycle</h2> * <p> * You are free to inject a {@link SessionFactory} or {@link EntityManagerFactory} create a new * {@link EntityManagerFactory#createEntityManager()}, start transactions and do everything you * need. * </p> * * <p> * For the time being, this doesn't work for a {@link Session} or {@link EntityManager}. A * {@link Session} {@link EntityManager} is bound to the current request, which means you can't * freely access from every single thread (like manually started thread, started by an executor * service, quartz, etc...). * </p> * * <p> * Another restriction, is the access from {@link Singleton} services. If you need access from a * singleton services, you need to inject a {@link Provider}. * </p> * * <pre> * * @Singleton * public class MySingleton { * * @Inject * public MySingleton(Provider<EntityManager> em) { * this.em = em; * } * } * </pre> * * <p> * Still, we strongly recommend to leave your services in the default scope and avoid to use * {@link Singleton} objects, except of course for really expensive resources. This is also * recommend it by Guice. * </p> * * <p> * Services in the default scope won't have this problem and are free to inject the * {@link Session} or {@link EntityManager} directly. * </p> * * @author edgar * @since 1.0.0.CR7 */ public class Hbm extends Jdbc { private List<BiConsumer<SessionFactoryImplementor, Registry>> listeners = new ArrayList<>(); private List<Consumer<Binder>> bindings = new ArrayList<>(); private List<BiConsumer<MetadataSources, Config>> sources = new ArrayList<>(); /** * Creates a new {@link Hbm} module. * * @param db A jdbc connection string or a property with a jdbc connection string. */ public Hbm(final String db) { super(db); } /** * Creates a new {@link Hbm} module. A <code>db</code> property must be present in your * <code>.conf</code> file. */ public Hbm() { } /** * Append persistent classes (classess annotated with Entity). * * @param classes Persistent classes. * @return This module. */ @SuppressWarnings("rawtypes") public Hbm classes(final Class... classes) { sources.add((m, c) -> Arrays.asList(classes).stream().forEach(m::addAnnotatedClass)); return this; } /** * Scan the provided packages and discover persistent classes (annotated with Entity). * * @param packages Package to scan. * @return This module. */ public Hbm scan(final String... packages) { sources.add((m, c) -> Arrays.asList(packages).stream().forEach(m::addPackage)); return this; } /** * Scan the application package (that's the package where you application was defined) and * discover persistent classes (annotated with Entity). * * @return This module. */ public Hbm scan() { sources.add((m, c) -> m.addPackage(c.getString("application.ns"))); return this; } /** * Creates an open session in view filter as described <a href= * "https://developer.jboss.org/wiki/OpenSessionInView#jive_content_id_Can_I_use_two_transactions_in_one_Session">here</a>. * * Please note a call to this method give you only the filter, you must add it to your application * like: * * <pre>{@code * { * use(new Hbm()); * * use("*", Hbm.openSessionInView()); * } * }</pre> * * @return This module. */ public static Route.Filter openSessionInView() { return new OpenSessionInView(); } /** * Register an hibernate event listener. Listener will be created and injected by Guice. * * @param type Event type. * @param listenerType Listener type. * @return This module. */ @SuppressWarnings("unchecked") public <T> Hbm onEvent(final EventType<T> type, final Class<? extends T> listenerType) { bindings.add(b -> { b.bind(listenerType).asEagerSingleton(); }); listeners.add((s, r) -> { ServiceRegistryImplementor serviceRegistry = s.getServiceRegistry(); EventListenerRegistry service = serviceRegistry.getService(EventListenerRegistry.class); T listener = r.require(listenerType); service.appendListeners(type, listener); }); return this; } /** * Configurer callback to apply advanced configuration while bootstrapping hibernate: * * <pre>{@code * { * use(new Hbm() * .doWith((BootstrapServiceRegistryBuilder bsrb, Config conf) -> { * // do with bsrb * }) * .doWith((StandardServiceRegistryBuilder ssrb, Config conf) -> { * // do with ssrb * }) * ); * } * }</pre> * * @param configurer Configurer callback. * @return This module */ @Override public <T> Hbm doWith(final BiConsumer<T, Config> configurer) { super.doWith(configurer); return this; } /** * Configurer callback to apply advanced configuration while bootstrapping hibernate: * * <pre>{@code * { * use(new Hbm() * .doWith((BootstrapServiceRegistryBuilder bsrb) -> { * // do with bsrb * }) * .doWith((StandardServiceRegistryBuilder ssrb) -> { * // do with ssrb * }) * ); * } * }</pre> * * @param configurer Configurer callback. * @return This module */ @Override public <T> Hbm doWith(final Consumer<T> configurer) { super.doWith(configurer); return this; } @Override public void configure(final Env env, final Config conf, final Binder binder) { super.configure(env, conf, binder, (name, ds) -> { BootstrapServiceRegistryBuilder bsrb = new BootstrapServiceRegistryBuilder(); bsrb.applyIntegrator(new JpaIntegrator()); callback(bsrb, conf); String ddl_auto = env.name().equals("dev") ? "update" : "none"; BootstrapServiceRegistry bsr = bsrb.build(); StandardServiceRegistryBuilder ssrb = new StandardServiceRegistryBuilder(bsr); ssrb.applySetting(AvailableSettings.HBM2DDL_AUTO, ddl_auto); ssrb.applySettings(settings(env, conf)); callback(ssrb, conf); ssrb.applySetting(AvailableSettings.DATASOURCE, ds); ssrb.applySetting(org.hibernate.jpa.AvailableSettings.DELAY_CDI_ACCESS, true); StandardServiceRegistry serviceRegistry = ssrb.build(); MetadataSources sources = new MetadataSources(serviceRegistry); this.sources.forEach(src -> src.accept(sources, conf)); callback(sources, conf); /** scan package? */ List<URL> packages = sources.getAnnotatedPackages() .stream() .map(pkg -> getClass().getResource("/" + pkg.replace('.', '/'))) .collect(Collectors.toList()); Metadata metadata = sources.getMetadataBuilder() .applyImplicitNamingStrategy(ImplicitNamingStrategyJpaCompliantImpl.INSTANCE) .applyScanEnvironment(new ScanEnvImpl(packages)) .build(); SessionFactoryBuilder sfb = metadata.getSessionFactoryBuilder(); callback(sfb, conf); sfb.applyName(name); Promise<Registry> registry = Promise.make(); sfb.applyBeanManager(GuiceBeanManager.beanManager(registry)); SessionFactory sessionFactory = sfb.build(); callback(sessionFactory, conf); Provider<Session> session = new SessionProvider(sessionFactory); ServiceKey serviceKey = env.serviceKey(); serviceKey.generate(SessionFactory.class, name, k -> binder.bind(k).toInstance(sessionFactory)); serviceKey.generate(EntityManagerFactory.class, name, k -> binder.bind(k).toInstance(sessionFactory)); /** Session/Entity Manager . */ serviceKey.generate(Session.class, name, k -> binder.bind(k).toProvider(session)); serviceKey.generate(EntityManager.class, name, k -> binder.bind(k).toProvider(session)); /** Unit of work . */ Provider<UnitOfWork> uow = new UnitOfWorkProvider(sessionFactory); serviceKey.generate(UnitOfWork.class, name, k -> binder.bind(k).toProvider(uow)); bindings.forEach(it -> it.accept(binder)); env.onStart(r -> { registry.success(r); listeners.forEach(it -> it.accept((SessionFactoryImplementor) sessionFactory, r)); }); env.onStop(sessionFactory::close); }); } @Override public Config config() { return ConfigFactory.parseResources(getClass(), "hbm.conf").withFallback(super.config()); } private static Map<Object, Object> settings(final Env env, final Config config) { Map<Object, Object> $ = new HashMap<>(); config.getConfig("hibernate") .entrySet() .forEach(e -> $.put("hibernate." + e.getKey(), e.getValue().unwrapped())); return $; } }