Skip to content
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

Configuration property to work with database schemas generated for Hibernate ORM 5.6 #31540

Merged
merged 5 commits into from
Mar 6, 2023
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
3 changes: 2 additions & 1 deletion build-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
<enforce-test-deps-scope.skip>${enforcer.skip}</enforce-test-deps-scope.skip>

<surefire.argLine.additional></surefire.argLine.additional>
<failsafe.argLine.additional>${surefire.argLine.additional}</failsafe.argLine.additional>
<os-maven-plugin.version>1.7.0</os-maven-plugin.version>

<!-- google cloud functions invoker-->
Expand Down Expand Up @@ -460,7 +461,7 @@
<project.groupId>${project.groupId}</project.groupId>
</systemPropertyVariables>
<!-- set tmpdir as early as possible because failsafe sets it too late for JDK16 -->
<argLine>-Djava.io.tmpdir="${project.build.directory}"</argLine>
<argLine>-Djava.io.tmpdir="${project.build.directory}" ${failsafe.argLine.additional}</argLine>
<excludedEnvironmentVariables>MAVEN_OPTS</excludedEnvironmentVariables>
</configuration>
</plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import java.util.TreeMap;

import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil;
import io.quarkus.hibernate.orm.runtime.config.DatabaseOrmCompatibilityVersion;
import io.quarkus.runtime.annotations.ConfigDocMapKey;
import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.quarkus.runtime.annotations.ConvertWith;

@ConfigRoot
public class HibernateOrmConfig {
Expand All @@ -26,6 +28,33 @@ public class HibernateOrmConfig {
@ConfigItem(defaultValue = "true")
public boolean enabled;

/**
* When set, attempts to exchange data with the database
* as the given version of Hibernate ORM would have,
* *on a best-effort basis*.
*
* Please note:
*
* * schema validation may still fail in some cases:
* this attempts to make Hibernate ORM 6+ behave correctly at runtime,
* but it may still expect a different (but runtime-compatible) schema.
* * robust test suites are still useful and recommended:
* you should still check that your application behaves as intended with your legacy schema.
* * this feature is inherently unstable:
* some aspects of it may stop working in future versions of Quarkus,
* and older versions will be dropped as Hibernate ORM changes pile up
* and support for those older versions becomes too unreliable.
* * you should still plan a migration of your schema to a newer version of Hibernate ORM.
* For help with migration, refer to
* link:https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.0:-Hibernate-ORM-5-to-6-migration[the Quarkus 3
* migration guide from Hibernate ORM 5 to 6].
*
* @asciidoclet
*/
@ConfigItem(name = "database.orm-compatibility.version", defaultValue = "LATEST")
@ConvertWith(DatabaseOrmCompatibilityVersion.Converter.class)
public DatabaseOrmCompatibilityVersion databaseOrmCompatibilityVersion;

/**
* Configuration for the default persistence unit.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,17 +382,21 @@ public void configurationDescriptorBuilding(
// First produce the PUs having a persistence.xml: these are not reactive, as we don't allow using a persistence.xml for them.
for (PersistenceXmlDescriptorBuildItem persistenceXmlDescriptorBuildItem : persistenceXmlDescriptors) {
ParsedPersistenceXmlDescriptor xmlDescriptor = persistenceXmlDescriptorBuildItem.getDescriptor();
Optional<JdbcDataSourceBuildItem> jdbcDataSource = jdbcDataSources.stream()
.filter(i -> i.isDefault())
.findFirst();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me a bit - why the first one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No particular reason, findAny (if that exists) would work. I copy pasted that code from the method that handles non-persistence.xml config.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But don't we attempt to match the right datasource configuration for this PU ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for persistence.xml handling has always assumed we were using the default datasource for these PUs:

https://github.com/yrodiere/quarkus/blob/8f171cdbcfa4c810f5ec10377240a5f3278607c0/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java#L396-L399

This new code is in line with that assumption.

If you think it's wrong and we should extract the datasource name from the persistence.xml (I guess it can be configured there?), I can open another ticket?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, existing code - no rush then. But yes it surprised me a bit - I guess I forgot we ignore the datasource section from a persistence.xml.
I guess ignoring it is fine, but to make things better we could try to spot if the user defined a datasource section and warn about it being ignored / or even fail.

persistenceUnitDescriptors
.produce(new PersistenceUnitDescriptorBuildItem(xmlDescriptor,
xmlDescriptor.getName(),
Optional.of(DataSourceUtil.DEFAULT_DATASOURCE_NAME),
jdbcDataSource.map(JdbcDataSourceBuildItem::getDbKind),
getMultiTenancyStrategy(Optional.ofNullable(persistenceXmlDescriptorBuildItem.getDescriptor()
.getProperties().getProperty("hibernate.multiTenancy"))), //FIXME this property is meaningless in Hibernate ORM 6
null,
jpaModel.getXmlMappings(persistenceXmlDescriptorBuildItem.getDescriptor().getName()),
Collections.emptyMap(),
false,
true));
hibernateOrmConfig.databaseOrmCompatibilityVersion,
false, true));
}

if (impliedPU.shouldGenerateImpliedBlockingPersistenceUnit()) {
Expand Down Expand Up @@ -1196,10 +1200,12 @@ private static void producePersistenceUnitDescriptorFromConfig(
persistenceUnitDescriptors.produce(
new PersistenceUnitDescriptorBuildItem(descriptor, descriptor.getName(),
jdbcDataSource.map(JdbcDataSourceBuildItem::getName),
jdbcDataSource.map(JdbcDataSourceBuildItem::getDbKind),
multiTenancyStrategy,
persistenceUnitConfig.multitenantSchemaDatasource.orElse(null),
xmlMappings,
persistenceUnitConfig.unsupportedProperties,
hibernateOrmConfig.databaseOrmCompatibilityVersion,
false, false));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.quarkus.datasource.common.runtime.DataSourceUtil;
import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDefinition;
import io.quarkus.hibernate.orm.runtime.boot.xml.RecordableXmlMapping;
import io.quarkus.hibernate.orm.runtime.config.DatabaseOrmCompatibilityVersion;
import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticDescriptor;
import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy;

Expand All @@ -29,35 +30,43 @@ public final class PersistenceUnitDescriptorBuildItem extends MultiBuildItem {
// use the name "<default>", so we need to convert between those.
private final String configurationName;
private final Optional<String> dataSource;
private final Optional<String> dbKind;
private final MultiTenancyStrategy multiTenancyStrategy;
private final String multiTenancySchemaDataSource;
private final List<RecordableXmlMapping> xmlMappings;
private final Map<String, String> quarkusConfigUnsupportedProperties;
private final DatabaseOrmCompatibilityVersion databaseOrmCompatibilityVersion;
private final boolean isReactive;
private final boolean fromPersistenceXml;

public PersistenceUnitDescriptorBuildItem(ParsedPersistenceXmlDescriptor descriptor, String configurationName,
Optional<String> dbKind,
List<RecordableXmlMapping> xmlMappings,
Map<String, String> quarkusConfigUnsupportedProperties,
DatabaseOrmCompatibilityVersion databaseOrmCompatibilityVersion,
boolean isReactive, boolean fromPersistenceXml) {
this(descriptor, configurationName,
Optional.of(DataSourceUtil.DEFAULT_DATASOURCE_NAME), MultiTenancyStrategy.NONE, null,
xmlMappings, quarkusConfigUnsupportedProperties, isReactive, fromPersistenceXml);
Optional.of(DataSourceUtil.DEFAULT_DATASOURCE_NAME), dbKind, MultiTenancyStrategy.NONE, null,
xmlMappings, quarkusConfigUnsupportedProperties, databaseOrmCompatibilityVersion,
isReactive, fromPersistenceXml);
}

public PersistenceUnitDescriptorBuildItem(ParsedPersistenceXmlDescriptor descriptor, String configurationName,
Optional<String> dataSource,
Optional<String> dataSource, Optional<String> dbKind,
MultiTenancyStrategy multiTenancyStrategy, String multiTenancySchemaDataSource,
List<RecordableXmlMapping> xmlMappings,
Map<String, String> quarkusConfigUnsupportedProperties,
DatabaseOrmCompatibilityVersion databaseOrmCompatibilityVersion,
boolean isReactive, boolean fromPersistenceXml) {
this.descriptor = descriptor;
this.configurationName = configurationName;
this.dataSource = dataSource;
this.dbKind = dbKind;
this.multiTenancyStrategy = multiTenancyStrategy;
this.multiTenancySchemaDataSource = multiTenancySchemaDataSource;
this.xmlMappings = xmlMappings;
this.quarkusConfigUnsupportedProperties = quarkusConfigUnsupportedProperties;
this.databaseOrmCompatibilityVersion = databaseOrmCompatibilityVersion;
this.isReactive = isReactive;
this.fromPersistenceXml = fromPersistenceXml;
}
Expand Down Expand Up @@ -100,8 +109,9 @@ public boolean isFromPersistenceXml() {

public QuarkusPersistenceUnitDefinition asOutputPersistenceUnitDefinition(
List<HibernateOrmIntegrationStaticDescriptor> integrationStaticDescriptors) {
return new QuarkusPersistenceUnitDefinition(descriptor, configurationName, dataSource, multiTenancyStrategy,
xmlMappings,
quarkusConfigUnsupportedProperties, isReactive, fromPersistenceXml, integrationStaticDescriptors);
return new QuarkusPersistenceUnitDefinition(descriptor, configurationName, dataSource, dbKind,
multiTenancyStrategy, xmlMappings,
quarkusConfigUnsupportedProperties, databaseOrmCompatibilityVersion,
isReactive, fromPersistenceXml, integrationStaticDescriptors);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.quarkus.hibernate.orm.config.unsupportedproperties;
package io.quarkus.hibernate.orm.config;

import java.io.Serializable;
import java.util.ArrayList;
Expand All @@ -20,7 +20,7 @@
* Feel free to use some other solution if you find one.
*/
public class SettingsSpyingIdentifierGenerator implements IdentifierGenerator {
static final List<Map<String, Object>> collectedSettings = new ArrayList<>();
public static final List<Map<String, Object>> collectedSettings = new ArrayList<>();

@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package io.quarkus.hibernate.orm.config.databaseormcompatibility;

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

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.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

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

import io.quarkus.hibernate.orm.config.SettingsSpyingIdentifierGenerator;
import io.quarkus.hibernate.orm.runtime.FastBootHibernatePersistenceProvider;
import io.quarkus.test.QuarkusUnitTest;

public class DatabaseOrmCompatibilityVersionTest {

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.database.orm-compatibility.version", "5.6")
// We allow overriding database/orm compatibility settings with .unsupported-properties,
// to enable step-by-step migration
.overrideConfigKey(
"quarkus.hibernate-orm.unsupported-properties.\"" + AvailableSettings.PREFERRED_INSTANT_JDBC_TYPE + "\"",
"TIMESTAMP_UTC")
// Expect warnings on startup
.setLogRecordPredicate(record -> FastBootHibernatePersistenceProvider.class.getName().equals(record.getLoggerName())
&& record.getLevel().intValue() >= Level.WARNING.intValue())
.assertLogRecords(records -> {
var assertion = assertThat(records)
.as("Warnings on startup")
.hasSizeGreaterThanOrEqualTo(3);
assertion.element(0).satisfies(record -> assertThat(LOG_FORMATTER.formatMessage(record))
.contains("Persistence-unit [<default>] sets unsupported properties")
// 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>]:"
+ " enabling best-effort backwards compatibility with 'quarkus.hibernate-orm.database.orm-compatibility.version=5.6'.",
"Quarkus will attempt to change the behavior and expected schema of Hibernate ORM"
+ " to match those of Hibernate ORM 5.6.",
"This is an inherently best-effort feature",
"may stop working in future versions of Quarkus",
"Consider migrating your application",
"https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.0:-Hibernate-ORM-5-to-6-migration"));
assertion.anySatisfy(record -> assertThat(LOG_FORMATTER.formatMessage(record))
.contains(
"Persistence-unit [<default>] - 5.6 compatibility: setting 'hibernate.timezone.default_storage=NORMALIZE'.",
"affects Hibernate ORM's behavior and schema compatibility",
"may stop working in future versions of Quarkus"));
});

@Inject
EntityManagerFactory emf;

@Inject
EntityManager em;

@Test
public void testPropertiesPropagatedToStaticInit() {
assertThat(SettingsSpyingIdentifierGenerator.collectedSettings).hasSize(1);
Map<String, Object> settings = SettingsSpyingIdentifierGenerator.collectedSettings.get(0);
assertThat(settings).containsAllEntriesOf(Map.of(
AvailableSettings.TIMEZONE_DEFAULT_STORAGE, "NORMALIZE",
// We allow overriding database/orm compatibility settings with .unsupported-properties,
// to enable step-by-step migration
AvailableSettings.PREFERRED_INSTANT_JDBC_TYPE, "TIMESTAMP_UTC"));
}

@Test
public void testPropertiesPropagatedToRuntimeInit() {
assertThat(emf.getProperties()).containsAllEntriesOf(Map.of(
AvailableSettings.TIMEZONE_DEFAULT_STORAGE, "NORMALIZE",
// We allow overriding database/orm compatibility settings with .unsupported-properties,
// to enable step-by-step migration
AvailableSettings.PREFERRED_INSTANT_JDBC_TYPE, "TIMESTAMP_UTC"));
}

@Entity
public static class SpyingIdentifierGeneratorEntity {
@Id
@GeneratedValue(generator = "spying-generator")
@GenericGenerator(name = "spying-generator", strategy = "io.quarkus.hibernate.orm.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
Expand Up @@ -27,6 +27,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.config.SettingsSpyingIdentifierGenerator;
import io.quarkus.hibernate.orm.runtime.FastBootHibernatePersistenceProvider;
import io.quarkus.narayana.jta.QuarkusTransaction;
import io.quarkus.test.QuarkusUnitTest;
Expand Down Expand Up @@ -211,7 +212,7 @@ public void setParent(ParentEntity parent) {
public static class SpyingIdentifierGeneratorEntity {
@Id
@GeneratedValue(generator = "spying-generator")
@GenericGenerator(name = "spying-generator", strategy = "io.quarkus.hibernate.orm.config.unsupportedproperties.SettingsSpyingIdentifierGenerator")
@GenericGenerator(name = "spying-generator", strategy = "io.quarkus.hibernate.orm.config.SettingsSpyingIdentifierGenerator")
private Long id;

public SpyingIdentifierGeneratorEntity() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,57 @@
package io.quarkus.hibernate.orm.runtime;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class BuildTimeSettings {
import io.quarkus.hibernate.orm.runtime.config.DatabaseOrmCompatibilityVersion;

private Map<String, Object> settings;
public class BuildTimeSettings {

public BuildTimeSettings(Map<String, Object> settings) {
this.settings = Collections.unmodifiableMap(new HashMap<>(settings));
private Map<String, Object> quarkusConfigSettings;
private DatabaseOrmCompatibilityVersion databaseOrmCompatibilityVersion;
private Map<String, String> databaseOrmCompatibilitySettings;
private Map<String, Object> allSettings;

public BuildTimeSettings(Map<String, Object> quarkusConfigSettings,
DatabaseOrmCompatibilityVersion databaseOrmCompatibilityVersion,
Map<String, String> databaseOrmCompatibilitySettings,
Map<String, Object> allSettings) {
this.quarkusConfigSettings = Map.copyOf(quarkusConfigSettings);
this.databaseOrmCompatibilityVersion = databaseOrmCompatibilityVersion;
this.databaseOrmCompatibilitySettings = Map.copyOf(databaseOrmCompatibilitySettings);
this.allSettings = Map.copyOf(allSettings);
}

public Object get(String key) {
return settings.get(key);
return allSettings.get(key);
}

public boolean getBoolean(String key) {
Object propertyValue = settings.get(key);
Object propertyValue = allSettings.get(key);
return propertyValue != null && Boolean.parseBoolean(propertyValue.toString());
}

public boolean isConfigured(String key) {
return settings.containsKey(key);
return allSettings.containsKey(key);
}

public Map<String, Object> getQuarkusConfigSettings() {
return quarkusConfigSettings;
}

public DatabaseOrmCompatibilityVersion getDatabaseOrmCompatibilityVersion() {
return databaseOrmCompatibilityVersion;
}

public Map<String, String> getDatabaseOrmCompatibilitySettings() {
return databaseOrmCompatibilitySettings;
}

public Map<String, Object> getSettings() {
return settings;
public Map<String, Object> getAllSettings() {
return allSettings;
}

@Override
public String toString() {
return this.getClass().getSimpleName() + " {" + settings.toString() + "}";
return this.getClass().getSimpleName() + " {" + allSettings.toString() + "}";
}
}
Loading