Multi-Tenancy with Hibernate (Moved from general)

Hi guys. I’m new here. I’m trying to implement muti-tenancy using Hibernate, but when I deploy the app to Payara server, an exception occured: java.lang.ClassCastException: com.sun.enterprise.container.common.impl.EntityManagerFactoryWrapper cannot be cast to org.hibernate.engine.spi.SessionFactoryImplementor
Below is my code.
DatabaseMultiTenantProvider.java

public class DatabaseMultiTenantProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseMultiTenantProvider.class);
    private static final long serialVersionUID = 1L;
    private static final String TENANT_SUPPORTED = "DATABASE";
    private DataSource dataSource;
    private String typeTenancy;

    @Override
    public boolean supportsAggressiveRelease() {
        return false;
    }

    @Override
    public void injectServices(ServiceRegistryImplementor serviceRegistry) {

        typeTenancy = (String) ((ConfigurationService) serviceRegistry
                .getService(ConfigurationService.class))
                .getSettings().get("hibernate.multiTenancy");

        dataSource = (DataSource) ((ConfigurationService) serviceRegistry
                .getService(ConfigurationService.class))
                .getSettings().get("hibernate.connection.datasource");

    }

    @SuppressWarnings("rawtypes")
    @Override
    public boolean isUnwrappableAs(Class clazz) {
        return false;
    }

    @Override
    public <T> T unwrap(Class<T> clazz) {
        return null;
    }

    @Override
    public Connection getAnyConnection() throws SQLException {
        final Connection connection = dataSource.getConnection();
        return connection;

    }

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        //Just use the multitenancy if the hibernate.multiTenancy == DATABASE
        if (TENANT_SUPPORTED.equals(typeTenancy)) {
            try {
                Properties props = FileIO.loadProperties(DatabaseMultiTenantProvider.class.getClassLoader().getResource("config/dbconfig.properties").getFile());
                String host = props.getProperty("hq.host");
                String port = props.getProperty("hq.port");
                String user = props.getProperty("hq.user");
                String password = props.getProperty("hq.password");
                MysqlDataSource mysqlDataSource = new MysqlDataSource();
                String DB_URL = "jdbc:mysql://" + host + ":" + port + "/" + tenantIdentifier + "?serverTimezone=UTC&user=" + user + "&password=" + password;
                mysqlDataSource.setURL(DB_URL);
                dataSource = mysqlDataSource;
            } catch (IOException ex) {
                LOGGER.error("Could not load database configuration file. {}", ex);
            }
        }
        return dataSource.getConnection();
    }

    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        connection.close();
    }

    @Override
    public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
        releaseAnyConnection(connection);
    }
}

DatabaseTenantResolver.java

public class DatabaseTenantResolver extends MultitenancyResolver {

    private final Map<String, String> userDatasourceMap;

    public DatabaseTenantResolver() {
        userDatasourceMap = new HashMap();
        userDatasourceMap.put("default", "ibigtime");
        userDatasourceMap.put("bigtime", "bigtime");
    }

    @Override
    public String resolveCurrentTenantIdentifier() {
        if (this.tenantIdentifier != null && userDatasourceMap.containsKey(this.tenantIdentifier)) {
            return userDatasourceMap.get(this.tenantIdentifier);
        }
        return userDatasourceMap.get("default");
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return false;
    }
}

MultitenancyResolver.java

public abstract class MultitenancyResolver implements CurrentTenantIdentifierResolver {

    protected String tenantIdentifier;

    public void setTenantIdentifier(String tenantIdentifier) {
        this.tenantIdentifier = tenantIdentifier;
    }
}

Dao.java

public abstract class Dao<T extends Entity> {

    @PersistenceUnit
    private EntityManagerFactory emf;

    protected EntityManager getEntityManager(String multitenancyIdentifier) {
        final MultitenancyResolver tenantResolver = (MultitenancyResolver) ((SessionFactoryImplementor) emf).getCurrentTenantIdentifierResolver();
        tenantResolver.setTenantIdentifier(multitenancyIdentifier);
        return emf.createEntityManager();
    }

    public Optional<T> findById(T entity, String multitenancyIdentifier) {
        return (Optional<T>) Optional
                .ofNullable(getEntityManager(multitenancyIdentifier).find(entity.getClass(), entity.getId()));
    }

    public T save(T entity, String multitenancyIdentifier) {
        getEntityManager(multitenancyIdentifier).persist(entity);
        return entity;
    }

    public T update(T entity, String multitenancyIdentifier) {
        getEntityManager(multitenancyIdentifier).merge(entity);
        return entity;
    }

    public void remove(T entity, String multitenancyIdentifier) {
        getEntityManager(multitenancyIdentifier).remove(entity);
    }
}

The exception is occured when we cast EntityManagerFactory to SessionFactoryImplementor in the method getEntityManager() of the Dao.java file.
I follow the code from the article: https://developers.redhat.com/blog/2020/06/15/jakarta-ee-multitenancy-with-jpa-on-wildfly-part-1# but It doesn’t work on Payara Server. Could anyone tell me how to work around this problem? Thanks

Hi,

Basically Payara provides the Eclipselink as a persistence provider. (please refer to Bill Of Material Artifact[1]). The if we prefer to use the Hibernate instead we may have to mention it at our persistence.xml as the following example: -

<persistence version=...>
    ....
    <persistence-unit name="...." transaction-type="....">
        <!-- 
            For hibernate 4.3 and above use `org.hibernate.jpa.HibernatePersistenceProvider`
            For hibernate 4.2 use `org.hibernate.ejb.HibernatePersistence`
         -->
        <provider><!-- select one from the above comment --></provider>
                ....
    </persistence-unit>
</persistence>

[1] Bill Of Material Artifact :: Payara Community Documentation

Here is my persistance.xml. I have specified persistence provider to be Hibernate, but the error still occurred. I asked this question in the Hibernate forum and got the answer that there are some bugs in implementation here. So that this is problem in Payara side, a not Hibernate side. I change the server to Wildfly and the app works. https://github.com/payara/Payara/blob/master/appserver/common/container-common/src/main/java/com/sun/enterprise/container/common/impl/EntityManagerFactoryWrapper.java#L192

<persistence>
    <persistence-unit name="ibigtime">

        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <property name="javax.persistence.schema-generation.database.action" value="none" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
            <property name="hibernate.multiTenancy" value="DATABASE"/>
            <property name="hibernate.tenant_identifier_resolver" value="com.spf.dao.multitenancy.DatabaseTenantResolver"/>
            <property name="hibernate.multi_tenant_connection_provider" value="com.spf.dao.multitenancy.DatabaseMultiTenantProvider"/>
        </properties>

    </persistence-unit>
</persistence>

Shall we try the Enhanced Classloading [1] via the glassfish-web.xml [2] ?

<!-- /WEB-INF/glassfish-web.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
  ...
  <class-loader delegate="false"/>
  ...
</glassfish-web-app>

Regarding to this approach, Payara will load the jar files by using the following order: -

First, libraries on WAR applications (included on WEB-INF/lib)
Then, libraries on EAR applications (included on /lib)
Then, libraries from the domain (located at ${DOMAIN_DIR}/lib)
Finally, libraries from the server (located at ${PAYARA_INSTALL_DIR}/modules_)

Then the hibernate related jar files should reside at our WEB-INF/lib.

[1] Enhanced Classloading :: Payara Enterprise Documentation
[2] Payara Server Deployment Descriptor Files :: Payara Community Documentation
[3] Payara Schemas :: Payara Community Documentation

Furthermore I found the Using Hibernate 5 on Payara Server [1] which may help us to achieve.

The significant may be the following: -

<persistence version="...>
    <persistence-unit name="..." transaction-type="JTA">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <jta-data-source>...</jta-data-source>
        <properties>
            ...
            <!-- here, may be significant property -->
            <property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/>
        </properties>
    </persistence-unit>
</persistence>

[1] Using Hibernate 5 on Payara Server

@charlee_ch : I added glassfish-web.xml inside /WEB-INF/ with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
  <class-loader delegate="false"/>
</glassfish-web-app>

I also added <property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform"/> to persistence.xml. After building and packaging, inside WEB-INF/lib there are 2 hibernate related jars: hibernate-commons-annotations-5.1.2.Final and hibernate-core-5.5.7.Final. So that, Payara loaded hibernate properly. But when I deployed the app, the error still occured.

Let’s step back a bit. I found that there is an example for getting the Hibernate SessionFactoryImplementor from EntityManagerFactory at Hibernate ORM 5.5.8.Final User Guide [1] as the following: -

protected EntityManager getEntityManager(String multitenancyIdentifier) {

    // Use `unwrap` instead of type casting.
    final SessionFactoryImplementor sessionFactory = 
        emf.unwrap( SessionFactoryImplementor.class );

    // Then the rest may be as the following.
    final MultitenancyResolver tenantResolver = 
        (MultitenancyResolver) sessionFactory .
                                   getCurrentTenantIdentifierResolver();

    tenantResolver.setTenantIdentifier(multitenancyIdentifier);
    return emf.createEntityManager();
}

[1] Hibernate ORM 5.5.8.Final User Guide

Hi,

The problem is in this conversion:

(SessionFactoryImplementor) emf

This isn’t portable. You can’t just convert standard JPA interface like EntityManagerFactory to a non-standard class and expect it will always work. You should instead use the unwrap method to do the conversion:

emf.unwrap(SessionFactoryImplementor.class)

This will properly convert the factory to SessionFactoryImplementor if you use Hibernate.

Payara Server doesn’t expose the Hibernate’s EntityManagerFactory directly. Instead, it creates its own factory wrapper that delegates to the Hibernate’s factory. When you call the unwrap() method, it will execute the unwrap() method on the Hibernate’s factory, which will cast itself to SessionFactoryImplementor. Here’s the code in Payara Server: Payara/EntityManagerFactoryWrapper.java at b263fb8b7eab9fee664cd8b0a9457ed3dc96cdd0 · payara/Payara · GitHub

I hope that helps,
Ondro

1 Like

@charlee_ch @ondromihalyi Now the code works. Thanks!