Skip to content

Commit

Permalink
Merge pull request #32044 from yrodiere/reactive-orm-features
Browse files Browse the repository at this point in the history
Port some Hibernate ORM extension features to the Hibernate Reactive extension
  • Loading branch information
gsmet authored Mar 22, 2023
2 parents 7d7bc87 + 05c05a9 commit ce446c7
Show file tree
Hide file tree
Showing 24 changed files with 877 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ private static ParsedPersistenceXmlDescriptor generateReactivePersistenceUnit(
namingStrategy -> desc.getProperties()
.setProperty(AvailableSettings.IMPLICIT_NAMING_STRATEGY, namingStrategy));

// Mapping
if (persistenceUnitConfig.mapping.timeZoneDefaultStorage.isPresent()) {
desc.getProperties().setProperty(AvailableSettings.TIMEZONE_DEFAULT_STORAGE,
persistenceUnitConfig.mapping.timeZoneDefaultStorage.get().name());
}
desc.getProperties().setProperty(AvailableSettings.PREFERRED_POOLED_OPTIMIZER,
persistenceUnitConfig.mapping.idOptimizerDefault
.orElse(HibernateOrmConfigPersistenceUnit.IdOptimizerType.POOLED_LO).configName);

//charset
desc.getProperties().setProperty(AvailableSettings.HBM2DDL_CHARSET_NAME,
persistenceUnitConfig.database.charset.name());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.quarkus.hibernate.reactive;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

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;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.EntityPersister;

public final class SchemaUtil {

private SchemaUtil() {
}

public static Set<String> getColumnNames(EntityManagerFactory entityManagerFactory, Class<?> entityType) {
Set<String> result = new HashSet<>();
AbstractEntityPersister persister = (AbstractEntityPersister) entityManagerFactory
.unwrap(SessionFactoryImplementor.class)
.getMetamodel().entityPersister(entityType);
if (persister == null) {
return result;
}
for (String propertyName : persister.getPropertyNames()) {
Collections.addAll(result, persister.getPropertyColumnNames(propertyName));
}
return result;
}

public static String getColumnTypeName(EntityManagerFactory entityManagerFactory, Class<?> entityType,
String columnName) {
MappingMetamodel domainModel = entityManagerFactory
.unwrap(SessionFactoryImplementor.class).getRuntimeMetamodels().getMappingMetamodel();
EntityPersister entityDescriptor = domainModel.findEntityDescriptor(entityType);
var columnFinder = new SelectableConsumer() {
private SelectableMapping found;

@Override
public void accept(int selectionIndex, SelectableMapping selectableMapping) {
if (found == null && selectableMapping.getSelectableName().equals(columnName)) {
found = 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.quarkus.hibernate.reactive.config;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;

/**
* This was the only way I found to get our hands on the settings used during metadata building.
* Feel free to use some other solution if you find one.
*/
public class SettingsSpyingIdentifierGenerator implements IdentifierGenerator {
public static final List<Map<String, Object>> collectedSettings = new ArrayList<>();

@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
collectedSettings.add(new HashMap<>(serviceRegistry.getService(ConfigurationService.class).getSettings()));
}

@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
throw new IllegalStateException("This should not be called");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package io.quarkus.hibernate.reactive.config.unsupportedproperties;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;

import java.util.Map;
import java.util.logging.Formatter;
import java.util.logging.Level;

import jakarta.inject.Inject;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

import org.hibernate.SessionFactory;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logmanager.formatters.PatternFormatter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.reactive.config.SettingsSpyingIdentifierGenerator;
import io.quarkus.hibernate.reactive.runtime.FastBootHibernateReactivePersistenceProvider;
import io.quarkus.test.QuarkusUnitTest;

public class UnsupportedPropertiesTest {

private static final Formatter LOG_FORMATTER = new PatternFormatter("%s");

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClass(SpyingIdentifierGeneratorEntity.class)
.addClass(SettingsSpyingIdentifierGenerator.class))
.withConfigurationResource("application.properties")
.overrideConfigKey("quarkus.hibernate-orm.jdbc.statement-batch-size", "10")
// This should be taken into account by Hibernate ORM
.overrideConfigKey("quarkus.hibernate-orm.unsupported-properties.\"" + AvailableSettings.ORDER_INSERTS + "\"",
"true")
// This is just to test a property set at build time
.overrideConfigKey("quarkus.hibernate-orm.unsupported-properties.\"hibernate.some.unknown.key.static-and-runtime\"",
"some-value-1")
// This is just to test a property set at runtime, which would not be available during the build
// (or even during static init with native applications).
.overrideRuntimeConfigKey(
"quarkus.hibernate-orm.unsupported-properties.\"hibernate.some.unknown.key.runtime-only\"",
"some-value-2")
// This should be ignored with a warning
.overrideConfigKey(
"quarkus.hibernate-orm.unsupported-properties.\"" + AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION
+ "\"",
"drop-and-create")
// Expect warnings on startup
.setLogRecordPredicate(
record -> FastBootHibernateReactivePersistenceProvider.class.getName().equals(record.getLoggerName())
&& record.getLevel().intValue() >= Level.WARNING.intValue())
.assertLogRecords(records -> {
var assertion = assertThat(records)
.as("Warnings on startup")
.hasSize(2);
assertion.element(0).satisfies(record -> assertThat(LOG_FORMATTER.formatMessage(record))
.contains("Persistence-unit [default-reactive] sets unsupported properties",
"These properties may not work correctly",
"may change when upgrading to a newer version of Quarkus (even just a micro/patch version)",
"Consider using a supported configuration property",
"make sure to file a feature request so that a supported configuration property can be added to Quarkus")
.contains(AvailableSettings.ORDER_INSERTS, AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION,
"hibernate.some.unknown.key.static-and-runtime", "hibernate.some.unknown.key.runtime-only")
// We should not log property values, that could be a security breach for some properties.
.doesNotContain("some-value"));
assertion.element(1).satisfies(record -> assertThat(LOG_FORMATTER.formatMessage(record))
.contains(
"Persistence-unit [default-reactive] sets property '"
+ AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION
+ "' to a custom value through 'quarkus.hibernate-orm.unsupported-properties.\""
+ AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION + "\"'",
"Quarkus already set that property independently",
"The custom value will be ignored"));
});

@Inject
SessionFactory ormSessionFactory; // This is an ORM SessionFactory, but it's backing Hibernate Reactive.

@Inject
Mutiny.SessionFactory sessionFactory;

@Test
public void testPropertiesPropagatedToStaticInit() {
assertThat(SettingsSpyingIdentifierGenerator.collectedSettings).hasSize(1);
Map<String, Object> settings = SettingsSpyingIdentifierGenerator.collectedSettings.get(0);
assertThat(settings)
.containsEntry("hibernate.some.unknown.key.static-and-runtime", "some-value-1")
.doesNotContainKey("hibernate.some.unknown.key.runtime-only");
}

@Test
public void testPropertiesPropagatedToRuntimeInit() {
assertThat(ormSessionFactory.getProperties())
.contains(entry("hibernate.order_inserts", "true"),
// Also test a property that Quarkus cannot possibly know about
entry("hibernate.some.unknown.key.static-and-runtime", "some-value-1"),
entry("hibernate.some.unknown.key.runtime-only", "some-value-2"));
}

@Entity
public static class SpyingIdentifierGeneratorEntity {
@Id
@GeneratedValue(generator = "spying-generator")
@GenericGenerator(name = "spying-generator", strategy = "io.quarkus.hibernate.reactive.config.SettingsSpyingIdentifierGenerator")
private Long id;

public SpyingIdentifierGeneratorEntity() {
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.quarkus.hibernate.reactive.mapping.id.optimizer.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.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.hibernate.reactive.id.impl.ReactiveGeneratorWrapper;
import org.hibernate.reactive.mutiny.Mutiny;
import org.junit.jupiter.api.Test;

import io.quarkus.hibernate.reactive.SchemaUtil;
import io.quarkus.test.vertx.RunOnVertxContext;
import io.quarkus.test.vertx.UniAsserter;

public abstract class AbstractIdOptimizerDefaultTest {

@Inject
SessionFactory ormSessionFactory; // This is an ORM SessionFactory, but it's backing Hibernate Reactive.

@Inject
Mutiny.SessionFactory sessionFactory;

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
@RunOnVertxContext
public void ids(UniAsserter asserter) {
for (long i = 1; i <= 51; i++) {
long expectedId = i;
// Apparently, we can rely on assertions being executed in order.
asserter.assertThat(() -> sessionFactory.withTransaction(s -> {
var entity = new EntityWithSequenceGenerator();
return s.persist(entity).replaceWith(() -> entity.id);
}),
id -> assertThat(id).isEqualTo(expectedId));
}
}

AbstractObjectAssert<?, Optimizer> assertOptimizer(Class<?> entityType) {
return assertThat(SchemaUtil.getGenerator(ormSessionFactory, entityType))
.as("Reactive ID generator wrapper for entity type " + entityType.getSimpleName())
.asInstanceOf(InstanceOfAssertFactories.type(ReactiveGeneratorWrapper.class))
.extracting("generator") // Needs reflection, unfortunately the blocking generator is not exposed...
.as("Blocking ID generator for entity type " + entityType.getSimpleName())
.asInstanceOf(InstanceOfAssertFactories.type(OptimizableGenerator.class))
.extracting(OptimizableGenerator::getOptimizer)
.as("ID optimizer for entity type " + entityType.getSimpleName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.hibernate.reactive.mapping.id.optimizer.optimizer;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class EntityWithDefaultGenerator {

@Id
@GeneratedValue
Long id;

public EntityWithDefaultGenerator() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.hibernate.reactive.mapping.id.optimizer.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() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.hibernate.reactive.mapping.id.optimizer.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() {
}

}
Loading

0 comments on commit ce446c7

Please sign in to comment.