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

[3.15] Backport Hibernate ORM fixes/workarounds around DB version checks to branch 3.15 #44496

Merged
merged 4 commits into from
Nov 14, 2024
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
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/datasource.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ quarkus.datasource.reactive.max-size=20
The following section describes the configuration for single or multiple datasources.
For simplicity, we will reference a single datasource as the default (unnamed) datasource.

[[configure-a-single-datasource]]
=== Configure a single datasource

A datasource can be either a JDBC datasource, reactive, or both.
Expand Down
45 changes: 33 additions & 12 deletions docs/src/main/asciidoc/hibernate-orm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,20 @@ or implicitly set by the Quarkus build process to a minimum supported version of
Quarkus will try to check this preconfigured version against the actual database version on startup,
leading to a startup failure when the actual version is lower.

This is because Hibernate ORM may generate SQL that is invalid
for versions of the database older than what is configured,
which would lead to runtime exceptions.
This is a safeguard: for versions of the database older than what is configured,
Hibernate ORM may generate SQL that is invalid which would lead to runtime exceptions.

// TODO disable the check by default when offline startup is opted in
// See https://github.com/quarkusio/quarkus/issues/13522
If the database cannot be reached, a warning will be logged but startup will proceed.
You can optionally disable the version check if you know the database won't be reachable on startup
using <<quarkus-hibernate-orm_quarkus-hibernate-orm-database-version-check-enabled,`quarkus.hibernate-orm.database.version-check.enabled=false`>>.

// TODO change the default to "always enabled" when we solve version detection problems
// See https://github.com/quarkusio/quarkus/issues/43703
// See https://github.com/quarkusio/quarkus/issues/42255
The version check is disabled by default when a dialect is set explicitly,
as a workaround for https://github.com/quarkusio/quarkus/issues/42255[#42255]/link:https://github.com/quarkusio/quarkus/issues/43703[#43703].
====

[[hibernate-dialect-other-databases]]
Expand Down Expand Up @@ -584,17 +593,29 @@ public class MyProducer {
[[persistence-xml]]
== Setting up and configuring Hibernate ORM with a `persistence.xml`

Alternatively, you can use a `META-INF/persistence.xml` to set up Hibernate ORM.
This is useful for:
To set up and configure Hibernate ORM, <<hibernate-configuration-properties,using `application.properties`>> is recommended,
but you can alternatively use a `META-INF/persistence.xml` file.
This is mainly useful for migrating existing code to Quarkus.

* migrating existing code
* when you have relatively complex settings requiring the full flexibility of the configuration
* or if you like it the good old way

[NOTE]
[WARNING]
====
If you use a `persistence.xml`, then you cannot use the `quarkus.hibernate-orm.*` properties
and only persistence units defined in `persistence.xml` will be taken into account.
Using a `persistence.xml` file implies a few constraints:

* Persistence units defined in `persistence.xml` always use the xref:datasource.adoc#configure-a-single-datasource[default datasource].
* Persistence units defined in `persistence.xml` must be configured explicitly:
Quarkus will keep injection of environment-related configuration to a minimum.
+
In particular, Quarkus will not configure the dialect or database version automatically based on the datasource,
so if the default configuration of Hibernate ORM doesn't suit your needs,
you will need to include in `persistence.xml` configuration such as
link:{hibernate-orm-docs-url}#settings-hibernate.dialect[`hibernate.dialect`]/link:{hibernate-orm-docs-url}#settings-jakarta.persistence.database-product-name[`jakarta.persistence.database-product-name`]
and possibly link:{hibernate-orm-docs-url}#settings-jakarta.persistence.database-product-version[`jakarta.persistence.database-product-version`].
* Using `persistence.xml` is incompatible with using `quarkus.hibernate-orm.*` properties in `{config-file}`:
if you mix them, Quarkus will raise an exception.
* Developer experience may be impacted negatively when using `persistence.xml`
compared to when <<hibernate-configuration-properties,using `application.properties`>>,
due to unavailable features, limited guidance in the Quarkus documentation,
and error messages providing resolution hints that cannot be applied (e.g. using `quarkus.hibernate-orm.*` properties).

If your classpath contains a `persistence.xml` that you want to ignore,
set the following configuration property:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ public void configurationDescriptorBuilding(
Optional.of(DataSourceUtil.DEFAULT_DATASOURCE_NAME),
jdbcDataSource.map(JdbcDataSourceBuildItem::getDbKind),
jdbcDataSource.flatMap(JdbcDataSourceBuildItem::getDbVersion),
Optional.ofNullable(xmlDescriptor.getProperties().getProperty(AvailableSettings.DIALECT)),
getMultiTenancyStrategy(
Optional.ofNullable(persistenceXmlDescriptorBuildItem.getDescriptor()
.getProperties().getProperty("hibernate.multiTenancy"))), //FIXME this property is meaningless in Hibernate ORM 6
Expand Down Expand Up @@ -1103,6 +1104,7 @@ private static void producePersistenceUnitDescriptorFromConfig(
jdbcDataSource.map(JdbcDataSourceBuildItem::getName),
jdbcDataSource.map(JdbcDataSourceBuildItem::getDbKind),
jdbcDataSource.flatMap(JdbcDataSourceBuildItem::getDbVersion),
persistenceUnitConfig.dialect().dialect(),
multiTenancyStrategy,
hibernateOrmConfig.database().ormCompatibilityVersion(),
persistenceUnitConfig.unsupportedProperties()),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.hibernate.orm;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import org.apache.commons.io.IOUtils;

public final class ResourceUtil {
private ResourceUtil() {
}

public static String loadResourceAndReplacePlaceholders(String resourceName, Map<String, String> placeholders) {
String content = null;
try (var stream = ResourceUtil.class.getClassLoader().getResourceAsStream(resourceName)) {
if (stream == null) {
throw new RuntimeException("Could not load '" + resourceName + "' from classpath");
}
content = IOUtils.toString(stream, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
for (var entry : placeholders.entrySet()) {
content = content.replace("${" + entry.getKey() + "}", entry.getValue());
}
return content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.quarkus.hibernate.orm.config.dialect;

import static io.quarkus.hibernate.orm.ResourceUtil.loadResourceAndReplacePlaceholders;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Map;

import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.MyEntity;
import io.quarkus.hibernate.orm.SmokeTestUtils;
import io.quarkus.hibernate.orm.runtime.config.DialectVersions;
import io.quarkus.test.QuarkusUnitTest;

/**
* Tests that the workaround for https://github.com/quarkusio/quarkus/issues/43703 /
* https://github.com/quarkusio/quarkus/issues/42255
* is effective.
*/
// TODO remove this test when change the default to "always enabled" when we solve version detection problems
// See https://github.com/quarkusio/quarkus/issues/43703
// See https://github.com/quarkusio/quarkus/issues/42255
public class DbVersionCheckDisabledAutomaticallyPersistenceXmlTest {

private static final String ACTUAL_H2_VERSION = DialectVersions.Defaults.H2;
// We will set the DB version to something higher than the actual version: this is invalid.
private static final String CONFIGURED_DB_VERSION = "999.999.0";
static {
assertThat(ACTUAL_H2_VERSION)
.as("Test setup - we need the required version to be different from the actual one")
.doesNotStartWith(CONFIGURED_DB_VERSION);
}

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClass(SmokeTestUtils.class)
.addClass(MyEntity.class)
.addAsManifestResource(new StringAsset(loadResourceAndReplacePlaceholders(
"META-INF/some-persistence-with-h2-version-placeholder-and-explicit-dialect.xml",
Map.of("H2_VERSION", "999.999"))),
"persistence.xml"))
.withConfigurationResource("application-datasource-only.properties");

@Inject
SessionFactory sessionFactory;

@Inject
Session session;

@Test
public void dialectVersion() {
var dialectVersion = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion();
assertThat(DialectVersions.toString(dialectVersion)).isEqualTo(CONFIGURED_DB_VERSION);
}

@Test
@Transactional
public void smokeTest() {
SmokeTestUtils.testSimplePersistRetrieveUpdateDelete(session,
MyEntity.class, MyEntity::new,
MyEntity::getId,
MyEntity::setName, MyEntity::getName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.quarkus.hibernate.orm.config.dialect;

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

import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.MyEntity;
import io.quarkus.hibernate.orm.SmokeTestUtils;
import io.quarkus.hibernate.orm.runtime.config.DialectVersions;
import io.quarkus.test.QuarkusUnitTest;

/**
* Tests that the workaround for https://github.com/quarkusio/quarkus/issues/43703 /
* https://github.com/quarkusio/quarkus/issues/42255
* is effective.
*/
// TODO remove this test when change the default to "always enabled" when we solve version detection problems
// See https://github.com/quarkusio/quarkus/issues/43703
// See https://github.com/quarkusio/quarkus/issues/42255
public class DbVersionCheckDisabledAutomaticallyTest {

private static final String ACTUAL_H2_VERSION = DialectVersions.Defaults.H2;
// We will set the DB version to something higher than the actual version: this is invalid.
private static final String CONFIGURED_DB_VERSION = "999.999.0";
static {
assertThat(ACTUAL_H2_VERSION)
.as("Test setup - we need the required version to be different from the actual one")
.doesNotStartWith(CONFIGURED_DB_VERSION);
}

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClass(SmokeTestUtils.class)
.addClass(MyEntity.class))
.withConfigurationResource("application.properties")
.overrideConfigKey("quarkus.datasource.db-version", "999.999")
// Setting a dialect should disable the version check, so Quarkus should boot just fine
.overrideConfigKey("quarkus.hibernate-orm.dialect", H2Dialect.class.getName());

@Inject
SessionFactory sessionFactory;

@Inject
Session session;

@Test
public void dialectVersion() {
var dialectVersion = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion();
assertThat(DialectVersions.toString(dialectVersion)).isEqualTo(CONFIGURED_DB_VERSION);
}

@Test
@Transactional
public void smokeTest() {
SmokeTestUtils.testSimplePersistRetrieveUpdateDelete(session,
MyEntity.class, MyEntity::new,
MyEntity::getId,
MyEntity::setName, MyEntity::getName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.quarkus.hibernate.orm.config.dialect;

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

import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.MyEntity;
import io.quarkus.hibernate.orm.SmokeTestUtils;
import io.quarkus.hibernate.orm.runtime.config.DialectVersions;
import io.quarkus.test.QuarkusUnitTest;

/**
* Tests that the workaround for https://github.com/quarkusio/quarkus/issues/43703 /
* https://github.com/quarkusio/quarkus/issues/42255
* is effective.
*/
// TODO remove this test when change the default to "always enabled" when we solve version detection problems
// See https://github.com/quarkusio/quarkus/issues/43703
// See https://github.com/quarkusio/quarkus/issues/42255
public class DbVersionCheckDisabledExplicitlyTest {

private static final String ACTUAL_H2_VERSION = DialectVersions.Defaults.H2;
// We will set the DB version to something higher than the actual version: this is invalid.
private static final String CONFIGURED_DB_VERSION = "999.999.0";
static {
assertThat(ACTUAL_H2_VERSION)
.as("Test setup - we need the required version to be different from the actual one")
.doesNotStartWith(CONFIGURED_DB_VERSION);
}

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClass(SmokeTestUtils.class)
.addClass(MyEntity.class))
.withConfigurationResource("application.properties")
.overrideConfigKey("quarkus.datasource.db-version", "999.999")
// We disable the version check explicitly, so Quarkus should boot just fine
.overrideConfigKey("quarkus.hibernate-orm.database.version-check.enabled", "false");

@Inject
SessionFactory sessionFactory;

@Inject
Session session;

@Test
public void dialectVersion() {
var dialectVersion = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion();
assertThat(DialectVersions.toString(dialectVersion)).isEqualTo(CONFIGURED_DB_VERSION);
}

@Test
@Transactional
public void smokeTest() {
SmokeTestUtils.testSimplePersistRetrieveUpdateDelete(session,
MyEntity.class, MyEntity::new,
MyEntity::getId,
MyEntity::setName, MyEntity::getName);
}
}
Loading
Loading