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