Skip to content

HHH-19420 - Support batch-size for collections in mapping.xml #10106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public interface JaxbPluralAttribute extends JaxbPersistentAttribute, JaxbLockab
JaxbCollectionIdImpl getCollectionId();
void setCollectionId(JaxbCollectionIdImpl id);

Integer getBatchSize();
void setBatchSize(Integer size);

LimitedCollectionClassification getClassification();
void setClassification(LimitedCollectionClassification value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.hibernate.boot.jaxb.mapping.spi.JaxbPluralFetchModeImpl;
import org.hibernate.boot.models.HibernateAnnotations;
import org.hibernate.boot.models.JpaAnnotations;
import org.hibernate.boot.models.annotations.internal.BatchSizeAnnotation;
import org.hibernate.boot.models.annotations.internal.FetchAnnotation;
import org.hibernate.boot.models.annotations.internal.MapKeyClassJpaAnnotation;
import org.hibernate.boot.models.annotations.internal.MapKeyColumnJpaAnnotation;
Expand Down Expand Up @@ -62,6 +63,14 @@ public static void applyPluralAttributeStructure(
}
}

if ( jaxbPluralAttribute.getBatchSize() != null ) {
final BatchSizeAnnotation batchSizeAnnotation = (BatchSizeAnnotation) memberDetails.applyAnnotationUsage(
HibernateAnnotations.BATCH_SIZE,
buildingContext
);
batchSizeAnnotation.size( jaxbPluralAttribute.getBatchSize() );
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// collection-structure

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3313,6 +3313,9 @@
</xsd:sequence>
</xsd:choice>

<!-- @BatchSize -->
<xsd:element name="batch-size" type="xsd:int" minOccurs="0"/>

<xsd:element name="sql-restriction" type="xsd:string" minOccurs="0"/>

<xsd:element name="sql-insert" type="orm:custom-sql" minOccurs="0"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@
*/
package org.hibernate.orm.test.batchfetch;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.hibernate.Hibernate;
import org.hibernate.cfg.AvailableSettings;

import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
Expand All @@ -19,6 +13,12 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.cfg.CacheSettings.USE_SECOND_LEVEL_CACHE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand All @@ -27,166 +27,118 @@
/**
* @author Gavin King
*/
@SuppressWarnings("JUnitMalformedDeclaration")
@DomainModel(
xmlMappings = "org/hibernate/orm/test/batchfetch/ProductLine.hbm.xml",
xmlMappings = "org/hibernate/orm/test/batchfetch/ProductLine.xml",
annotatedClasses = BatchLoadableEntity.class
)
@SessionFactory(
generateStatistics = true
)
@ServiceRegistry(
settings = {
@Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "false")
}
)
@SessionFactory(generateStatistics = true)
@ServiceRegistry(settings = @Setting(name = USE_SECOND_LEVEL_CACHE, value = "false"))
public class BatchFetchTest {

@SuppressWarnings("unchecked")
@Test
public void testBatchFetch(SessionFactoryScope scope) {
ProductLine ossProductLine = new ProductLine();
Model hibernateModel = new Model( ossProductLine );
scope.inTransaction(
session -> {
ProductLine cars = new ProductLine();
cars.setDescription( "Cars" );
Model monaro = new Model( cars );
monaro.setName( "monaro" );
monaro.setDescription( "Holden Monaro" );
Model hsv = new Model( cars );
hsv.setName( "hsv" );
hsv.setDescription( "Holden Commodore HSV" );
session.persist( cars );

ossProductLine.setDescription( "OSS" );
Model jboss = new Model( ossProductLine );
jboss.setName( "JBoss" );
jboss.setDescription( "JBoss Application Server" );

hibernateModel.setName( "Hibernate" );
hibernateModel.setDescription( "Hibernate" );
Model cache = new Model( ossProductLine );
cache.setName( "JBossCache" );
cache.setDescription( "JBoss TreeCache" );
session.persist( ossProductLine );
}
);

scope.getSessionFactory().getCache().evictEntityData( Model.class );
scope.getSessionFactory().getCache().evictEntityData( ProductLine.class );

scope.inTransaction(
session -> {
List<ProductLine> list = session.createQuery( "from ProductLine pl order by pl.description" )
.list();
ProductLine cars = list.get( 0 );
ProductLine oss = list.get( 1 );
assertFalse( Hibernate.isInitialized( cars.getModels() ) );
assertFalse( Hibernate.isInitialized( oss.getModels() ) );
assertEquals( 2, cars.getModels().size() ); //fetch both collections
assertTrue( Hibernate.isInitialized( cars.getModels() ) );
assertTrue( Hibernate.isInitialized( oss.getModels() ) );

session.clear();

List<Model> models = session.createQuery( "from Model m" ).list();
Model hibernate = session.get( Model.class, hibernateModel.getId() );
hibernate.getProductLine().getId();
for ( Model aList : models ) {
assertFalse( Hibernate.isInitialized( aList.getProductLine() ) );
}
assertEquals( hibernate.getProductLine().getDescription(), "OSS" ); //fetch both productlines

session.clear();

Iterator<Model> iter = session.createQuery( "from Model" ).list().iterator();
models = new ArrayList();
while ( iter.hasNext() ) {
models.add( iter.next() );
}
Model m = models.get( 0 );
m.getDescription(); //fetch a batch of 4

session.clear();

list = session.createQuery( "from ProductLine" ).list();
ProductLine pl = list.get( 0 );
ProductLine pl2 = list.get( 1 );
session.evict( pl2 );
pl.getModels().size(); //fetch just one collection! (how can we write an assertion for that??)
}
);

scope.inTransaction(
session -> {
List<ProductLine> list = session.createQuery( "from ProductLine pl order by pl.description" )
.list();
ProductLine cars = list.get( 0 );
ProductLine oss = list.get( 1 );
assertEquals( cars.getModels().size(), 2 );
assertEquals( oss.getModels().size(), 3 );
session.remove( cars );
session.remove( oss );
}
);
ProductLine ossProductLine = new ProductLine( "OSS" );
Model hibernateModel = new Model( "Hibernate", "Hibernate", ossProductLine );
scope.inTransaction( (session) -> {
ProductLine cars = new ProductLine( "Cars" );
new Model( "monaro", "Holden Monaro", cars );
new Model( "hsv", "Holden Commodore HSV", cars );
session.persist( cars );

ossProductLine.setDescription( "OSS" );
new Model( "JBoss", "JBoss Application Server", ossProductLine );
new Model( "JBossCache", "JBoss TreeCache", ossProductLine );
session.persist( ossProductLine );
} );

scope.inTransaction( (session) -> {
List<ProductLine> list = session.createQuery( "from ProductLine pl order by pl.description" ).list();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note test

Invoking
QueryProducerImplementor.createQuery
should be avoided because it has been deprecated.
ProductLine cars = list.get( 0 );
ProductLine oss = list.get( 1 );
assertFalse( Hibernate.isInitialized( cars.getModels() ) );
assertFalse( Hibernate.isInitialized( oss.getModels() ) );
assertEquals( 2, cars.getModels().size() ); //fetch both collections
assertTrue( Hibernate.isInitialized( cars.getModels() ) );
assertTrue( Hibernate.isInitialized( oss.getModels() ) );
} );

scope.inTransaction( (session) -> {
List<Model> models = session.createQuery( "from Model m" ).list();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note test

Invoking
QueryProducerImplementor.createQuery
should be avoided because it has been deprecated.
Model hibernate = session.find( Model.class, hibernateModel.getId() );
hibernate.getProductLine().getId();
for ( Model aList : models ) {
assertFalse( Hibernate.isInitialized( aList.getProductLine() ) );
}
//fetch both product lines
assertThat( hibernate.getProductLine().getDescription() ).isEqualTo( "OSS" );
} );

scope.inTransaction( (session) -> {
Iterator<Model> iter = session.createQuery( "from Model" ).list().iterator();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note test

Invoking
QueryProducerImplementor.createQuery
should be avoided because it has been deprecated.
ArrayList<Model> models = new ArrayList<>();
while ( iter.hasNext() ) {
models.add( iter.next() );
}
Model m = models.get( 0 );
m.getDescription(); //fetch a batch of 4

session.clear();

List<ProductLine> list = session.createQuery( "from ProductLine" ).list();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note test

Invoking
QueryProducerImplementor.createQuery
should be avoided because it has been deprecated.
ProductLine pl = list.get( 0 );
ProductLine pl2 = list.get( 1 );
session.evict( pl2 );
pl.getModels().size(); //fetch just one collection! (how can we write an assertion for that??)
} );

scope.inTransaction( (session) -> {
List<ProductLine> list = session.createQuery( "from ProductLine pl order by pl.description" ).list();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note test

Invoking
QueryProducerImplementor.createQuery
should be avoided because it has been deprecated.
ProductLine cars = list.get( 0 );
ProductLine oss = list.get( 1 );
assertThat( cars.getModels().size() ).isEqualTo( 2 );
assertThat( oss.getModels().size() ).isEqualTo( 3 );
} );
}

@Test
@SuppressWarnings("unchecked")
public void testBatchFetch2(SessionFactoryScope scope) {
int size = 32 + 14;
scope.inTransaction(
session -> {
for ( int i = 0; i < size; i++ ) {
session.persist( new BatchLoadableEntity( i ) );
}
}
);

scope.inTransaction(
session -> {
// load them all as proxies
for ( int i = 0; i < size; i++ ) {
BatchLoadableEntity entity = session.getReference( BatchLoadableEntity.class, i );
assertFalse( Hibernate.isInitialized( entity ) );
}
scope.getSessionFactory().getStatistics().clear();
// now start initializing them...
for ( int i = 0; i < size; i++ ) {
BatchLoadableEntity entity = session.getReference( BatchLoadableEntity.class, i );
Hibernate.initialize( entity );
assertTrue( Hibernate.isInitialized( entity ) );
}
// so at this point, all entities are initialized. see how many fetches were performed.
final int expectedFetchCount;
// if ( sessionFactory().getSettings().getBatchFetchStyle() == BatchFetchStyle.LEGACY ) {
// expectedFetchCount = 3; // (32 + 10 + 4)
// }
// else if ( sessionFactory().getSettings().getBatchFetchStyle() == BatchFetchStyle.DYNAMIC ) {
// expectedFetchCount = 2; // (32 + 14) : because we limited batch-size to 32
// }
// else {
// PADDED
expectedFetchCount = 2; // (32 + 16*) with the 16 being padded
// }
assertEquals(
expectedFetchCount,
scope.getSessionFactory().getStatistics()
.getEntityStatistics( BatchLoadableEntity.class.getName() )
.getFetchCount()
);
}
);
scope.inTransaction( (session) -> {
for ( int i = 0; i < size; i++ ) {
session.persist( new BatchLoadableEntity( i ) );
}
} );

scope.inTransaction( (session) -> {
// load them all as proxies
for ( int i = 0; i < size; i++ ) {
BatchLoadableEntity entity = session.getReference( BatchLoadableEntity.class, i );
assertFalse( Hibernate.isInitialized( entity ) );
}
scope.getSessionFactory().getStatistics().clear();
// now start initializing them...
for ( int i = 0; i < size; i++ ) {
BatchLoadableEntity entity = session.getReference( BatchLoadableEntity.class, i );
Hibernate.initialize( entity );
assertTrue( Hibernate.isInitialized( entity ) );
}
// so at this point, all entities are initialized. see how many fetches were performed.
final int expectedFetchCount;
expectedFetchCount = 2; // (32 + 16*) with the 16 being padded

assertEquals(
expectedFetchCount,
scope.getSessionFactory().getStatistics()
.getEntityStatistics( BatchLoadableEntity.class.getName() )
.getFetchCount()
);
} );
}

@AfterEach
public void tearDown(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery( "delete BatchLoadableEntity" ).executeUpdate();
session.createQuery( "delete Model" ).executeUpdate();
session.createQuery( "delete ProductLine" ).executeUpdate();
}
);
scope.dropData();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@
* @author Gavin King
*/
public class Model {
private String id;
private Integer id;
private String name;
private String description;
private ProductLine productLine;

Model() {}
Model() {
}

public Model(ProductLine pl) {
this.productLine = pl;
pl.getModels().add(this);
public Model(String name, String description, ProductLine productLine) {
this.name = name;
this.description = description;
this.productLine = productLine;
productLine.getModels().add(this);
}

public String getDescription() {
Expand All @@ -27,10 +30,10 @@ public String getDescription() {
public void setDescription(String description) {
this.description = description;
}
public String getId() {
public Integer getId() {
return id;
}
public void setId(String id) {
public void setId(Integer id) {
this.id = id;
}
public String getName() {
Expand Down
Loading
Loading