diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java index 8ca4ac692d8ad..3959bb5f7072f 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java @@ -11,6 +11,7 @@ import java.util.Set; import org.hibernate.annotations.TimeZoneStorageType; +import org.hibernate.id.enhanced.StandardOptimizerDescriptor; import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigDocSection; @@ -381,8 +382,84 @@ public static class HibernateOrmConfigPersistenceUnitMapping { @ConfigItem(name = "timezone.default-storage", defaultValueDocumentation = "default") public Optional timeZoneDefaultStorage; + /** + * The optimizer to apply to identifier generators + * whose optimizer is not configured explicitly. + * + * Only relevant for table- and sequence-based identifier generators. + * Other generators, such as UUID-based generators, will ignore this setting. + * + * The optimizer is responsible for pooling new identifier values, + * in order to reduce the frequency of database calls to retrieve those values + * and thereby improve performance. + * + * @asciidoclet + */ + @ConfigItem(name = "id.optimizer.default", defaultValueDocumentation = "pooled-lo") + // Note this needs to be a build-time property due to + // org.hibernate.boot.internal.InFlightMetadataCollectorImpl.handleIdentifierValueBinding + // which may call (indirectly) org.hibernate.id.enhanced.SequenceStructure.buildSequence + // whose output depends on org.hibernate.id.enhanced.SequenceStructure.applyIncrementSizeToSourceValues + // which is determined by the optimizer. + public Optional idOptimizerDefault; + public boolean isAnyPropertySet() { - return timeZoneDefaultStorage.isPresent(); + return timeZoneDefaultStorage.isPresent() + || idOptimizerDefault.isPresent(); + } + + } + + public enum IdOptimizerType { + /** + * Assumes the value retrieved from the table/sequence is the lower end of the pool. + * + * Upon retrieving value `N`, the new pool of identifiers will go from `N` to `N + - 1`, inclusive. + * `pooled`:: + * Assumes the value retrieved from the table/sequence is the higher end of the pool. + * + + * Upon retrieving value `N`, the new pool of identifiers will go from `N - ` to `N + + * - 1`, inclusive. + * + + * The first value, `1`, is handled differently to avoid negative identifiers. + * + + * Use this to get the legacy behavior of Quarkus 2 / Hibernate ORM 5 or older. + * `none`:: + * No optimizer, resulting in a database call each and every time an identifier value is needed from the generator. + * + + * Not recommended in production environments: + * may result in degraded performance and/or frequent gaps in identifier values. + * + * @asciidoclet + */ + POOLED_LO(StandardOptimizerDescriptor.POOLED_LO), + /** + * Assumes the value retrieved from the table/sequence is the higher end of the pool. + * + * Upon retrieving value `N`, the new pool of identifiers will go from `N - ` to `N + + * - 1`, inclusive. + * + * The first value, `1`, is handled differently to avoid negative identifiers. + * + * Use this to get the legacy behavior of Quarkus 2 / Hibernate ORM 5 or older. + * + * @asciidoclet + */ + POOLED(StandardOptimizerDescriptor.POOLED), + /** + * No optimizer, resulting in a database call each and every time an identifier value is needed from the generator. + * + * Not recommended in production environments: + * may result in degraded performance and/or frequent gaps in identifier values. + * + * @asciidoclet + */ + NONE(StandardOptimizerDescriptor.NONE); + + public final String configName; + + IdOptimizerType(StandardOptimizerDescriptor delegate) { + configName = delegate.getExternalName(); } } diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index 99cfa6bb50474..bf2f09ef4c3a5 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -1022,6 +1022,9 @@ private static void producePersistenceUnitDescriptorFromConfig( descriptor.getProperties().setProperty(AvailableSettings.TIMEZONE_DEFAULT_STORAGE, persistenceUnitConfig.mapping.timeZoneDefaultStorage.get().name()); } + descriptor.getProperties().setProperty(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, + persistenceUnitConfig.mapping.idOptimizerDefault + .orElse(HibernateOrmConfigPersistenceUnit.IdOptimizerType.POOLED_LO).configName); //charset descriptor.getProperties().setProperty(AvailableSettings.HBM2DDL_CHARSET_NAME, diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/SchemaUtil.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/SchemaUtil.java index d1223244e886a..e2f936ecdf31c 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/SchemaUtil.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/SchemaUtil.java @@ -7,6 +7,7 @@ import jakarta.persistence.EntityManagerFactory; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.generator.Generator; import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; @@ -50,4 +51,11 @@ public void accept(int selectionIndex, SelectableMapping selectableMapping) { entityDescriptor.forEachSelectable(columnFinder); return columnFinder.found.getJdbcMapping().getJdbcType().getFriendlyName(); } + + public static Generator getGenerator(EntityManagerFactory entityManagerFactory, Class entityType) { + MappingMetamodel domainModel = entityManagerFactory + .unwrap(SessionFactoryImplementor.class).getRuntimeMetamodels().getMappingMetamodel(); + EntityPersister entityDescriptor = domainModel.findEntityDescriptor(entityType); + return entityDescriptor.getGenerator(); + } } diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/AbstractIdOptimizerDefaultTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/AbstractIdOptimizerDefaultTest.java new file mode 100644 index 0000000000000..a41e595fe1268 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/AbstractIdOptimizerDefaultTest.java @@ -0,0 +1,69 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import jakarta.inject.Inject; + +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.id.OptimizableGenerator; +import org.hibernate.id.enhanced.Optimizer; +import org.hibernate.id.enhanced.PooledLoOptimizer; +import org.hibernate.id.enhanced.PooledOptimizer; +import org.junit.jupiter.api.Test; + +import io.quarkus.hibernate.orm.SchemaUtil; +import io.quarkus.narayana.jta.QuarkusTransaction; + +public abstract class AbstractIdOptimizerDefaultTest { + + @Inject + SessionFactory sessionFactory; + + @Inject + Session session; + + abstract Class defaultOptimizerType(); + + @Test + public void defaults() { + assertThat(List.of( + EntityWithDefaultGenerator.class, + EntityWithGenericGenerator.class, + EntityWithSequenceGenerator.class, + EntityWithTableGenerator.class)) + .allSatisfy(c -> assertOptimizer(c).isInstanceOf(defaultOptimizerType())); + } + + @Test + public void explicitOverrides() { + assertOptimizer(EntityWithGenericGeneratorAndPooledOptimizer.class) + .isInstanceOf(PooledOptimizer.class); + assertOptimizer(EntityWithGenericGeneratorAndPooledLoOptimizer.class) + .isInstanceOf(PooledLoOptimizer.class); + } + + @Test + public void ids() { + for (long i = 1; i <= 51; i++) { + assertThat(QuarkusTransaction.requiringNew().call(() -> { + var entity = new EntityWithSequenceGenerator(); + session.persist(entity); + return entity.id; + })) + .isEqualTo(i); + } + } + + AbstractObjectAssert assertOptimizer(Class entityType) { + return assertThat(SchemaUtil.getGenerator(sessionFactory, entityType)) + .as("ID generator for entity type " + entityType.getSimpleName()) + .asInstanceOf(InstanceOfAssertFactories.type(OptimizableGenerator.class)) + .extracting(OptimizableGenerator::getOptimizer) + .as("ID optimizer for entity type " + entityType.getSimpleName()); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithDefaultGenerator.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithDefaultGenerator.java new file mode 100644 index 0000000000000..edbb3ad6c5efa --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithDefaultGenerator.java @@ -0,0 +1,17 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class EntityWithDefaultGenerator { + + @Id + @GeneratedValue + Long id; + + public EntityWithDefaultGenerator() { + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGenerator.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGenerator.java new file mode 100644 index 0000000000000..28e905a0c1a58 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGenerator.java @@ -0,0 +1,21 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.id.enhanced.SequenceStyleGenerator; + +@Entity +public class EntityWithGenericGenerator { + + @Id + @GeneratedValue(generator = "gen_gen") + @GenericGenerator(name = "gen_gen", type = SequenceStyleGenerator.class) + Long id; + + public EntityWithGenericGenerator() { + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGeneratorAndPooledLoOptimizer.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGeneratorAndPooledLoOptimizer.java new file mode 100644 index 0000000000000..4104926d0d767 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGeneratorAndPooledLoOptimizer.java @@ -0,0 +1,23 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.hibernate.id.OptimizableGenerator; +import org.hibernate.id.enhanced.SequenceStyleGenerator; + +@Entity +public class EntityWithGenericGeneratorAndPooledLoOptimizer { + + @Id + @GeneratedValue(generator = "gen_gen_pooled_lo") + @GenericGenerator(name = "gen_gen_pooled_lo", type = SequenceStyleGenerator.class, parameters = @Parameter(name = OptimizableGenerator.OPT_PARAM, value = "pooled-lo")) + Long id; + + public EntityWithGenericGeneratorAndPooledLoOptimizer() { + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGeneratorAndPooledOptimizer.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGeneratorAndPooledOptimizer.java new file mode 100644 index 0000000000000..ab08b1d857c0e --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithGenericGeneratorAndPooledOptimizer.java @@ -0,0 +1,23 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.hibernate.id.OptimizableGenerator; +import org.hibernate.id.enhanced.SequenceStyleGenerator; + +@Entity +public class EntityWithGenericGeneratorAndPooledOptimizer { + + @Id + @GeneratedValue(generator = "gen_gen_pooled_lo") + @GenericGenerator(name = "gen_gen_pooled_lo", type = SequenceStyleGenerator.class, parameters = @Parameter(name = OptimizableGenerator.OPT_PARAM, value = "pooled")) + Long id; + + public EntityWithGenericGeneratorAndPooledOptimizer() { + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithSequenceGenerator.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithSequenceGenerator.java new file mode 100644 index 0000000000000..02ba281ec0323 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithSequenceGenerator.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; + +@Entity +public class EntityWithSequenceGenerator { + + @Id + @GeneratedValue(generator = "seq_gen") + @SequenceGenerator(name = "seq_gen") + Long id; + + public EntityWithSequenceGenerator() { + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithTableGenerator.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithTableGenerator.java new file mode 100644 index 0000000000000..09d4c9d86b1dc --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/EntityWithTableGenerator.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.TableGenerator; + +@Entity +public class EntityWithTableGenerator { + + @Id + @GeneratedValue(generator = "tab_gen") + @TableGenerator(name = "tab_gen") + Long id; + + public EntityWithTableGenerator() { + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultDefaultTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultDefaultTest.java new file mode 100644 index 0000000000000..2588cbe2e8933 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultDefaultTest.java @@ -0,0 +1,25 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import org.hibernate.id.enhanced.PooledLoOptimizer; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.SchemaUtil; +import io.quarkus.test.QuarkusUnitTest; + +public class IdOptimizerDefaultDefaultTest extends AbstractIdOptimizerDefaultTest { + + @RegisterExtension + static QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(EntityWithDefaultGenerator.class, EntityWithGenericGenerator.class, + EntityWithSequenceGenerator.class, EntityWithTableGenerator.class, + EntityWithGenericGeneratorAndPooledOptimizer.class, + EntityWithGenericGeneratorAndPooledLoOptimizer.class) + .addClasses(SchemaUtil.class)) + .withConfigurationResource("application.properties"); + + @Override + Class defaultOptimizerType() { + return PooledLoOptimizer.class; + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultNoneTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultNoneTest.java new file mode 100644 index 0000000000000..01c1f2359bce7 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultNoneTest.java @@ -0,0 +1,33 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import org.hibernate.id.enhanced.NoopOptimizer; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.SchemaUtil; +import io.quarkus.test.QuarkusUnitTest; + +public class IdOptimizerDefaultNoneTest extends AbstractIdOptimizerDefaultTest { + + @RegisterExtension + static QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(EntityWithDefaultGenerator.class, EntityWithGenericGenerator.class, + EntityWithSequenceGenerator.class, EntityWithTableGenerator.class, + EntityWithGenericGeneratorAndPooledOptimizer.class, + EntityWithGenericGeneratorAndPooledLoOptimizer.class) + .addClasses(SchemaUtil.class)) + .withConfigurationResource("application.properties") + .overrideConfigKey("quarkus.hibernate-orm.mapping.id.optimizer.default", "none"); + + @Override + @Disabled("The 'none' optimizer will produce a different stream of IDs (1 then 51 then 101 then ...)") + public void ids() { + super.ids(); + } + + @Override + Class defaultOptimizerType() { + return NoopOptimizer.class; + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultPooledLoTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultPooledLoTest.java new file mode 100644 index 0000000000000..ac8b6ff722a29 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultPooledLoTest.java @@ -0,0 +1,26 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import org.hibernate.id.enhanced.PooledLoOptimizer; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.SchemaUtil; +import io.quarkus.test.QuarkusUnitTest; + +public class IdOptimizerDefaultPooledLoTest extends AbstractIdOptimizerDefaultTest { + + @RegisterExtension + static QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(EntityWithDefaultGenerator.class, EntityWithGenericGenerator.class, + EntityWithSequenceGenerator.class, EntityWithTableGenerator.class, + EntityWithGenericGeneratorAndPooledOptimizer.class, + EntityWithGenericGeneratorAndPooledLoOptimizer.class) + .addClasses(SchemaUtil.class)) + .withConfigurationResource("application.properties") + .overrideConfigKey("quarkus.hibernate-orm.mapping.id.optimizer.default", "pooled-lo"); + + @Override + Class defaultOptimizerType() { + return PooledLoOptimizer.class; + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultPooledTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultPooledTest.java new file mode 100644 index 0000000000000..219c4905300c4 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/mapping/id/optimizer/IdOptimizerDefaultPooledTest.java @@ -0,0 +1,26 @@ +package io.quarkus.hibernate.orm.mapping.id.optimizer; + +import org.hibernate.id.enhanced.PooledOptimizer; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.SchemaUtil; +import io.quarkus.test.QuarkusUnitTest; + +public class IdOptimizerDefaultPooledTest extends AbstractIdOptimizerDefaultTest { + + @RegisterExtension + static QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(EntityWithDefaultGenerator.class, EntityWithGenericGenerator.class, + EntityWithSequenceGenerator.class, EntityWithTableGenerator.class, + EntityWithGenericGeneratorAndPooledOptimizer.class, + EntityWithGenericGeneratorAndPooledLoOptimizer.class) + .addClasses(SchemaUtil.class)) + .withConfigurationResource("application.properties") + .overrideConfigKey("quarkus.hibernate-orm.mapping.id.optimizer.default", "pooled"); + + @Override + Class defaultOptimizerType() { + return PooledOptimizer.class; + } +}