Search code examples
jpajakarta-eecdi

How to inject dynamic EntityManager into a Third Party Library


I have a library with some functionality that I want to reuse in other projects. My issue is that my service requires writing to the database. I would like for my library to use the datasource of the project that is inject my service.

Here is the minimal setup of my service

@Stateless
public class CustomService {
    //to be added in producer
    private EntityManager em;
    private Principal principal;

    //default constructor
    public CustomService() {}
    //custom constructor called in provider
    public CustomService(Principal p, EntityManager e) {
        principal = p;
        em = e;
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    @Transactional
    public CustomJPAObject createObject(...params...) {
       //create JPA Object
       em.persist(customObject);
       em.flush();
       return customObject;
    }

}

I created a Custom Annotation for overriding the datasource

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD, ElementType.METHOD})
public @interface DynamicDS {
    @Nonbinding String value() default "";

}

I also created a Singleton to be an EntityManager Producer

@Singleton
public class CustomEMProducer {
    private Map<String, EntityManagerFactory> emfMap = new HashMap<>();

    @Produces @Dependent @DynamicDS
    public EntityManager produceEntityManager(InjectionPoint injectionPoint) {
        String dataSourceName = null;
        for(Annotation qualifier: injectionPoint.getQualifiers()) {
            if(qualifier instanceof DynamicDS) {
                DynamicDS dds = (DynamicDS) qualifier;
                dataSourceName = dds.value();
                break;
            }
        }
        EntityManagerFactory emf = emfMap.get(dataSourceName);
        if (emf == null) {
            emf = Persistence.createEntityManagerFactory(dataSourceName);
            emfMap.put(dataSourceName, emf);
        }
        return emf.createEntityManager();
    }

    @PostConstruct
    public void cleanup() {
        emfMap.entrySet().stream().forEach(entry -> entry.getValue().close());
    }
}

Here is the code for my Service Producer

@Stateless
public class CustomServiceProvider {
    @Inject private Principal principal;

    @Produces @Dependent @DynamicDS
    public BackgroundJobService getBackgroundJobService(InjectionPoint injectionPoint) throws EntityManagerNotCreatedException {
        Annotation dsAnnotation = null;
        for(Annotation qualifier: injectionPoint.getQualifiers()) {
            if(qualifier instanceof BackgroundJobDS) {
                dsAnnotation = qualifier;
                break;
            }
        }
        if (dsAnnotation != null) {
            EntityManager em = CDI.current().select(EntityManager.class, dsAnnotation).get();
            CustomService service = new CustomService(principal, em);
            return service;
        }
        throw new EntityManagerNotCreatedException("Could not Produce CustomService");
    }
}

The following is where I try to inject my new service

@Stateless
public class ProjectService {
    @Inject @DynamicDS("project-ds") CustomerService service;

    public CustomObject create(...params...) {
        return service.createObject(...params...);
    }
}

When I deploy my code and attempt to call the injected service I get the following error:

Caused by: javax.persistence.TransactionRequiredException: no transaction is in progress
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.checkTransactionNeeded(AbstractEntityManagerImpl.java:1171)
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:1332)
    ...

It looks like all of the different levels of providers are preventing the @Transactional on the CustomService.createObject() method call from propagating the transaction. Does anyone have insight into why this is or an alternate way of accomplishing my goal of injecting a dynamic EntityManager?


Solution

  • After much experimenting, I was unable to get dynamically generate an EntityManager through the above code. After much research, I gave up on trying to pass in the name from outside the 3rd part library. I would up creating the following interface:

    public interface CustomEntityManager {
        EntityManager getEntityManager();
    }
    

    This meant that inside the project that uses the 3rd party service I can do the create the following implementation to inject the EntityManager

    public ProjectSpecificEntityManager implements CustomEntityManager {
        @PersistenceContext(unitname = "project-ds")
        private EntityManager em;
    
        public EntityManager getEntityManager() {
            return em;
        }
    }
    

    I had to update my CustomService to the following

    @Stateless
    public class CustomService {
       //Ignore warning about no bean eligible because it is intended 
       //that the project that uses this library will provide the
       //implementation
       @SuppressWarnings("cdi-ambiguous-dependency")
       @Inject
       CustomEntityManager cem;
    
       @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
       @Transactional
       public CustomJPAObject createObject(...params...) {
           //create JPA Object
           cem.getEntityManager().persist(customObject);
           return customObject;
       }
    }