使用JNDI进行Hibernate搜索



我使用Hibernate Search 6与多租户(请参阅Hibernate Search 6与多租户问题,HSEARCH000520, HSEARCH600029)。我的环境:Hibernate ORM 5.4.28, Hibernate Search 6.0.2, Payara server 2021.1, MariaDb。我配置了2个数据源(2个数据库)- myDS和my2ndDS。我可以使用下面的多租户解析器方法,通过成功引用租户id来查找/合并实体。我也将这种方法应用于搜索(参见下面的编码)。现在的问题是,当我搜索一些东西会显示下面的错误。

@PersistenceUnit
private EntityManagerFactory emf;
public EntityManager getEM(final String tenantId) {
final SessionFactoryImplementor sf = emf.unwrap(SessionFactoryImplementor.class);
final MultitenancyResolver tenantResolver = (MultitenancyResolver) sf.getCurrentTenantIdentifierResolver();
tenantResolver.setTenantIdentifier(tenantId);
return emf.createEntityManager();
}

hibernate搜索类/方法:

@Stateless
public class SearchAnnouncementMessage {
...
private EntityManager getEM(final String tenantId) {
...
} 
public ResultSearchObject searchAnnouncementMsgs(final String tenantId,
final boolean reindexWithHibernateSearch, final String searchWord,
final int[] range) {
....
final SearchSession searchSession = Search.session(getEM(tenantId));
if (reindexWithHibernateSearch) {
logger.info("Reindex with HibernateSearch");
try {
searchSession.massIndexer()
.idFetchSize(150)
.batchSizeToLoadObjects(25)
.threadsToLoadObjects(THREADS_LOAD_OBJ)
.transactionTimeout(SEARCH_TIMEOUT)
.startAndWait();
} catch (final InterruptedException e) {
logger.info("#1 can't search at this time; error: {}", () -> e.getMessage());
return null;
}
}
try {

logger.info("search in AnnouncementMsgs");
final SearchQuery<AnnouncementMsgs> result = searchSession.search(AnnouncementMsgs.class).extension(LuceneExtension.get())
.where(f -> f.bool(b -> { 
...
}
}

错误信息(show at line with Search.session(getEM(tenantId));:

#
HSEARCH000058: Exception occurred javax.persistence.PersistenceException: org.hibernate.HibernateException: Error trying to get datasource ['java:app/jdbc/my2ndDS']
Failing operation:
Fetching identifiers of entities to index for entity 'Users' during mass indexing
javax.persistence.PersistenceException: org.hibernate.HibernateException: Error trying to get datasource ['java:app/jdbc/my2ndDS']
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:154)
at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1602)
at org.hibernate.query.internal.AbstractProducedQuery.uniqueResult(AbstractProducedQuery.java:1635)
at org.hibernate.query.criteria.internal.compile.CriteriaQueryTypeQueryAdapter.uniqueResult(CriteriaQueryTypeQueryAdapter.java:81)
at org.hibernate.search.mapper.orm.massindexing.impl.IdentifierProducer.loadAllIdentifiers(IdentifierProducer.java:144)
at org.hibernate.search.mapper.orm.massindexing.impl.IdentifierProducer.inTransactionWrapper(IdentifierProducer.java:124)
at org.hibernate.search.mapper.orm.massindexing.impl.IdentifierProducer.run(IdentifierProducer.java:96)
at org.hibernate.search.mapper.orm.massindexing.impl.OptionallyWrapInJTATransaction.runWithFailureHandler(OptionallyWrapInJTATransaction.java:68)
at org.hibernate.search.mapper.orm.massindexing.impl.FailureHandledRunnable.run(FailureHandledRunnable.java:33)
at org.hibernate.search.util.common.impl.CancellableExecutionCompletableFuture$CompletingRunnable.run(CancellableExecutionCompletableFuture.java:49)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.hibernate.HibernateException: Error trying to get datasource ['java:app/jdbc/my2ndDS']
at com.dao.multitenancy.DatabaseMultiTenantProvider.getConnection(DatabaseMultiTenantProvider.java:96)
at org.hibernate.internal.ContextualJdbcConnectionAccess.obtainConnection(ContextualJdbcConnectionAccess.java:43)
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:108)
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:138)
at org.hibernate.engine.jdbc.internal.StatementPreparerImpl.connection(StatementPreparerImpl.java:50)
at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$5.doPrepare(StatementPreparerImpl.java:149)
at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$StatementPreparationTemplate.prepareStatement(StatementPreparerImpl.java:176)
at org.hibernate.engine.jdbc.internal.StatementPreparerImpl.prepareQueryStatement(StatementPreparerImpl.java:151)
at org.hibernate.loader.Loader.prepareQueryStatement(Loader.java:2103)
at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:2040)
at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:2018)
at org.hibernate.loader.Loader.doQuery(Loader.java:948)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:349)
at org.hibernate.loader.Loader.doList(Loader.java:2849)
at org.hibernate.loader.Loader.doList(Loader.java:2831)
at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2663)
at org.hibernate.loader.Loader.list(Loader.java:2658)
at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:506)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:400)
at org.hibernate.engine.query.spi.HQLQueryPlan.performList(HQLQueryPlan.java:219)
at org.hibernate.internal.StatelessSessionImpl.list(StatelessSessionImpl.java:564)
at org.hibernate.query.internal.AbstractProducedQuery.doList(AbstractProducedQuery.java:1625)
at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1593)
... 13 more
Caused by: javax.naming.NamingException: Lookup failed for 'java:app/jdbc/my2ndDS' in SerialContext[myEnv={java.naming.factory.initial=com.sun.enterprise.naming.impl.SerialInitContextFactory, java.naming.factory.state=com.sun.corba.ee.impl.presentation.rmi.JNDIStateFactoryImpl, java.naming.factory.url.pkgs=com.sun.enterprise.naming} [Root exception is javax.naming.NamingException: Invocation exception: Got null ComponentInvocation ]
at com.sun.enterprise.naming.impl.SerialContext.lookup(SerialContext.java:496)
at com.sun.enterprise.naming.impl.SerialContext.lookup(SerialContext.java:442)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at com.dao.multitenancy.DatabaseMultiTenantProvider.getConnection(DatabaseMultiTenantProvider.java:94)
... 35 more
Caused by: javax.naming.NamingException: Invocation exception: Got null ComponentInvocation 
at com.sun.enterprise.naming.impl.GlassfishNamingManagerImpl.getComponentId(GlassfishNamingManagerImpl.java:870)
at com.sun.enterprise.naming.impl.GlassfishNamingManagerImpl.lookup(GlassfishNamingManagerImpl.java:737)
at com.sun.enterprise.naming.impl.JavaURLContext.lookup(JavaURLContext.java:167)
at com.sun.enterprise.naming.impl.SerialContext.lookup(SerialContext.java:476)
... 39 more
|#] 

在公共类DatabaseMultiTenantProvider(见第94,96行;附近的底部):

import java.sql.Connection;
import org.hibernate.HibernateException;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.hibernate.service.spi.ServiceRegistryAwareService;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource; 
import java.sql.SQLException; 
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class DatabaseMultiTenantProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService {
private static final long serialVersionUID = 1L;
private static final String TENANT_SUPPORTED = "DATABASE";
private DataSource dataSource;
private String typeTenancy;
private static final Logger logger = LogManager.getLogger();
@Override
public boolean supportsAggressiveRelease() {
return false;
}
@Override
public void injectServices(ServiceRegistryImplementor serviceRegistry) {
logger.debug("injectService for DatabaseMultiTenantProvider");
typeTenancy = (String) serviceRegistry
.getService(ConfigurationService.class)
.getSettings().get("hibernate.multiTenancy");
logger.debug("datasouce casting result: {}", () -> serviceRegistry.getService(ConfigurationService.class).getSettings().get("hibernate.connection.datasource"));
if (serviceRegistry
.getService(ConfigurationService.class)
.getSettings().get("hibernate.connection.datasource") instanceof DataSource) {
logger.debug("can cast to DataSource");
dataSource = (DataSource) serviceRegistry
.getService(ConfigurationService.class)
.getSettings().get("hibernate.connection.datasource");
} else {
logger.debug("can't cast to DataSource; have to use JNDI lookup");
try {
final Context init = new InitialContext();
dataSource = (DataSource) init.lookup((String) serviceRegistry
.getService(ConfigurationService.class)
.getSettings().get("hibernate.connection.datasource"));
} catch (final NamingException e) {
logger.error("error in init lookup: {}", ()->e.getMessage());
throw new RuntimeException(e);
}
}
}
@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
logger.debug("connecting to tenent: {}", () -> tenantIdentifier);
if (TENANT_SUPPORTED.equals(typeTenancy)) {
try {

final Context  init = new InitialContext();
logger.debug("use tenant datasource: {}", () -> tenantIdentifier);
final String ds = "java:app/jdbc/"+tenantIdentifier;
logger.debug("getConnection for: {}", ()->ds);
dataSource = (DataSource) init.lookup(ds); //line 94
} catch (NamingException e) {
throw new HibernateException("Error trying to get datasource ['java:app/jdbc/" + tenantIdentifier + "']", e);//line 96 
}
}
return dataSource.getConnection();
}
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
logger.debug("release any connection");
connection.close();
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
logger.debug("release a connection for tenentId: {}", () -> tenantIdentifier);
releaseAnyConnection(connection);
}
}

我认为这个问题应该来自JNDI与Hibernate搜索。如有任何建议或提示,不胜感激。

已更新(1):我也测试了新的ThreadLocal(见下文),仍然显示相同的错误。

With new ThreadLocal:

public  class CallEntityManager {
private static ThreadLocal<EntityManager> threadLocal = new ThreadLocal<>();
private static EntityManagerFactory emf;
public static EntityManager getEM(final String tenantId) {
if (emf == null) { 
emf = Persistence.createEntityManagerFactory("jakartaEEPU");
}  
final SessionFactoryImplementor sf = emf.unwrap(SessionFactoryImplementor.class);
final MultitenancyResolver tenantResolver = (MultitenancyResolver) sf.getCurrentTenantIdentifierResolver();
tenantResolver.setTenantIdentifier(tenantId);
EntityManager em = threadLocal.get();
if (em == null) {
logger.debug("em is null; will create EM now");
em = emf.createEntityManager();
threadLocal.set(em);
}
return em;
}
public static void closeEntityManager() {
final EntityManager em = threadLocal.get();
if (em != null) {
em.close();
threadLocal.set(null); 
}
}
}

Hibernate Search的海量索引器需要自己创建实体管理器,因为它可以并行索引,而且您不能在多个线程中并发地使用一个实体管理器。但是,它应该自动使用创建它的原始会话的租户ID。而且,从错误消息判断,在您的情况下确实如此:那里有对my2ndDS的引用。

据我所知,问题在于检索数据源,而不是处理多租户。并不是说Hibernate Search在mass-indexer中创建了自己的线程。数据源检索是否依赖于线程本地上下文,而这些上下文可能不会在这些新线程中初始化?

一个快速而肮脏的测试方法是手动创建一个线程(new Thread()),并尝试从该线程调用getEM()。如果您得到相同的错误,问题可能是数据源解析依赖于一些未初始化的线程本地上下文。然后,您应该调查堆栈跟踪中没有显示的部分,在"查找失败的'java:app/jdbc/my2nDS'"

顺便说一下,Got null ComponentInvocation似乎是Payara的特征。这是我搜索网页时得到的第一个结果:https://github.com/payara/Payara/issues/2430如果我是你,我会调查JNDI解析出了什么问题。

解决方案:

  1. 在getConnection方法中更新DatabaseMultiTenantProvider类(见下文),

  2. 可以像开始一样使用PersistanceContext(不需要使用ThreadLocal)。

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
    //Just use the multitenancy if the hibernate.multiTenancy == DATABASE
    logger.debug("connecting to tenent DS: {}", () -> tenantIdentifier);
    if (TENANT_SUPPORTED.equals(typeTenancy)) {
    try {
    logger.debug("use tenant dataSource name: {}", () -> tenantIdentifier);
    //final String tenant[] = tenantIdentifier.split(":{2}");
    //if (tenant != null && tenant.length > 1) { 
    final MariaDbDataSource mds = new MariaDbDataSource();
    if (tenantIdentifier.equals("myDS")) {
    logger.debug("using 1st dataSource: {}", () -> tenantIdentifier);
    mds.setUrl("jdbc:mariadb://localhost:3306/mtDb");
    mds.setUser("mtII");
    mds.setPassword("mt123");
    mds.setServerName("localhost");
    mds.setPort(3306);
    mds.setDatabaseName("mtDb");
    } else if (tenantIdentifier.equals("my2ndDS")) {
    logger.debug("using 2nd database: {}", () -> tenantIdentifier);
    mds.setUrl("jdbc:mariadb://localhost:3306/mt2Db");
    mds.setUser("mtII");
    mds.setPassword("mt123");
    mds.setServerName("localhost");
    mds.setPort(3306);
    mds.setDatabaseName("mt2Db");
    }
    dataSource = mds;
    return dataSource.getConnection();
    /* } else {
    logger.debug("normal way in connecting to dataSource");
    final String dsURL = "java:app/jdbc/" + tenantIdentifier;
    logger.debug("getConnection for: {}", () -> dsURL);
    final Context init = new InitialContext();
    dataSource = (DataSource) init.lookup(dsURL);
    return dataSource.getConnection();
    }*/
    } catch (Exception e) {
    //e.printStackTrace();
    throw new HibernateException("Error trying to get dataSource ['java:app/jdbc/" + tenantIdentifier + "']", e);
    }
    }
    return null;
    

}

最新更新