diff --git a/build/config/src/main/java/org/hibernate/search/build/report/loggers/LoggerCategoriesProcessor.java b/build/config/src/main/java/org/hibernate/search/build/report/loggers/LoggerCategoriesProcessor.java index 8d67af029a6..1a54937ca45 100644 --- a/build/config/src/main/java/org/hibernate/search/build/report/loggers/LoggerCategoriesProcessor.java +++ b/build/config/src/main/java/org/hibernate/search/build/report/loggers/LoggerCategoriesProcessor.java @@ -9,7 +9,6 @@ import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; diff --git a/build/enforcer/src/main/java/org/hibernate/search/build/enforcer/MavenProjectUtils.java b/build/enforcer/src/main/java/org/hibernate/search/build/enforcer/MavenProjectUtils.java index ed9eedf21dd..976f431a68a 100644 --- a/build/enforcer/src/main/java/org/hibernate/search/build/enforcer/MavenProjectUtils.java +++ b/build/enforcer/src/main/java/org/hibernate/search/build/enforcer/MavenProjectUtils.java @@ -11,7 +11,8 @@ public class MavenProjectUtils { public static final String HIBERNATE_SEARCH_PARENT_PUBLIC = "hibernate-search-parent-public"; public static final String HIBERNATE_SEARCH_PARENT_PUBLIC_LUCENE_NEXT = "hibernate-search-parent-public-lucene-next"; public static final String HIBERNATE_SEARCH_PARENT_INTEGRATION_TEST = "hibernate-search-parent-integrationtest"; - public static final String HIBERNATE_SEARCH_PARENT_INTEGRATION_TEST_LUCENE_NEXT = "hibernate-search-parent-integrationtest-lucene-next"; + public static final String HIBERNATE_SEARCH_PARENT_INTEGRATION_TEST_LUCENE_NEXT = + "hibernate-search-parent-integrationtest-lucene-next"; public static final String HIBERNATE_SEARCH_PARENT_RELOCATION = "hibernate-search-parent-relocation"; public static final String DEPLOY_SKIP = "deploy.skip"; @@ -35,7 +36,7 @@ public static boolean isAnyParentRelocationParent(MavenProject project) { public static boolean isAnyParentIntegrationTestParent(MavenProject project) { return project.hasParent() && ( HIBERNATE_SEARCH_PARENT_INTEGRATION_TEST.equals( project.getParent().getArtifactId() ) - || HIBERNATE_SEARCH_PARENT_INTEGRATION_TEST_LUCENE_NEXT.equals( project.getParent().getArtifactId() ) + || HIBERNATE_SEARCH_PARENT_INTEGRATION_TEST_LUCENE_NEXT.equals( project.getParent().getArtifactId() ) || isAnyParentIntegrationTestParent( project.getParent() ) ); } diff --git a/build/jqassistant/rules/rules.xml b/build/jqassistant/rules/rules.xml index a9a1ed05db3..035d0c4d250 100644 --- a/build/jqassistant/rules/rules.xml +++ b/build/jqassistant/rules/rules.xml @@ -280,6 +280,7 @@ WHEN 'hibernate-search-mapper-pojo-standalone' THEN 'StandalonePojo' WHEN 'hibernate-search-mapper-orm' THEN 'HibernateOrm' WHEN 'hibernate-search-mapper-orm-outbox-polling' THEN 'OutboxPolling' + WHEN 'hibernate-search-mapper-orm-jakarta-batch-core' THEN 'BatchCore' WHEN 'hibernate-search-mapper-orm-jakarta-batch-jberet' THEN 'JBeret' ELSE 'UNKNOWN-MODULE-SPECIFIC-KEYWORD-PLEASE-UPDATE-JQASSISTANT-RULES' END diff --git a/integrationtest/mapper/orm-jakarta-batch/src/test/java/org/hibernate/search/integrationtest/jakarta/batch/util/JobTestUtil.java b/integrationtest/mapper/orm-jakarta-batch/src/test/java/org/hibernate/search/integrationtest/jakarta/batch/util/JobTestUtil.java index 0bd66848828..4c728e144eb 100644 --- a/integrationtest/mapper/orm-jakarta-batch/src/test/java/org/hibernate/search/integrationtest/jakarta/batch/util/JobTestUtil.java +++ b/integrationtest/mapper/orm-jakarta-batch/src/test/java/org/hibernate/search/integrationtest/jakarta/batch/util/JobTestUtil.java @@ -22,7 +22,7 @@ import org.hibernate.SessionFactory; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.search.jakarta.batch.core.massindexing.MassIndexingJob; -import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.EntityTypeDescriptor; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.BatchCoreEntityTypeDescriptor; import org.hibernate.search.mapper.orm.Search; import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmLoadingTypeContext; import org.hibernate.search.mapper.orm.mapping.SearchMapping; @@ -141,11 +141,11 @@ private static List find(Session session, Class clazz, String key, Str .fetchHits( 1000 ); } - public static EntityTypeDescriptor createEntityTypeDescriptor(EntityManagerFactory emf, Class clazz) { + public static BatchCoreEntityTypeDescriptor createEntityTypeDescriptor(EntityManagerFactory emf, Class clazz) { SearchMapping mapping = Search.mapping( emf ); BatchMappingContext mappingContext = (BatchMappingContext) mapping; HibernateOrmLoadingTypeContext type = mappingContext.typeContextProvider() .byEntityName().getOrFail( mapping.indexedEntity( clazz ).jpaName() ); - return EntityTypeDescriptor.create( emf.unwrap( SessionFactoryImplementor.class ), type ); + return BatchCoreEntityTypeDescriptor.create( emf.unwrap( SessionFactoryImplementor.class ), type ); } } diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/impl/JobContextData.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/impl/JobContextData.java index d649bb7896c..65c22a2ae37 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/impl/JobContextData.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/impl/JobContextData.java @@ -15,7 +15,7 @@ import jakarta.persistence.EntityManagerFactory; -import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.EntityTypeDescriptor; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.BatchCoreEntityTypeDescriptor; import org.hibernate.search.mapper.orm.tenancy.spi.TenancyConfiguration; import org.hibernate.search.mapper.pojo.massindexing.MassIndexingDefaultCleanOperation; @@ -33,7 +33,7 @@ public class JobContextData { * In Jakarta Batch standard, only string values can be propagated using job properties, but class types are frequently * used too. So this map has string keys to facilitate lookup for values extracted from job properties. */ - private Map> entityTypeDescriptorMap; + private Map> entityTypeDescriptorMap; private TenancyConfiguration tenancyConfiguration; private MassIndexingDefaultCleanOperation massIndexingDefaultCleanOperation; @@ -50,8 +50,8 @@ public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) { this.entityManagerFactory = entityManagerFactory; } - public void setEntityTypeDescriptors(Collection> descriptors) { - for ( EntityTypeDescriptor descriptor : descriptors ) { + public void setEntityTypeDescriptors(Collection> descriptors) { + for ( BatchCoreEntityTypeDescriptor descriptor : descriptors ) { entityTypeDescriptorMap.put( descriptor.jpaEntityName(), descriptor ); } } @@ -72,8 +72,8 @@ public void setMassIndexingDefaultCleanOperation(MassIndexingDefaultCleanOperati this.massIndexingDefaultCleanOperation = massIndexingDefaultCleanOperation; } - public EntityTypeDescriptor getEntityTypeDescriptor(String entityName) { - EntityTypeDescriptor descriptor = entityTypeDescriptorMap.get( entityName ); + public BatchCoreEntityTypeDescriptor getEntityTypeDescriptor(String entityName) { + BatchCoreEntityTypeDescriptor descriptor = entityTypeDescriptorMap.get( entityName ); if ( descriptor == null ) { String msg = String.format( Locale.ROOT, "entity type %s not found.", entityName ); throw new NoSuchElementException( msg ); @@ -81,13 +81,13 @@ public void setMassIndexingDefaultCleanOperation(MassIndexingDefaultCleanOperati return descriptor; } - public List> getEntityTypeDescriptors() { + public List> getEntityTypeDescriptors() { return new ArrayList<>( entityTypeDescriptorMap.values() ); } public List> getEntityTypes() { return entityTypeDescriptorMap.values().stream() - .map( EntityTypeDescriptor::javaClass ) + .map( BatchCoreEntityTypeDescriptor::javaClass ) .collect( Collectors.toList() ); } diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchEntityLoader.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchEntityLoader.java new file mode 100644 index 00000000000..87284271fee --- /dev/null +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchEntityLoader.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.jakarta.batch.core.massindexing.loading.impl; + +import java.util.List; + +import jakarta.persistence.LockModeType; + +import org.hibernate.Session; +import org.hibernate.query.Query; +import org.hibernate.query.QueryFlushMode; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntityLoader; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntityLoadingOptions; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntitySink; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchLoadingTypeContext; + +public class BatchCoreDefaultHibernateOrmBatchEntityLoader implements HibernateOrmBatchEntityLoader { + private static final String ID_PARAMETER_NAME = "ids"; + + private final HibernateOrmBatchEntitySink sink; + private final Query query; + + public BatchCoreDefaultHibernateOrmBatchEntityLoader(HibernateOrmBatchLoadingTypeContext typeContext, + HibernateOrmBatchEntitySink sink, HibernateOrmBatchEntityLoadingOptions options) { + this.sink = sink; + + StringBuilder query = new StringBuilder(); + query.append( "select e from " ) + .append( typeContext.jpaEntityName() ) + .append( " e where e." ) + .append( typeContext.uniquePropertyName() ) + .append( " in(:" ) + .append( ID_PARAMETER_NAME ) + .append( ")" ); + + this.query = options.context( Session.class ).createQuery( query.toString(), typeContext.javaClass() ) + .setReadOnly( true ) + .setCacheable( false ) + .setLockMode( LockModeType.NONE ) + .setCacheMode( options.cacheMode() ) + .setQueryFlushMode( QueryFlushMode.NO_FLUSH ) + .setFetchSize( options.batchSize() ); + } + + @Override + public void close() { + } + + @Override + public void load(List identifiers) { + sink.accept( query.setParameter( ID_PARAMETER_NAME, identifiers ).list() ); + } + +} diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchIdentifierLoader.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchIdentifierLoader.java new file mode 100644 index 00000000000..4cdaade48f7 --- /dev/null +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchIdentifierLoader.java @@ -0,0 +1,193 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.jakarta.batch.core.massindexing.loading.impl; + +import java.util.HashSet; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.persistence.LockModeType; + +import org.hibernate.ScrollMode; +import org.hibernate.ScrollableResults; +import org.hibernate.StatelessSession; +import org.hibernate.query.Query; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.IdOrder; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoader; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoadingOptions; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchLoadingTypeContext; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; +import org.hibernate.search.util.common.AssertionFailure; +import org.hibernate.search.util.common.impl.Closer; + +public class BatchCoreDefaultHibernateOrmBatchIdentifierLoader implements HibernateOrmBatchIdentifierLoader { + + private final StatelessSession session; + private final String ormEntityName; + private final String uniquePropertyName; + private final IdOrder idOrder; + private final HibernateOrmBatchIdentifierLoadingOptions options; + private final IdLoader idLoader; + + public BatchCoreDefaultHibernateOrmBatchIdentifierLoader(HibernateOrmBatchLoadingTypeContext typeContext, + HibernateOrmBatchIdentifierLoadingOptions options, IdOrder idOrder) { + this.session = options.context( StatelessSession.class ); + this.ormEntityName = typeContext.jpaEntityName(); + this.uniquePropertyName = typeContext.uniquePropertyName(); + this.idOrder = idOrder; + this.options = options; + this.idLoader = options.maxResults().orElse( -1 ) == 1 ? new QuerySingleIdLoader() : new ScrollIdLoader(); + } + + @Override + public void close() { + try ( Closer closer = new Closer<>() ) { + if ( idLoader != null ) { + closer.push( IdLoader::close, idLoader ); + } + } + } + + @Override + public OptionalLong totalCount() { + StringBuilder query = new StringBuilder(); + query.append( "select count(e) from " ) + .append( ormEntityName ) + .append( " e " ); + + return OptionalLong.of( createQuery( session, query, + options.reindexOnlyCondition().map( Set::of ).orElseGet( Set::of ), Long.class, Optional.empty() ) + .uniqueResult() ); + } + + @Override + public Object next() { + return idLoader.next(); + } + + @Override + public boolean hasNext() { + return idLoader.hasNext(); + } + + private Query createQueryLoading(StatelessSession session) { + StringBuilder query = new StringBuilder(); + query.append( "select e." ) + .append( uniquePropertyName ) + .append( " from " ) + .append( ormEntityName ) + .append( " e " ); + Set conditions = new HashSet<>(); + options.reindexOnlyCondition().ifPresent( conditions::add ); + options.lowerBound().ifPresent( b -> conditions + .add( idOrder.idGreater( "HIBERNATE_SEARCH_ID_LOWER_BOUND_", b, options.lowerBoundInclusive() ) ) ); + options.upperBound().ifPresent( b -> conditions + .add( idOrder.idLesser( "HIBERNATE_SEARCH_ID_UPPER_BOUND_", b, options.upperBoundInclusive() ) ) ); + + Query select = createQuery( session, query, conditions, Object.class, Optional.of( idOrder.ascOrder() ) ) + .setFetchSize( options.fetchSize() ) + .setReadOnly( true ) + .setCacheable( false ) + .setLockMode( LockModeType.NONE ); + options.offset().ifPresent( select::setFirstResult ); + options.maxResults().ifPresent( select::setMaxResults ); + return select; + } + + private Query createQuery(StatelessSession session, + StringBuilder hql, Set conditions, Class returnedType, + Optional order) { + if ( !conditions.isEmpty() ) { + hql.append( " where " ); + hql.append( conditions.stream() + .map( c -> "( " + c.conditionString() + " )" ) + .collect( Collectors.joining( " AND ", " ", " " ) ) + ); + } + order.ifPresent( o -> hql.append( " ORDER BY " ).append( o ) ); + Query query = session.createQuery( hql.toString(), returnedType ) + .setCacheable( false ); + + for ( var condition : conditions ) { + for ( var entry : condition.params().entrySet() ) { + query.setParameter( entry.getKey(), entry.getValue() ); + } + } + + return query; + } + + private interface IdLoader { + Object next(); + + boolean hasNext(); + + void close(); + } + + private class QuerySingleIdLoader implements IdLoader { + + private boolean hasNextCalled = false; + private boolean nextCalled = false; + + private Query id = createQueryLoading( session ); + private Object currentId; + + @Override + public Object next() { + if ( hasNextCalled ) { + nextCalled = true; + hasNextCalled = false; + return currentId; + } + else { + throw new AssertionFailure( "Cannot call next() before calling hasNext()" ); + } + } + + @Override + public boolean hasNext() { + if ( nextCalled ) { + // we expect to have just a single ID, so if we called next and got the id we don't need to execute the query anymore: + return false; + } + currentId = id.getSingleResultOrNull(); + hasNextCalled = true; + return currentId != null; + } + + @Override + public void close() { + id = null; + } + } + + private class ScrollIdLoader implements IdLoader { + private ScrollableResults id = createQueryLoading( session ).scroll( ScrollMode.FORWARD_ONLY ); + + @Override + public Object next() { + return id.get(); + } + + @Override + public boolean hasNext() { + return id.next(); + } + + @Override + public void close() { + try ( Closer closer = new Closer<>() ) { + if ( id != null ) { + closer.push( ScrollableResults::close, id ); + id = null; + } + } + } + } + +} diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchLoadingStrategy.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchLoadingStrategy.java new file mode 100644 index 00000000000..08c67db68c6 --- /dev/null +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/loading/impl/BatchCoreDefaultHibernateOrmBatchLoadingStrategy.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.jakarta.batch.core.massindexing.loading.impl; + +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.CompositeIdOrder; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.IdOrder; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.SingularIdOrder; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntityLoader; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntityLoadingOptions; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntitySink; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoader; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoadingOptions; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchLoadingStrategy; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchLoadingTypeContext; +import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmLoadingTypeContext; + +public class BatchCoreDefaultHibernateOrmBatchLoadingStrategy implements HibernateOrmBatchLoadingStrategy { + + private final IdOrder idOrder; + + public BatchCoreDefaultHibernateOrmBatchLoadingStrategy(HibernateOrmLoadingTypeContext type) { + EntityIdentifierMapping identifierMapping = type.entityMappingType().getIdentifierMapping(); + if ( identifierMapping.getPartMappingType() instanceof EmbeddableMappingType ) { + idOrder = new CompositeIdOrder<>( type ); + } + else { + idOrder = new SingularIdOrder<>( type ); + } + } + + @Override + public HibernateOrmBatchIdentifierLoader createIdentifierLoader(HibernateOrmBatchLoadingTypeContext typeContext, + HibernateOrmBatchIdentifierLoadingOptions options) { + return new BatchCoreDefaultHibernateOrmBatchIdentifierLoader<>( typeContext, options, idOrder ); + } + + @Override + public HibernateOrmBatchEntityLoader createEntityLoader(HibernateOrmBatchLoadingTypeContext typeContext, + HibernateOrmBatchEntitySink sink, HibernateOrmBatchEntityLoadingOptions options) { + return new BatchCoreDefaultHibernateOrmBatchEntityLoader<>( typeContext, sink, options ); + } +} diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/EntityWriter.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/EntityWriter.java index 2f7dee1208f..e723943d5bc 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/EntityWriter.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/EntityWriter.java @@ -14,12 +14,9 @@ import jakarta.batch.runtime.context.StepContext; import jakarta.inject.Inject; import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.LockModeType; import org.hibernate.CacheMode; import org.hibernate.Session; -import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.query.QueryFlushMode; import org.hibernate.search.engine.backend.work.execution.DocumentCommitStrategy; import org.hibernate.search.engine.backend.work.execution.DocumentRefreshStrategy; import org.hibernate.search.engine.backend.work.execution.OperationSubmitter; @@ -27,16 +24,20 @@ import org.hibernate.search.jakarta.batch.core.logging.impl.JakartaBatchLog; import org.hibernate.search.jakarta.batch.core.massindexing.MassIndexingJobParameters; import org.hibernate.search.jakarta.batch.core.massindexing.impl.JobContextData; -import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.EntityTypeDescriptor; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.BatchCoreEntityTypeDescriptor; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.MassIndexingPartitionProperties; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.PersistenceUtil; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.SerializationUtil; import org.hibernate.search.mapper.orm.Search; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntityLoader; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntityLoadingOptions; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchEntitySink; import org.hibernate.search.mapper.orm.mapping.SearchMapping; import org.hibernate.search.mapper.orm.spi.BatchMappingContext; import org.hibernate.search.mapper.orm.tenancy.spi.TenancyConfiguration; import org.hibernate.search.mapper.pojo.work.spi.PojoIndexer; import org.hibernate.search.mapper.pojo.work.spi.PojoScopeWorkspace; +import org.hibernate.search.util.common.AssertionFailure; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.impl.Futures; @@ -79,7 +80,7 @@ public class EntityWriter extends AbstractItemWriter { private EntityManagerFactory emf; private BatchMappingContext mappingContext; - private EntityTypeDescriptor type; + private BatchCoreEntityTypeDescriptor type; private PojoScopeWorkspace workspace; private WriteMode writeMode; @@ -128,9 +129,28 @@ public void open(Serializable checkpoint) { @Override public void writeItems(List entityIds) { try ( Session session = PersistenceUtil.openSession( emf, tenancyConfiguration.convert( tenantId ) ) ) { - SessionImplementor sessionImplementor = session.unwrap( SessionImplementor.class ); - - PojoIndexer indexer = mappingContext.sessionContext( session ).createIndexer(); + HibernateOrmBatchEntityLoader entityLoader = entityLoader( type, + new HibernateOrmBatchEntityLoadingOptions() { + @Override + public int batchSize() { + return entityFetchSize; + } + + @Override + public CacheMode cacheMode() { + return cacheMode; + } + + @SuppressWarnings("unchecked") + @Override + public T context(Class contextType) { + if ( Session.class.isAssignableFrom( contextType ) ) { + return (T) session; + } + throw new AssertionFailure( "Unexpected context " + contextType + " requested." ); + } + }, session + ); int i = 0; while ( i < entityIds.size() ) { @@ -138,9 +158,7 @@ public void writeItems(List entityIds) { i += entityFetchSize; int toIndex = Math.min( i, entityIds.size() ); - List entities = loadEntities( sessionImplementor, entityIds.subList( fromIndex, toIndex ) ); - - indexAndWaitForCompletion( entities, indexer ); + entityLoader.load( entityIds.subList( fromIndex, toIndex ) ); } } @@ -171,18 +189,6 @@ public void close() throws Exception { JakartaBatchLog.INSTANCE.closingEntityWriter( partitionIdStr, entityName ); } - private List loadEntities(SessionImplementor session, List entityIds) { - return type.createLoadingQuery( session, ID_PARAMETER_NAME ) - .setParameter( ID_PARAMETER_NAME, entityIds ) - .setReadOnly( true ) - .setCacheable( false ) - .setLockMode( LockModeType.NONE ) - .setCacheMode( cacheMode ) - .setQueryFlushMode( QueryFlushMode.NO_FLUSH ) // FlushMode.MANUAL - .setFetchSize( entityFetchSize ) - .list(); - } - private void indexAndWaitForCompletion(List entities, PojoIndexer indexer) { if ( entities == null || entities.isEmpty() ) { return; @@ -219,6 +225,21 @@ private CompletableFuture writeItem(PojoIndexer indexer, Object entity) { ); } + HibernateOrmBatchEntityLoader entityLoader(BatchCoreEntityTypeDescriptor type, + HibernateOrmBatchEntityLoadingOptions options, + Session session) { + return type.batchLoadingStrategy().createEntityLoader( + type, + createSink( session ), + options + ); + } + + HibernateOrmBatchEntitySink createSink(Session session) { + PojoIndexer indexer = mappingContext.sessionContext( session ).createIndexer(); + return entities -> indexAndWaitForCompletion( entities, indexer ); + } + private enum WriteMode { ADD, UPDATE; diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/HibernateSearchPartitionMapper.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/HibernateSearchPartitionMapper.java index f1bd90dd1cc..22690640eb4 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/HibernateSearchPartitionMapper.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/HibernateSearchPartitionMapper.java @@ -6,7 +6,11 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; import java.util.Properties; import jakarta.batch.api.BatchProperty; @@ -16,20 +20,18 @@ import jakarta.batch.runtime.context.JobContext; import jakarta.inject.Inject; import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.LockModeType; import org.hibernate.StatelessSession; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.query.SelectionQuery; import org.hibernate.search.jakarta.batch.core.logging.impl.JakartaBatchLog; import org.hibernate.search.jakarta.batch.core.massindexing.MassIndexingJobParameters; import org.hibernate.search.jakarta.batch.core.massindexing.impl.JobContextData; -import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.EntityTypeDescriptor; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.BatchCoreEntityTypeDescriptor; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.MassIndexingPartitionProperties; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.PartitionBound; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.PersistenceUtil; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.SerializationUtil; -import org.hibernate.search.mapper.orm.loading.spi.ConditionalExpression; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoadingOptions; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; /** * This partition mapper provides a dynamic partition plan for chunk processing. @@ -131,15 +133,15 @@ public PartitionPlan mapPartitions() throws Exception { ); int checkpointInterval = MassIndexingJobParameters.Defaults.checkpointInterval( checkpointIntervalRaw, rowsPerPartition ); - ConditionalExpression reindexOnly = + HibernateOrmBatchReindexCondition reindexOnly = SerializationUtil.parseReindexOnlyParameters( reindexOnlyHql, serializedReindexOnlyParameters ); - List> entityTypeDescriptors = jobData.getEntityTypeDescriptors(); + List> entityTypeDescriptors = jobData.getEntityTypeDescriptors(); List partitionBounds = new ArrayList<>(); - for ( EntityTypeDescriptor entityTypeDescriptor : entityTypeDescriptors ) { - partitionBounds.addAll( buildPartitionUnitsFrom( ss, entityTypeDescriptor, - maxResults, rowsPerPartition, reindexOnly ) ); + for ( BatchCoreEntityTypeDescriptor entityTypeDescriptor : entityTypeDescriptors ) { + partitionBounds.addAll( + buildPartitionUnitsFrom( ss, entityTypeDescriptor, maxResults, rowsPerPartition, reindexOnly ) ); } // Build partition plan @@ -178,18 +180,27 @@ public PartitionPlan mapPartitions() throws Exception { } } - private List buildPartitionUnitsFrom(StatelessSession ss, - EntityTypeDescriptor type, - Integer maxResults, int rowsPerPartition, ConditionalExpression reindexOnly) { + private List buildPartitionUnitsFrom(StatelessSession session, + BatchCoreEntityTypeDescriptor type, + Integer maxResults, int rowsPerPartition, HibernateOrmBatchReindexCondition reindexOnly) { List partitionUnits = new ArrayList<>(); int index = 0; Object lowerBound = null; - Object upperBound; + Object upperBound = null; // If there are no results or fewer than "rowsPerPartition" results, // we'll just create one partition with two null bounds. do { - upperBound = selectNextId( type, reindexOnly, ss, lowerBound, rowsPerPartition ); + var options = new LoadingOptions( session, rowsPerPartition, lowerBound, reindexOnly ); + try ( var identifierLoader = type.batchLoadingStrategy().createIdentifierLoader( type, options ) ) { + if ( identifierLoader.hasNext() ) { + upperBound = identifierLoader.next(); + } + else { + upperBound = null; + } + } + partitionUnits.add( new PartitionBound( type, lowerBound, upperBound ) ); index += rowsPerPartition; lowerBound = upperBound; @@ -199,24 +210,70 @@ private List buildPartitionUnitsFrom(StatelessSession ss, return partitionUnits; } - private I selectNextId(EntityTypeDescriptor type, ConditionalExpression reindexOnly, - StatelessSession ss, Object lowerId, int offset) { - List conditions = new ArrayList<>(); - if ( reindexOnly != null ) { - conditions.add( reindexOnly ); + private static class LoadingOptions implements HibernateOrmBatchIdentifierLoadingOptions { + private final HibernateOrmBatchReindexCondition reindexOnly; + private final Map, Object> contextData; + private final int offset; + private final Object lowerBound; + + LoadingOptions(StatelessSession session, + int offset, + Object lowerBound, + HibernateOrmBatchReindexCondition reindexOnly) { + this.offset = offset; + this.lowerBound = lowerBound; + this.reindexOnly = reindexOnly; + + this.contextData = new HashMap<>(); + + this.contextData.put( StatelessSession.class, session ); + } + + @Override + public int fetchSize() { + return 1; + } + + @Override + public OptionalInt maxResults() { + return OptionalInt.of( 1 ); + } + + @Override + public OptionalInt offset() { + return OptionalInt.of( offset ); } - if ( lowerId != null ) { - conditions.add( type.idOrder() - .idGreater( "HIBERNATE_SEARCH_PARTITION_LOWER_BOUND_", lowerId ) ); + + @Override + public Optional reindexOnlyCondition() { + return Optional.ofNullable( reindexOnly ); + } + + @Override + public Optional upperBound() { + return Optional.empty(); + } + + @Override + public boolean upperBoundInclusive() { + return false; + } + + @Override + public Optional lowerBound() { + return Optional.ofNullable( lowerBound ); + } + + @Override + public boolean lowerBoundInclusive() { + return false; + } + + @SuppressWarnings("unchecked") + @Override + public T context(Class contextType) { + return (T) contextData.get( contextType ); } - SelectionQuery query = type.createIdentifiersQuery( (SharedSessionContractImplementor) ss, conditions ) - .setFetchSize( 1 ) - .setReadOnly( true ) - .setCacheable( false ) - .setLockMode( LockModeType.NONE ); - query.setFirstResult( offset ); - query.setMaxResults( 1 ); - return query.getSingleResultOrNull(); } } diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/StepProgressSetupListener.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/StepProgressSetupListener.java index abb38cc880c..7b45b1fbf0b 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/StepProgressSetupListener.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/impl/StepProgressSetupListener.java @@ -5,7 +5,11 @@ package org.hibernate.search.jakarta.batch.core.massindexing.step.impl; import java.io.IOException; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.OptionalLong; import jakarta.batch.api.BatchProperty; import jakarta.batch.api.listener.AbstractStepListener; @@ -13,17 +17,17 @@ import jakarta.batch.runtime.context.StepContext; import jakarta.inject.Inject; import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.LockModeType; import org.hibernate.StatelessSession; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.search.jakarta.batch.core.logging.impl.JakartaBatchLog; import org.hibernate.search.jakarta.batch.core.massindexing.MassIndexingJobParameters; import org.hibernate.search.jakarta.batch.core.massindexing.impl.JobContextData; -import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.EntityTypeDescriptor; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.BatchCoreEntityTypeDescriptor; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.PersistenceUtil; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.SerializationUtil; -import org.hibernate.search.mapper.orm.loading.spi.ConditionalExpression; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoader; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoadingOptions; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; /** * Listener for managing the step indexing progress. @@ -66,21 +70,31 @@ public void beforeStep() throws IOException, ClassNotFoundException { stepProgress = new StepProgress(); JobContextData jobData = (JobContextData) jobContext.getTransientUserData(); EntityManagerFactory emf = jobData.getEntityManagerFactory(); - ConditionalExpression reindexOnly = + HibernateOrmBatchReindexCondition reindexOnly = SerializationUtil.parseReindexOnlyParameters( reindexOnlyHql, serializedReindexOnlyParameters ); try ( StatelessSession session = PersistenceUtil.openStatelessSession( emf, jobData.getTenancyConfiguration().convert( tenantId ) ) ) { - for ( EntityTypeDescriptor type : jobData.getEntityTypeDescriptors() ) { - Long rowCount = countAll( session, type, reindexOnly ); - JakartaBatchLog.INSTANCE.rowsToIndex( type.jpaEntityName(), rowCount ); - stepProgress.setRowsToIndex( type.jpaEntityName(), rowCount ); + ReindexConditionLoadingOptions options = new ReindexConditionLoadingOptions( reindexOnly, session ); + for ( BatchCoreEntityTypeDescriptor type : jobData.getEntityTypeDescriptors() ) { + try ( var loader = createIdentifierLoader( type, options ) ) { + OptionalLong count = loader.totalCount(); + if ( count.isPresent() ) { + JakartaBatchLog.INSTANCE.rowsToIndex( type.jpaEntityName(), count.getAsLong() ); + stepProgress.setRowsToIndex( type.jpaEntityName(), count.getAsLong() ); + } + } } } } stepContext.setTransientUserData( stepProgress ); } + private HibernateOrmBatchIdentifierLoader createIdentifierLoader(BatchCoreEntityTypeDescriptor type, + ReindexConditionLoadingOptions options) { + return type.batchLoadingStrategy().createIdentifierLoader( type, options ); + } + /** * Persist the step-level indexing progress after the end of the step's execution. This method is called when the * step is terminated by any reason, e.g. finished, stopped. @@ -91,12 +105,63 @@ public void afterStep() { stepContext.setPersistentUserData( stepProgress ); } - private static Long countAll(StatelessSession session, EntityTypeDescriptor type, ConditionalExpression reindexOnly) { - return type.createCountQuery( (SharedSessionContractImplementor) session, - reindexOnly == null ? List.of() : List.of( reindexOnly ) ) - .setReadOnly( true ) - .setCacheable( false ) - .setLockMode( LockModeType.NONE ) - .uniqueResult(); + private static class ReindexConditionLoadingOptions implements HibernateOrmBatchIdentifierLoadingOptions { + + private final HibernateOrmBatchReindexCondition reindexOnly; + private final Map, Object> contextData; + + ReindexConditionLoadingOptions(HibernateOrmBatchReindexCondition reindexOnly, StatelessSession session) { + this.reindexOnly = reindexOnly; + this.contextData = new HashMap<>(); + + this.contextData.put( StatelessSession.class, session ); + } + + @Override + public int fetchSize() { + return 1; + } + + @Override + public OptionalInt maxResults() { + return OptionalInt.empty(); + } + + @Override + public OptionalInt offset() { + return OptionalInt.empty(); + } + + @Override + public Optional reindexOnlyCondition() { + return Optional.ofNullable( reindexOnly ); + } + + @Override + public Optional upperBound() { + return Optional.empty(); + } + + @Override + public boolean upperBoundInclusive() { + return false; + } + + @Override + public Optional lowerBound() { + return Optional.empty(); + } + + @Override + public boolean lowerBoundInclusive() { + return false; + } + + @SuppressWarnings("unchecked") + @Override + public T context(Class contextType) { + return (T) contextData.get( contextType ); + } } + } diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/spi/EntityIdReader.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/spi/EntityIdReader.java index 2675bdf2a1d..f6279d7664d 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/spi/EntityIdReader.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/step/spi/EntityIdReader.java @@ -6,8 +6,10 @@ import java.io.IOException; import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; import jakarta.batch.api.BatchProperty; import jakarta.batch.api.chunk.AbstractItemReader; @@ -16,13 +18,8 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.LockModeType; -import org.hibernate.ScrollMode; -import org.hibernate.ScrollableResults; import org.hibernate.StatelessSession; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.query.SelectionQuery; import org.hibernate.search.jakarta.batch.core.context.jpa.spi.EntityManagerFactoryRegistry; import org.hibernate.search.jakarta.batch.core.inject.scope.spi.HibernateSearchPartitionScoped; import org.hibernate.search.jakarta.batch.core.logging.impl.JakartaBatchLog; @@ -30,13 +27,15 @@ import org.hibernate.search.jakarta.batch.core.massindexing.impl.JobContextData; import org.hibernate.search.jakarta.batch.core.massindexing.step.impl.HibernateSearchPartitionMapper; import org.hibernate.search.jakarta.batch.core.massindexing.step.impl.PartitionContextData; -import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.EntityTypeDescriptor; +import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.BatchCoreEntityTypeDescriptor; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.JobContextUtil; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.MassIndexingPartitionProperties; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.PartitionBound; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.PersistenceUtil; import org.hibernate.search.jakarta.batch.core.massindexing.util.impl.SerializationUtil; -import org.hibernate.search.mapper.orm.loading.spi.ConditionalExpression; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoader; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchIdentifierLoadingOptions; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; import org.hibernate.search.mapper.orm.tenancy.spi.TenancyConfiguration; import org.hibernate.search.util.common.impl.Closer; @@ -115,12 +114,12 @@ public class EntityIdReader extends AbstractItemReader { private String serializedUpperBound; private EntityManagerFactory emf; - private EntityTypeDescriptor type; + private BatchCoreEntityTypeDescriptor type; private TenancyConfiguration tenancyConfiguration; private int idFetchSize; private Integer maxResults; - private ConditionalExpression reindexOnly; + private HibernateOrmBatchReindexCondition reindexOnly; private Object upperBound; private Object lowerBound; @@ -255,7 +254,7 @@ private JobContextData getOrCreateJobContextData() { private class ChunkState implements AutoCloseable { private StatelessSession session; - private ScrollableResults scroll; + private HibernateOrmBatchIdentifierLoader identifierLoader; private CheckpointInfo lastCheckpointInfo; private int processedEntityCount = 0; @@ -267,23 +266,30 @@ public ChunkState(Serializable checkpointInfo) { /** * Get the next element for the current chunk. + * * @return The next element for this chunk. */ public Object next() { - if ( scroll == null ) { + if ( identifierLoader == null ) { start(); } - if ( !scroll.next() ) { + if ( !identifierLoader.hasNext() ) { return null; } - Object id = scroll.get(); + Object id = identifierLoader.next(); lastProcessedEntityId = id; ++processedEntityCount; return id; } + private HibernateOrmBatchIdentifierLoader createIdentifierLoader(BatchCoreEntityTypeDescriptor type, + HibernateOrmBatchIdentifierLoadingOptions options) { + return type.batchLoadingStrategy().createIdentifierLoader( type, options ); + } + /** * End a chunk. + * * @return The checkpoint info for the chunk that just ended. */ public Serializable end() { @@ -308,9 +314,9 @@ public Serializable end() { @Override public void close() { try ( Closer closer = new Closer<>() ) { - if ( scroll != null ) { - closer.push( ScrollableResults::close, scroll ); - scroll = null; + if ( identifierLoader != null ) { + closer.push( HibernateOrmBatchIdentifierLoader::close, identifierLoader ); + identifierLoader = null; } if ( session != null ) { closer.push( StatelessSession::close, session ); @@ -322,7 +328,13 @@ public void close() { private void start() { session = PersistenceUtil.openStatelessSession( emf, tenancyConfiguration.convert( tenantId ) ); try { - scroll = createScroll( type, session ); + boolean hasNoPreviousCheckpoint = lastCheckpointInfo == null; + HibernateOrmBatchIdentifierLoadingOptions options = new LoadingOptions( session, + idFetchSize, actualMaxResults(), upperBound, + hasNoPreviousCheckpoint ? lowerBound : lastCheckpointInfo.getLastProcessedEntityId(), + hasNoPreviousCheckpoint, reindexOnly + ); + identifierLoader = createIdentifierLoader( type, options ); } catch (Throwable t) { try { @@ -335,24 +347,7 @@ private void start() { } } - private ScrollableResults createScroll(EntityTypeDescriptor type, StatelessSession session) { - List conditions = new ArrayList<>(); - if ( reindexOnly != null ) { - conditions.add( reindexOnly ); - } - if ( upperBound != null ) { - conditions.add( type.idOrder().idLesser( "HIBERNATE_SEARCH_PARTITION_UPPER_BOUND_", upperBound ) ); - } - if ( lastCheckpointInfo != null ) { - conditions.add( type.idOrder().idGreater( "HIBERNATE_SEARCH_LAST_CHECKPOINT_", - lastCheckpointInfo.getLastProcessedEntityId() ) ); - } - else if ( lowerBound != null ) { - conditions.add( type.idOrder().idGreaterOrEqual( "HIBERNATE_SEARCH_PARTITION_LOWER_BOUND_", lowerBound ) ); - } - - SelectionQuery query = type.createIdentifiersQuery( (SharedSessionContractImplementor) session, conditions ); - + private Integer actualMaxResults() { if ( maxResults != null ) { int remaining; if ( lastCheckpointInfo != null ) { @@ -361,15 +356,9 @@ else if ( lowerBound != null ) { else { remaining = maxResults; } - query.setMaxResults( remaining ); + return remaining; } - - return query - .setReadOnly( true ) - .setCacheable( false ) - .setLockMode( LockModeType.NONE ) - .setFetchSize( idFetchSize ) - .scroll( ScrollMode.FORWARD_ONLY ); + return null; } } @@ -400,4 +389,75 @@ public String toString() { } } + private static class LoadingOptions implements HibernateOrmBatchIdentifierLoadingOptions { + private final HibernateOrmBatchReindexCondition reindexOnly; + private final Map, Object> contextData; + private final int fetchSize; + private final Integer maxResults; + private final Object upperBound; + private final Object lowerBound; + private final boolean lowerBoundInclusive; + + + LoadingOptions(StatelessSession session, + int fetchSize, Integer maxResults, Object upperBound, Object lowerBound, + boolean lowerBoundInclusive, HibernateOrmBatchReindexCondition reindexOnly) { + this.fetchSize = fetchSize; + this.maxResults = maxResults; + this.upperBound = upperBound; + this.lowerBound = lowerBound; + this.reindexOnly = reindexOnly; + this.lowerBoundInclusive = lowerBoundInclusive; + + this.contextData = new HashMap<>(); + + this.contextData.put( StatelessSession.class, session ); + } + + @Override + public int fetchSize() { + return fetchSize; + } + + @Override + public OptionalInt maxResults() { + return maxResults == null ? OptionalInt.empty() : OptionalInt.of( maxResults ); + } + + @Override + public OptionalInt offset() { + return OptionalInt.empty(); + } + + @Override + public Optional reindexOnlyCondition() { + return Optional.ofNullable( reindexOnly ); + } + + @Override + public Optional upperBound() { + return Optional.ofNullable( upperBound ); + } + + @Override + public boolean upperBoundInclusive() { + return false; + } + + @Override + public Optional lowerBound() { + return Optional.ofNullable( lowerBound ); + } + + @Override + public boolean lowerBoundInclusive() { + return lowerBoundInclusive; + } + + @SuppressWarnings("unchecked") + @Override + public T context(Class contextType) { + return (T) contextData.get( contextType ); + } + } } diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/BatchCoreEntityTypeDescriptor.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/BatchCoreEntityTypeDescriptor.java new file mode 100644 index 00000000000..9980c1d8bd6 --- /dev/null +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/BatchCoreEntityTypeDescriptor.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.jakarta.batch.core.massindexing.util.impl; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.search.jakarta.batch.core.massindexing.loading.impl.BatchCoreDefaultHibernateOrmBatchLoadingStrategy; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchLoadingStrategy; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchLoadingTypeContext; +import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmLoadingTypeContext; +import org.hibernate.search.mapper.pojo.model.spi.PojoRawTypeIdentifier; + +public class BatchCoreEntityTypeDescriptor implements HibernateOrmBatchLoadingTypeContext { + + public static BatchCoreEntityTypeDescriptor create(SessionFactoryImplementor sessionFactory, + HibernateOrmLoadingTypeContext type) { + HibernateOrmBatchLoadingStrategy batchLoadingStrategy = type.batchLoadingStrategy(); + if ( batchLoadingStrategy == null ) { + batchLoadingStrategy = new BatchCoreDefaultHibernateOrmBatchLoadingStrategy<>( type ); + } + return new BatchCoreEntityTypeDescriptor<>( type, batchLoadingStrategy ); + } + + private final HibernateOrmLoadingTypeContext delegate; + private final HibernateOrmBatchLoadingStrategy batchLoadingStrategy; + + public BatchCoreEntityTypeDescriptor(HibernateOrmLoadingTypeContext delegate, + HibernateOrmBatchLoadingStrategy batchLoadingStrategy) { + this.delegate = delegate; + this.batchLoadingStrategy = batchLoadingStrategy; + } + + public PojoRawTypeIdentifier typeIdentifier() { + return delegate.typeIdentifier(); + } + + @Override + public Class javaClass() { + return delegate.typeIdentifier().javaClass(); + } + + @Override + public String jpaEntityName() { + return delegate.jpaEntityName(); + } + + @Override + public String uniquePropertyName() { + return delegate.uniquePropertyName(); + } + + public HibernateOrmBatchLoadingStrategy batchLoadingStrategy() { + return batchLoadingStrategy; + } + +} diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/BatchCoreHqlReindexCondition.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/BatchCoreHqlReindexCondition.java new file mode 100644 index 00000000000..5036f10627b --- /dev/null +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/BatchCoreHqlReindexCondition.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.jakarta.batch.core.massindexing.util.impl; + +import java.util.Collections; +import java.util.Map; + +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; + +public class BatchCoreHqlReindexCondition implements HibernateOrmBatchReindexCondition { + + private final String reindexOnlyHql; + private final Map params; + + public BatchCoreHqlReindexCondition(String reindexOnlyHql, Map params) { + this.reindexOnlyHql = reindexOnlyHql; + this.params = Collections.unmodifiableMap( params ); + } + + @Override + public String conditionString() { + return reindexOnlyHql; + } + + @Override + public Map params() { + return params; + } +} diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/CompositeIdOrder.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/CompositeIdOrder.java index 983c4bd62a1..d2f033548ca 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/CompositeIdOrder.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/CompositeIdOrder.java @@ -6,7 +6,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import jakarta.persistence.EmbeddedId; import jakarta.persistence.IdClass; @@ -15,7 +17,7 @@ import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.model.domain.NavigableRole; -import org.hibernate.search.mapper.orm.loading.spi.ConditionalExpression; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmLoadingTypeContext; /** @@ -53,19 +55,13 @@ public CompositeIdOrder(HibernateOrmLoadingTypeContext type) { } @Override - public ConditionalExpression idGreater(String paramNamePrefix, Object idObj) { - return restrictLexicographically( paramNamePrefix, idObj, ">", false ); + public HibernateOrmBatchReindexCondition idGreater(String paramNamePrefix, Object idObj, boolean inclusive) { + return restrictLexicographically( paramNamePrefix, idObj, ">", inclusive ); } @Override - public ConditionalExpression idGreaterOrEqual(String paramNamePrefix, Object idObj) { - // Caution, using ">=" here won't cut it, we really need to separate the strict operator from the equals. - return restrictLexicographically( paramNamePrefix, idObj, ">", true ); - } - - @Override - public ConditionalExpression idLesser(String paramNamePrefix, Object idObj) { - return restrictLexicographically( paramNamePrefix, idObj, "<", false ); + public HibernateOrmBatchReindexCondition idLesser(String paramNamePrefix, Object idObj, boolean inclusive) { + return restrictLexicographically( paramNamePrefix, idObj, "<", inclusive ); } @Override @@ -81,7 +77,7 @@ public String ascOrder() { return builder.toString(); } - private ConditionalExpression restrictLexicographically(String paramNamePrefix, Object idObj, + private HibernateOrmBatchReindexCondition restrictLexicographically(String paramNamePrefix, Object idObj, String strictOperator, boolean orEquals) { List orClauses = new ArrayList<>(); @@ -109,12 +105,12 @@ private ConditionalExpression restrictLexicographically(String paramNamePrefix, orClauses.add( junction( andClauses, " and " ) ); } - var expression = new ConditionalExpression( junction( orClauses, " or " ) ); Object[] selectableValues = idMappingType.getValues( idObj ); + Map params = new HashMap<>(); for ( int i = 0; i < selectableValues.length; i++ ) { - expression.param( paramNamePrefix + i, selectableValues[i] ); + params.put( paramNamePrefix + i, selectableValues[i] ); } - return expression; + return new BatchCoreHqlReindexCondition( junction( orClauses, " or " ), params ); } private String toPath(ModelPart subPart) { diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/EntityTypeDescriptor.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/EntityTypeDescriptor.java deleted file mode 100644 index 4d733a07d38..00000000000 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/EntityTypeDescriptor.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.search.jakarta.batch.core.massindexing.util.impl; - -import java.util.List; -import java.util.Set; - -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.EmbeddableMappingType; -import org.hibernate.metamodel.mapping.EntityIdentifierMapping; -import org.hibernate.query.SelectionQuery; -import org.hibernate.search.mapper.orm.loading.spi.ConditionalExpression; -import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmEntityLoadingStrategy; -import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmLoadingTypeContext; -import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmQueryLoader; -import org.hibernate.search.mapper.pojo.model.spi.PojoRawTypeIdentifier; - -public class EntityTypeDescriptor { - - public static EntityTypeDescriptor create(SessionFactoryImplementor sessionFactory, - HibernateOrmLoadingTypeContext type) { - EntityIdentifierMapping identifierMapping = type.entityMappingType().getIdentifierMapping(); - IdOrder idOrder; - if ( identifierMapping.getPartMappingType() instanceof EmbeddableMappingType ) { - idOrder = new CompositeIdOrder<>( type ); - } - else { - idOrder = new SingularIdOrder<>( type ); - } - return new EntityTypeDescriptor<>( sessionFactory, type, type.loadingStrategy(), idOrder ); - } - - private final SessionFactoryImplementor sessionFactory; - private final HibernateOrmLoadingTypeContext delegate; - private final HibernateOrmEntityLoadingStrategy loadingStrategy; - private final IdOrder idOrder; - - public EntityTypeDescriptor(SessionFactoryImplementor sessionFactory, HibernateOrmLoadingTypeContext delegate, - HibernateOrmEntityLoadingStrategy loadingStrategy, IdOrder idOrder) { - this.sessionFactory = sessionFactory; - this.delegate = delegate; - this.loadingStrategy = loadingStrategy; - this.idOrder = idOrder; - } - - public PojoRawTypeIdentifier typeIdentifier() { - return delegate.typeIdentifier(); - } - - public Class javaClass() { - return delegate.typeIdentifier().javaClass(); - } - - public String jpaEntityName() { - return delegate.jpaEntityName(); - } - - public IdOrder idOrder() { - return idOrder; - } - - public SelectionQuery createCountQuery(SharedSessionContractImplementor session, - List conditions) { - return queryLoader( conditions, null ).createCountQuery( session ); - } - - public SelectionQuery createIdentifiersQuery(SharedSessionContractImplementor session, - List conditions) { - return queryLoader( conditions, idOrder.ascOrder() ).createIdentifiersQuery( session ); - } - - public SelectionQuery createLoadingQuery(SessionImplementor session, String idParameterName) { - return queryLoader( List.of(), null ).createLoadingQuery( session, idParameterName ); - } - - private HibernateOrmQueryLoader queryLoader(List conditions, String order) { - return loadingStrategy.createQueryLoader( sessionFactory, Set.of( delegate.delegate() ), conditions, order ); - } - -} diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/IdOrder.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/IdOrder.java index 971ff74f732..3ef475cd78d 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/IdOrder.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/IdOrder.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.jakarta.batch.core.massindexing.util.impl; -import org.hibernate.search.mapper.orm.loading.spi.ConditionalExpression; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; /** * Provides ID-based, order-sensitive restrictions @@ -19,23 +19,18 @@ public interface IdOrder { /** * @param paramNamePrefix A unique prefix for the name of parameters added by the resulting expression. * @param idObj The ID all results should be lesser than. + * @param inclusive Whether the {@code idObj} should also be included. * @return A "strictly greater than" restriction on the ID. */ - ConditionalExpression idGreater(String paramNamePrefix, Object idObj); - - /** - * @param paramNamePrefix A unique prefix for the name of parameters added by the resulting expression. - * @param idObj The ID all results should be lesser than. - * @return A "greater or equal" restriction on the ID. - */ - ConditionalExpression idGreaterOrEqual(String paramNamePrefix, Object idObj); + HibernateOrmBatchReindexCondition idGreater(String paramNamePrefix, Object idObj, boolean inclusive); /** * @param paramNamePrefix A unique prefix for the name of parameters added by the resulting expression. * @param idObj The ID all results should be lesser than. + * @param inclusive Whether the {@code idObj} should also be included. * @return A "lesser than" restriction on the ID. */ - ConditionalExpression idLesser(String paramNamePrefix, Object idObj); + HibernateOrmBatchReindexCondition idLesser(String paramNamePrefix, Object idObj, boolean inclusive); String ascOrder(); diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/JobContextUtil.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/JobContextUtil.java index 388c6484800..ebc6b1fb59d 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/JobContextUtil.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/JobContextUtil.java @@ -84,7 +84,7 @@ private static JobContextData createData(EntityManagerFactory emf, String entity entityTypesToIndex.add( mapping.typeContextProvider().byEntityName().getOrFail( s ) ); } - List> descriptors = PersistenceUtil.createDescriptors( + List> descriptors = PersistenceUtil.createDescriptors( emf.unwrap( SessionFactoryImplementor.class ), entityTypesToIndex ); diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/PartitionBound.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/PartitionBound.java index 3efc452a17a..53562aee355 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/PartitionBound.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/PartitionBound.java @@ -14,11 +14,11 @@ */ public class PartitionBound { - private EntityTypeDescriptor entityType; + private BatchCoreEntityTypeDescriptor entityType; private Object lowerBound; private Object upperBound; - public PartitionBound(EntityTypeDescriptor entityType, Object lowerBound, Object upperBound) { + public PartitionBound(BatchCoreEntityTypeDescriptor entityType, Object lowerBound, Object upperBound) { this.entityType = entityType; this.lowerBound = lowerBound; this.upperBound = upperBound; diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/PersistenceUtil.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/PersistenceUtil.java index 51f9b2598e5..52095222150 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/PersistenceUtil.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/PersistenceUtil.java @@ -73,11 +73,11 @@ public static StatelessSession openStatelessSession(EntityManagerFactory entityM return builder.openStatelessSession(); } - public static List> createDescriptors(SessionFactoryImplementor sessionFactory, + public static List> createDescriptors(SessionFactoryImplementor sessionFactory, Set> types) { - List> result = new ArrayList<>( types.size() ); + List> result = new ArrayList<>( types.size() ); for ( HibernateOrmLoadingTypeContext type : types ) { - result.add( EntityTypeDescriptor.create( sessionFactory, type ) ); + result.add( BatchCoreEntityTypeDescriptor.create( sessionFactory, type ) ); } return result; } diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/SerializationUtil.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/SerializationUtil.java index e06b80397ea..628ec2ee837 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/SerializationUtil.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/SerializationUtil.java @@ -15,7 +15,7 @@ import org.hibernate.CacheMode; import org.hibernate.search.jakarta.batch.core.logging.impl.JakartaBatchLog; -import org.hibernate.search.mapper.orm.loading.spi.ConditionalExpression; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.impl.StringHelper; @@ -106,21 +106,19 @@ private static > T parseEnumParameter(Class clazz, String k } } - public static ConditionalExpression parseReindexOnlyParameters(String reindexOnlyHql, + public static HibernateOrmBatchReindexCondition parseReindexOnlyParameters( + String reindexOnlyHql, String serializedReindexOnlyParameters) throws IOException, ClassNotFoundException { if ( reindexOnlyHql == null ) { return null; } else { - ConditionalExpression reindexOnly = new ConditionalExpression( reindexOnlyHql ); @SuppressWarnings("unchecked") - Map params = (Map) SerializationUtil.deserialize( serializedReindexOnlyParameters ); - if ( params != null ) { - params.forEach( reindexOnly::param ); - } - return reindexOnly; + Map params = (Map) SerializationUtil.deserialize( serializedReindexOnlyParameters ); + return new BatchCoreHqlReindexCondition( reindexOnlyHql, params ); } } + } diff --git a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/SingularIdOrder.java b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/SingularIdOrder.java index ef77c7df179..aba21781256 100644 --- a/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/SingularIdOrder.java +++ b/mapper/orm-jakarta-batch/core/src/main/java/org/hibernate/search/jakarta/batch/core/massindexing/util/impl/SingularIdOrder.java @@ -4,7 +4,9 @@ */ package org.hibernate.search.jakarta.batch.core.massindexing.util.impl; -import org.hibernate.search.mapper.orm.loading.spi.ConditionalExpression; +import java.util.Map; + +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchReindexCondition; import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmLoadingTypeContext; /** @@ -24,18 +26,13 @@ public SingularIdOrder(HibernateOrmLoadingTypeContext type) { } @Override - public ConditionalExpression idGreater(String paramNamePrefix, Object idObj) { - return restrict( paramNamePrefix, ">", idObj ); - } - - @Override - public ConditionalExpression idGreaterOrEqual(String paramNamePrefix, Object idObj) { - return restrict( paramNamePrefix, ">=", idObj ); + public HibernateOrmBatchReindexCondition idGreater(String paramNamePrefix, Object idObj, boolean inclusive) { + return restrict( paramNamePrefix, inclusive ? ">=" : ">", idObj ); } @Override - public ConditionalExpression idLesser(String paramNamePrefix, Object idObj) { - return restrict( paramNamePrefix, "<", idObj ); + public HibernateOrmBatchReindexCondition idLesser(String paramNamePrefix, Object idObj, boolean inclusive) { + return restrict( paramNamePrefix, inclusive ? "<=" : "<", idObj ); } @Override @@ -43,11 +40,12 @@ public String ascOrder() { return idPropertyName + " asc"; } - private ConditionalExpression restrict(String paramNamePrefix, String operator, Object idObj) { + private HibernateOrmBatchReindexCondition restrict(String paramNamePrefix, String operator, Object idObj) { String paramName = paramNamePrefix + "REF"; - var expression = new ConditionalExpression( idPropertyName + " " + operator + " :" + paramName ); - expression.param( paramName, idObj ); - return expression; + return new BatchCoreHqlReindexCondition( + idPropertyName + " " + operator + " :" + paramName, + Map.of( paramName, idObj ) + ); } } diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntityLoader.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntityLoader.java new file mode 100644 index 00000000000..1415d2fdf82 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntityLoader.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.mapper.orm.loading.batch; + +import java.util.List; + +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * A loader for mass loading of entities, used in particular during mass indexing. + *

+ * This loader: + *

    + *
  • Receives batches of identifiers from a {@link HibernateOrmBatchIdentifierLoader}
  • + *
  • Is expected to load a very large number of entities in multiple small batches.
  • + *
  • Pushes loaded entities to a sink.
  • + *
  • Sets up its own context (session, transactions, ...), instead of potentially relying on a pre-existing context.
  • + *
  • Is free to discard the entities after the sink is done processing them.
  • + *
+ * + */ +@Incubating +public interface HibernateOrmBatchEntityLoader extends AutoCloseable { + + /** + * Closes this {@link HibernateOrmBatchEntityLoader}. + */ + @Override + void close(); + + /** + * Loads the entities corresponding to the given identifiers and adds them to the given sink, + * blocking the current thread while doing so. + *

+ * Calls to the sink must be performed synchronously (before this method returns). + *

+ * Entities must be passed to the sink using a single call to {@link HibernateOrmBatchEntitySink#accept(List)}. + *

+ * Entities passed to the sink do not need to be the same order as {@code identifiers}. + * + * @param identifiers A list of identifiers of entities to load. + */ + void load(List identifiers); + +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntityLoadingOptions.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntityLoadingOptions.java new file mode 100644 index 00000000000..606f45bc4a8 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntityLoadingOptions.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.mapper.orm.loading.batch; + +import java.util.List; + +import org.hibernate.CacheMode; +import org.hibernate.search.util.common.annotation.Incubating; + +@Incubating +public interface HibernateOrmBatchEntityLoadingOptions { + + /** + * @return How many entities to load and index in each batch. + * Defines the maximum expected size of each list of IDs + * loaded by {@link HibernateOrmBatchIdentifierLoader#next()} + * and passed to {@link HibernateOrmBatchEntityLoader#load(List)}. + */ + int batchSize(); + + CacheMode cacheMode(); + + /** + * Search will add a {@link org.hibernate.StatelessSession} to the context by default. + */ + T context(Class contextType); +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntitySink.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntitySink.java new file mode 100644 index 00000000000..2f5f1d5e6ef --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchEntitySink.java @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.mapper.orm.loading.batch; + +import java.util.List; + +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * A sink for use by a {@link HibernateOrmBatchEntityLoader}. + * + * @param The type of loaded entities. + */ +@Incubating +public interface HibernateOrmBatchEntitySink { + + /** + * Adds a batch of entities to the sink. + *

+ * The list and entities need to stay usable at least until this method returns, + * as they will be consumed synchronously. + * Afterwards, they can be discarded or reused at will. + * + * @param batch The next batch of identifiers. Never {@code null}, never empty. + */ + void accept(List batch); + +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchIdentifierLoader.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchIdentifierLoader.java new file mode 100644 index 00000000000..223db34ba4f --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchIdentifierLoader.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.mapper.orm.loading.batch; + +import java.util.OptionalLong; + +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * TODO + */ +@Incubating +public interface HibernateOrmBatchIdentifierLoader extends AutoCloseable { + + /** + * Closes this {@link HibernateOrmBatchIdentifierLoader}. + */ + @Override + void close(); + + /** + * @return The total count of identifiers expected to be loaded. + */ + OptionalLong totalCount(); + + /** + * Loads the next identifier + */ + Object next(); + + boolean hasNext(); + +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchIdentifierLoadingOptions.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchIdentifierLoadingOptions.java new file mode 100644 index 00000000000..b9b670b9506 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchIdentifierLoadingOptions.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.mapper.orm.loading.batch; + +import java.util.Optional; +import java.util.OptionalInt; + +import org.hibernate.search.util.common.annotation.Incubating; + +@Incubating +public interface HibernateOrmBatchIdentifierLoadingOptions { + + int fetchSize(); + + OptionalInt maxResults(); + + OptionalInt offset(); + + Optional reindexOnlyCondition(); + + Optional upperBound(); + + boolean upperBoundInclusive(); + + Optional lowerBound(); + + boolean lowerBoundInclusive(); + + /** + * Search will add a {@link org.hibernate.StatelessSession} to the context by default. + */ + T context(Class contextType); +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchLoadingStrategy.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchLoadingStrategy.java new file mode 100644 index 00000000000..ab307e0714f --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchLoadingStrategy.java @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.mapper.orm.loading.batch; + +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * A strategy for mass loading, used in particular during mass indexing. + * + * @param The type of loaded entities. + * @param The type of entity identifiers. + */ +@Incubating +public interface HibernateOrmBatchLoadingStrategy { + + /** + * @param obj Another strategy + * @return {@code true} if the other strategy targets the same entity hierarchy + * and can be used as a replacement for this one. + * {@code false} otherwise or when unsure. + */ + @Override + boolean equals(Object obj); + + /* + * Hashcode must be overridden to be consistent with equals. + */ + @Override + int hashCode(); + + /** + * @param typeContext A representation of all entity types that will have to be loaded. + * @param options Loading options configured by the requester (who requested batch indexing, ...). + * @return An entity identifier loader. + */ + HibernateOrmBatchIdentifierLoader createIdentifierLoader(HibernateOrmBatchLoadingTypeContext typeContext, + HibernateOrmBatchIdentifierLoadingOptions options); + + /** + * @param typeContext A representation of all entity types that will have to be loaded. + * @param sink A sink to which the entity loader will pass loaded entities. + * @param options Loading options configured by the requester (who requested mass indexing, ...). + * @return An entity loader. + */ + HibernateOrmBatchEntityLoader createEntityLoader(HibernateOrmBatchLoadingTypeContext typeContext, + HibernateOrmBatchEntitySink sink, + HibernateOrmBatchEntityLoadingOptions options); + +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchLoadingTypeContext.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchLoadingTypeContext.java new file mode 100644 index 00000000000..f962124d113 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchLoadingTypeContext.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.mapper.orm.loading.batch; + +import org.hibernate.search.util.common.annotation.Incubating; + +@Incubating +public interface HibernateOrmBatchLoadingTypeContext { + + Class javaClass(); + + String jpaEntityName(); + + String uniquePropertyName(); + +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchReindexCondition.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchReindexCondition.java new file mode 100644 index 00000000000..5c9811c08a0 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/batch/HibernateOrmBatchReindexCondition.java @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.mapper.orm.loading.batch; + +import java.util.Map; + +import org.hibernate.search.util.common.annotation.Incubating; + +@Incubating +public interface HibernateOrmBatchReindexCondition { + String conditionString(); + + Map params(); +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/spi/ConditionalExpression.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/spi/ConditionalExpression.java index 9aa71ce112e..a20f65ed2ad 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/spi/ConditionalExpression.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/spi/ConditionalExpression.java @@ -4,6 +4,7 @@ */ package org.hibernate.search.mapper.orm.loading.spi; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -34,6 +35,10 @@ public void param(String name, Object value) { params.put( name, value ); } + public Map params() { + return Collections.unmodifiableMap( params ); + } + public void applyParams(Query query) { for ( Map.Entry entry : params.entrySet() ) { query.setParameter( entry.getKey(), entry.getValue() ); diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/spi/HibernateOrmLoadingTypeContext.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/spi/HibernateOrmLoadingTypeContext.java index ef6fbca1fac..1a38be4eef1 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/spi/HibernateOrmLoadingTypeContext.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/loading/spi/HibernateOrmLoadingTypeContext.java @@ -5,6 +5,7 @@ package org.hibernate.search.mapper.orm.loading.spi; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchLoadingStrategy; import org.hibernate.search.mapper.pojo.loading.spi.PojoLoadingTypeContext; import org.hibernate.search.mapper.pojo.model.spi.PojoRawTypeIdentifier; @@ -24,6 +25,8 @@ public interface HibernateOrmLoadingTypeContext { */ EntityMappingType entityMappingType(); - HibernateOrmEntityLoadingStrategy loadingStrategy(); + HibernateOrmBatchLoadingStrategy batchLoadingStrategy(); + + String uniquePropertyName(); } diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/mapping/impl/AbstractHibernateOrmTypeContext.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/mapping/impl/AbstractHibernateOrmTypeContext.java index 58e47d654f3..aa1f57d71ef 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/mapping/impl/AbstractHibernateOrmTypeContext.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/mapping/impl/AbstractHibernateOrmTypeContext.java @@ -9,6 +9,7 @@ import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.search.mapper.orm.event.impl.HibernateOrmListenerTypeContext; +import org.hibernate.search.mapper.orm.loading.batch.HibernateOrmBatchLoadingStrategy; import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmEntityLoadingStrategy; import org.hibernate.search.mapper.orm.loading.spi.HibernateOrmLoadingTypeContext; import org.hibernate.search.mapper.orm.model.impl.DocumentIdSourceProperty; @@ -31,7 +32,8 @@ abstract class AbstractHibernateOrmTypeContext private final String jpaEntityName; private final EntityMappingType entityMappingType; private final boolean documentIdIsEntityId; - private final HibernateOrmEntityLoadingStrategy loadingStrategy; + private final HibernateOrmBatchLoadingStrategy batchLoadingStrategy; + private final String uniquePropertyName; private final PojoPathFilter dirtyFilter; private final PojoPathFilter dirtyContainingAssociationFilter; @@ -44,7 +46,10 @@ abstract class AbstractHibernateOrmTypeContext this.entityMappingType = metamodel.getEntityDescriptor( builder.hibernateOrmEntityName ); this.documentIdIsEntityId = builder.documentIdSourceProperty != null && builder.documentIdSourceProperty.name.equals( entityMappingType.getIdentifierMapping().getAttributeName() ); - this.loadingStrategy = builder.loadingStrategy; + this.batchLoadingStrategy = builder.batchLoadingStrategy; + this.uniquePropertyName = builder.documentIdSourceProperty != null + ? builder.documentIdSourceProperty.name + : entityMappingType.getIdentifierMapping().getAttributeName(); this.dirtyFilter = builder.dirtyFilter; this.dirtyContainingAssociationFilter = builder.dirtyContainingAssociationFilter; } @@ -79,8 +84,13 @@ public EntityMappingType entityMappingType() { } @Override - public HibernateOrmEntityLoadingStrategy loadingStrategy() { - return loadingStrategy; + public HibernateOrmBatchLoadingStrategy batchLoadingStrategy() { + return batchLoadingStrategy; + } + + @Override + public String uniquePropertyName() { + return uniquePropertyName; } @Override @@ -113,7 +123,7 @@ public abstract static class Builder implements PojoTypeExtendedMappingCollec private DocumentIdSourceProperty documentIdSourceProperty; private PojoPathFilter dirtyFilter; private PojoPathFilter dirtyContainingAssociationFilter; - private HibernateOrmEntityLoadingStrategy loadingStrategy; + private HibernateOrmBatchLoadingStrategy batchLoadingStrategy; Builder(PojoRawTypeModel typeModel, PersistentClass persistentClass) { this.typeIdentifier = typeModel.typeIdentifier(); @@ -141,11 +151,14 @@ public void dirtyContainingAssociationFilter(PojoPathFilter filter) { @SuppressWarnings("unchecked") // The binder uses reflection to create a strategy of the appropriate type public void applyLoadingBinder(Object binder, PojoEntityLoadingBindingContext context) { var castBinder = (HibernateOrmEntityLoadingBinder) binder; - this.loadingStrategy = (HibernateOrmEntityLoadingStrategy) castBinder - .createLoadingStrategy( persistentClass, documentIdSourceProperty ); - if ( this.loadingStrategy != null ) { - context.selectionLoadingStrategy( typeIdentifier.javaClass(), this.loadingStrategy ); - context.massLoadingStrategy( typeIdentifier.javaClass(), this.loadingStrategy ); + // TODO: we'll implement this one a biiiiiiiiiiit later ... + this.batchLoadingStrategy = null; + + HibernateOrmEntityLoadingStrategy loadingStrategy = (HibernateOrmEntityLoadingStrategy) castBinder.createLoadingStrategy( persistentClass, documentIdSourceProperty ); + if ( loadingStrategy != null ) { + context.selectionLoadingStrategy( typeIdentifier.javaClass(), loadingStrategy ); + context.massLoadingStrategy( typeIdentifier.javaClass(), loadingStrategy ); } } } diff --git a/mapper/pojo-standalone/src/main/java/org/hibernate/search/mapper/pojo/standalone/loading/MassEntitySink.java b/mapper/pojo-standalone/src/main/java/org/hibernate/search/mapper/pojo/standalone/loading/MassEntitySink.java index afc0bdc0db9..d4b0ea8ef77 100644 --- a/mapper/pojo-standalone/src/main/java/org/hibernate/search/mapper/pojo/standalone/loading/MassEntitySink.java +++ b/mapper/pojo-standalone/src/main/java/org/hibernate/search/mapper/pojo/standalone/loading/MassEntitySink.java @@ -9,7 +9,7 @@ import org.hibernate.search.util.common.annotation.Incubating; /** - * A sink for use by a {@link MassIdentifierLoader}. + * A sink for use by a {@link MassEntityLoader}. * * @param The type of loaded entities. */