EclipseLink @Multitenant EntityManager/Transaction handling

635 views Asked by At

I create a web application with the requirement of multi tenancy(single-table). The @Multitenant feature looks very interesting, but I don't know how to handle it correctly. I use the following components:

  • WildFly 8.2 Final
  • EclipseLink v2.6
  • Apache Shiro v1.2.3

Each request to the application is assigned to a user and a particular customer. The tenant id can I read from Apache Shiro and set the tenant.id property to create the EntityManager (Application-Managed).

The first approach is to use EntityTransaction in a ServletFilter with the EntityManager-per-request pattern and RESSOURCE_LOCAL transactions (here without exception handling):

public class EntityManagerServletFilter implements Filter {

private EntityManagerFactory entityManagerFactory;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
    entityManagerFactory = Persistence.createEntityManagerFactory("MESaaS");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    Session session = SecurityUtils.getSubject().getSession();
    Long tenantId = null;
    if(session != null) {
        tenantId = (Long) session.getAttribute("tenantId");
    }
    EntityManager em = entityManagerFactory.createEntityManager();
    em.setProperty("tenant.id", tenantId);
    EntityTransaction trn = em.getTransaction();
    trn.begin();

    EntityManagerProvider.setCurrentEntityManager(em);

    chain.doFilter(request, response);

    trn.commit();
}

@Override
public void destroy() {
    entityManagerFactory = null;
}

}

This approach is not working and throws the following exception:

Exception Description: An exception was thrown when trying to get a primary key class instance.
Internal Exception: java.lang.InstantiationException: java.lang.Long
Descriptor: RelationalDescriptor(ch.hauserag.mesaas.entities.Hierarchy --> [DatabaseTable(HIERARCHY)])
 at org.apache.shiro.web.servlet.AdviceFilter.cleanup(AdviceFilter.java:196) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.filter.authc.AuthenticatingFilter.cleanup(AuthenticatingFilter.java:155) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:148) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90) [shiro-core-1.2.3.jar:1.2.3]
 at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83) [shiro-core-1.2.3.jar:1.2.3]
 at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383) [shiro-core-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) [shiro-web-1.2.3.jar:1.2.3]
 at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:60) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:132) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:85) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:61) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
 at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:56) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.security.handlers.AuthenticationConstraintHandler.handleRequest(AuthenticationConstraintHandler.java:51) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:45) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:63) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.security.ServletSecurityConstraintHandler.handleRequest(ServletSecurityConstraintHandler.java:56) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:58) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:70) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.security.handlers.SecurityInitialHandler.handleRequest(SecurityInitialHandler.java:76) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
 at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:261) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:247) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:76) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:166) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.server.Connectors.executeRootHandler(Connectors.java:197) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:759) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [rt.jar:1.8.0_40]
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [rt.jar:1.8.0_40]
 at java.lang.Thread.run(Thread.java:745) [rt.jar:1.8.0_40]
Caused by: javax.persistence.RollbackException: Exception [EclipseLink-222] (Eclipse Persistence Services - 2.6.0.v20150309-bf26070): org.eclipse.persistence.exceptions.DescriptorException
Exception Description: An exception was thrown when trying to get a primary key class instance.
Internal Exception: java.lang.InstantiationException: java.lang.Long
Descriptor: RelationalDescriptor(ch.hauserag.mesaas.entities.Hierarchy --> [DatabaseTable(HIERARCHY)])
 at org.eclipse.persistence.internal.jpa.transaction.EntityTransactionImpl.commit(EntityTransactionImpl.java:159) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at ch.hauserag.mesaas.daos.EntityManagerServletFilter.doFilter(EntityManagerServletFilter.java:61) [classes:]
 at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:60) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:132) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
 at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108) [shiro-web-1.2.3.jar:1.2.3]
 at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137) [shiro-web-1.2.3.jar:1.2.3]
 ... 43 more
Caused by: Exception [EclipseLink-222] (Eclipse Persistence Services - 2.6.0.v20150309-bf26070): org.eclipse.persistence.exceptions.DescriptorException
Exception Description: An exception was thrown when trying to get a primary key class instance.
Internal Exception: java.lang.InstantiationException: java.lang.Long
Descriptor: RelationalDescriptor(ch.hauserag.mesaas.entities.Hierarchy --> [DatabaseTable(HIERARCHY)])
 at org.eclipse.persistence.exceptions.DescriptorException.exceptionAccessingPrimaryKeyInstance(DescriptorException.java:2084) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at org.eclipse.persistence.internal.jpa.CMP3Policy.getPKClassInstance(CMP3Policy.java:205) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at org.eclipse.persistence.descriptors.CMPPolicy.createPrimaryKeyInstance(CMPPolicy.java:441) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.updateDerivedIds(UnitOfWorkImpl.java:5445) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.calculateChanges(UnitOfWorkImpl.java:653) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.commitToDatabaseWithChangeSet(UnitOfWorkImpl.java:1516) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork.commitRootUnitOfWork(RepeatableWriteUnitOfWork.java:278) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.commitAndResume(UnitOfWorkImpl.java:1169) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 at org.eclipse.persistence.internal.jpa.transaction.EntityTransactionImpl.commit(EntityTransactionImpl.java:134) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 ... 49 more
Caused by: java.lang.InstantiationException: java.lang.Long
 at java.lang.Class.newInstance(Class.java:427) [rt.jar:1.8.0_40]
 at org.eclipse.persistence.internal.jpa.CMP3Policy.getPKClassInstance(CMP3Policy.java:201) [eclipselink-2.6.0.jar:2.6.0.v20150309-bf26070]
 ... 56 more
Caused by: java.lang.NoSuchMethodException: java.lang.Long.<init>()
 at java.lang.Class.getConstructor0(Class.java:3082) [rt.jar:1.8.0_40]
 at java.lang.Class.newInstance(Class.java:412) [rt.jar:1.8.0_40]
 ... 57 more

But when I comment out the lines "trn.begin();" and "trn.commit();" everything works fine, except manipulations on the database are not persisted (as expected). The requests coming async and i think that is the problem(between begin() and commit(), EntityManager is not more the same).

Another Approach is to use a request scoped EntityManger and make use of JTA :

public class EntityManagerRequestScoped {

@PersistenceUnit(unitName="MESaaS")
EntityManagerFactory emf;

@Produces
@RequestScoped
public EntityManager getEntityManager() {
    Session session = SecurityUtils.getSubject().getSession();
    Long tenantId = null;
    if(session != null) {
        tenantId = (Long) session.getAttribute("tenantId");
    }
    EntityManager em = emf.createEntityManager();
    em.setProperty("tenant.id", tenantId);
    return em;      
}

}

And my abstract DAO looks like:

public abstract class AbstractDAO<T> {

@Inject
EntityManager em;

abstract public Class<T> getEntityClass();

protected EntityManager getEntityManager() {
    return em;
}

public T createOrUpdate(T obj) {
    T result = getEntityManager().merge(obj);
    return result;
}

public T getById(long id) {
    T result = getEntityManager().find(getEntityClass(), id);
    return result;
}

public List<T> getAll() {
    TypedQuery<T> q = getEntityManager().createQuery("SELECT o FROM "
            + getTableName() + " o", getEntityClass());
    List<T> result = q.getResultList();
    return result;
}

public void delete(long id) {
    getEntityManager().remove(getById(id));
}

}

And a concrete DAO looks like:

public class UserDAO extends AbstractDAO<User> {

@Override
public Class<User> getEntityClass() {
    return User.class;
}

public User getUserByEmail(String email) {
    String query =  "SELECT u FROM " + getTableName() + " u " +
                    "WHERE u.email =:email";
    TypedQuery<User> q = getEntityManager().createQuery(query, User.class);
    q.setParameter("email", email);
    User user = q.getResultList().isEmpty() ? null : q.getSingleResult();
    return user;
}

}

This DAO's are then injected in a service layer:

public class AdministrationService {

@Inject
UserDAO userDao;

@Inject
TenantDAO tenantDao;

@Inject
RoleDAO roleDao;

@Inject
Subject subject;

@Inject
TenantResolver<Long> currentTenant;

public List<User> getAllUsers() {
    return userDao.getAll();
}

public User getUserById(long id) {
    return userDao.getById(id);
}

public User getUserByEmailForLogin(String email, long tenantId) {
    return userDao.getUserByEmail(email, tenantId);
}

public User getUserByEmail(String email) {
    return userDao.getUserByEmail(email);
}

public Tenant getTenantByEmail(String email) {
    return tenantDao.getTenantByUserName(email);
}

public List<Tenant> getAllTenants() {
    return tenantDao.getAll();
}

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void insertUser(User user) {
    RandomNumberGenerator rng = new SecureRandomNumberGenerator();
    ByteSource salt = rng.nextBytes();
    String hashedPasswordBase64 = new Sha256Hash(user.getPassword(), salt, 1024).toBase64();

    user.setActive(true);
    user.setPassword(hashedPasswordBase64);
    user.setSalt(salt.toString());

    Tenant t = tenantDao.getById(currentTenant.getCurrentTenantId());
    user.setTenant(t);

    userDao.createOrUpdate(user);
}

public void deleteUser(long id) {
    userDao.delete(id);
}

}

This service is then injected in a REST facade:

@Path("/administration/")
public class AdministrationRESTService {

private static final Logger LOG = LoggerFactory.getLogger(AdministrationRESTService.class);

@Inject
private Subject subject;

@Inject
private AdministrationService administrationService;

@Inject
private TenantResolver<Long> tenantResolver;

@GET
@Path("user")
@Produces("application/json")
public Response getUsers() {
    List<User> users = administrationService.getAllUsers();
    return Response.ok(DTOFactory.getUserDTOs(users)).build();
}

@GET
@Path("user/{id}")
@Produces("application/json")
public Response getUser(@PathParam("id") long id) {
    User user = administrationService.getUserById(id);
    return Response.ok(DTOFactory.getUserDTO(user)).build();
}

@POST
@Path("user")
@Consumes("application/json")
@Produces("application/json")
public Response createUser(UserDTO user) {
    if(Validator.validateUser(user)) {
        User u = DTOFactory.getUserEntity(user);
        administrationService.insertUser(u);
        return Response.status(Status.OK).build();
    }
    return Response.status(Status.BAD_REQUEST).build();
}

@DELETE
@Path("user/{id}")
@Produces("application/json")
public Response deleteUser(@PathParam("id") long id) {
    administrationService.deleteUser(id);
    return Response.status(Status.OK).build();
}

}

I have read that the EntityManager can join a JTA transaction with em.joinTransaction(). But how and where can i handle the join and the lifecycle of a application managed EntityManager? As far as I know the annotation @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) is container managed, but I don't know how to handle it.

For any assistance I am very grateful. Kind regards Lorenz

0

There are 0 answers