Search code examples
javajpadependency-injectionormguice

JPA EntityManager not working when using Guice's PrivateModule


I have a service with a persistence setup using JPA, Hibernate and Guice (if it's useful, I'm not using Spring). This is the first, working version of my code:

public class BookDao {

    @Inject
    protected Provider<EntityManager> entityManagerProvider;

    protected EntityManager getEntityManager() {
        return entityManagerProvider.get();
    }

    @Transactional
    public void persist(Book book) {
        getEntityManager().persist(book);
    }

}

public class MyAppModule extends AbstractModule {

    @Override
    protected void configure() {
        initializePersistence();
    }

    private void initializePersistence() {
        final JpaPersistModule jpaPersistModule = new JpaPersistModule("prod");
        jpaPersistModule.properties(new Properties());
        install(jpaPersistModule);
    }

}

But now I need to configure multiple persistence units. I'm following the advice in this mailing list, and according to them, I should move my module logic to a private module. I did as suggested and created a second version of the same code, the changes are commented below:

@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD, PARAMETER, METHOD })
public @interface ProductionDataSource {} // defined this new annotation

public class BookDao {

    @Inject
    @ProductionDataSource // added the annotation here
    protected Provider<EntityManager> entityManagerProvider;

    protected EntityManager getEntityManager() {
        return entityManagerProvider.get();
    }

    @Transactional
    public void persist(Book book) throws Exception {
        getEntityManager().persist(book);
    }

}

public class MyAppModule extends PrivateModule { // module is now private

    @Override
    protected void configure() {
        initializePersistence();
        // expose the annotated entity manager
        Provider<EntityManager> entityManagerProvider = binder().getProvider(EntityManager.class);
        bind(EntityManager.class).annotatedWith(ProductionDataSource.class).toProvider(entityManagerProvider);
        expose(EntityManager.class).annotatedWith(ProductionDataSource.class);

    }

    private void initializePersistence() {
        JpaPersistModule jpaPersistModule = new JpaPersistModule("prod");
        jpaPersistModule.properties(new Properties());
        install(jpaPersistModule);
    }

}

The newly annotated EntityManager is being correctly injected by Guice and is non-null, but here's the fun part: some of my unit tests started failing, for example:

class BookDaoTest {

    private Injector injector;
    private BookDao testee;

    @BeforeEach
    public void setup() {
        injector = Guice.createInjector(new MyAppModule());
        injector.injectMembers(this);
        testee = injector.getInstance(BookDao.class);
    }

    @Test
    public void testPersistBook() throws Exception {
        // given
        Book newBook = new Book();
        assertNull(newBook.getId());
        // when
        newBook = testee.persist(newBook);
        // then
        assertNotNull(newBook.getId()); // works in the first version, fails in the second
    }

}

In the first version of my code the last line above just works: the entity is persisted and has a new id. However, in the second version of my code (using a PrivateModule and exposing an annotated EntityManager from it) the persist() operation doesn't work anymore, the entity is without an id. What could be the problem? I didn't do any other configuration changes in my environment, and I don't see error messages in the logs. Let me know if you need more details.


Solution

  • It turns out that the problem was the @Transactional annotation. In the first version of my code, Guice automatically adds interceptors for managing the transaction. By doing a debug, I found out that before executing my persist(Book book) method, Guice calls the following method from the com.google.inject.internal.InterceptorStackCallback package:

    public Object intercept(Object proxy, Method method, Object[] arguments, MethodProxy methodProxy)
    

    In the second version of my code, when I exposed the persistence unit from a private module the above interceptor was no longer called, leaving my persist operation without transaction handling. This is a known issue and is by design.

    As a workaround I had to implement transactions by hand, making my code more verbose. I also had to change the way the entity manager is injected. This solution worked for me:

    public class BookDao {
    
        @Inject
        @Named(PROD_PERSISTENCE_UNIT_NAME)
        private EntityManagerFactory entityManagerFactory;
    
        private EntityManager getEntityManager() {
            return entityManagerFactory.createEntityManager();
        }
    
        public void persist(Book book) throws Exception {
            EntityManager em = getEntityManager();
            try {
                em.getTransaction().begin();
                em.persist(book);
                em.getTransaction().commit();
            } catch (Exception e) {
                em.getTransaction().rollback();
                throw e;
            } finally {
                em.close();
            }
        }
    
    }
    
    public class MyAppModule extends PrivateModule {
    
        public static final String PROD_PERSISTENCE_UNIT_NAME = "prod";
    
        @Override
        protected void configure() {
            initializePersistence();
        }
    
        private void initializePersistence() {
            // persistence unit set to prod DB
            final JpaPersistModule jpaPersistModule = new JpaPersistModule(PROD_PERSISTENCE_UNIT_NAME);
            // connection properties set to suitable prod values
            jpaPersistModule.properties(new Properties());
            install(jpaPersistModule);
            // expose bindings to entity manager annotated as "prod"
            bind(JPAInitializer.class).asEagerSingleton();
            bind(PersistService.class).annotatedWith(named(PROD_PERSISTENCE_UNIT_NAME)).to(PersistService.class).asEagerSingleton();
            expose(PersistService.class).annotatedWith(named(PROD_PERSISTENCE_UNIT_NAME));
            bind(EntityManagerFactory.class).annotatedWith(named(PROD_PERSISTENCE_UNIT_NAME)).toProvider(binder().getProvider(EntityManagerFactory.class));
            expose(EntityManagerFactory.class).annotatedWith(named(PROD_PERSISTENCE_UNIT_NAME));
            bind(EntityManager.class).annotatedWith(named(PROD_PERSISTENCE_UNIT_NAME)).toProvider(binder().getProvider(EntityManager.class));
            expose(EntityManager.class).annotatedWith(named(PROD_PERSISTENCE_UNIT_NAME));
            bind(UnitOfWork.class).annotatedWith(named(PROD_PERSISTENCE_UNIT_NAME)).toProvider(binder().getProvider(UnitOfWork.class));
            expose(UnitOfWork.class).annotatedWith(named(PROD_PERSISTENCE_UNIT_NAME));
        }
    
    }
    

    As a lesson, be very watchful around annotations and other such "magic" that modifies your code under the hood, finding bugs becomes quite difficult.