JPA EntityManager not working when using Guice's PrivateModule

Viewed 371

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.

1 Answers

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.

Related