Skip to content

Commit 7895d01

Browse files
committed
Support DB version checks (enabled by default) with Hibernate Reactive
1 parent 04f7f05 commit 7895d01

File tree

12 files changed

+390
-42
lines changed

12 files changed

+390
-42
lines changed

docs/src/main/asciidoc/hibernate-reactive.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ A `Mutiny.SessionFactory` will be created based on the Quarkus `datasource` conf
122122

123123
The dialect will be selected based on the Reactive SQL client - unless you set one explicitly.
124124

125+
NOTE: For more information on dialect selection and database versions,
126+
see xref:hibernate-orm.adoc#hibernate-dialect[the corresponding section of the Hibernate ORM guide].
127+
125128
You can then happily inject your `Mutiny.SessionFactory`:
126129

127130
[source,java]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.quarkus.hibernate.reactive;
2+
3+
import java.lang.reflect.InvocationTargetException;
4+
5+
import org.hibernate.dialect.Dialect;
6+
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
7+
import org.hibernate.reactive.mutiny.Mutiny;
8+
import org.hibernate.reactive.mutiny.impl.MutinySessionFactoryImpl;
9+
import org.hibernate.service.ServiceRegistry;
10+
11+
import io.quarkus.arc.ClientProxy;
12+
import io.quarkus.hibernate.orm.runtime.config.DialectVersions;
13+
14+
public class DialectUtils {
15+
16+
private DialectUtils() {
17+
}
18+
19+
public static String getDefaultVersion(Class<? extends Dialect> dialectClass) {
20+
try {
21+
return DialectVersions.toString(dialectClass.getConstructor().newInstance().getVersion());
22+
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
23+
throw new RuntimeException(e);
24+
}
25+
}
26+
27+
public static String getConfiguredVersion(Mutiny.SessionFactory sessionFactory) {
28+
ServiceRegistry serviceRegistry = ((MutinySessionFactoryImpl) ClientProxy.unwrap(sessionFactory))
29+
.getServiceRegistry();
30+
return DialectVersions.toString(serviceRegistry.requireService(JdbcEnvironment.class).getDialect().getVersion());
31+
}
32+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.quarkus.hibernate.reactive;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.Id;
7+
8+
@Entity
9+
public class MyEntity {
10+
private long id;
11+
12+
@Column(nullable = false)
13+
private String name;
14+
15+
public MyEntity() {
16+
}
17+
18+
public MyEntity(String name) {
19+
this.name = name;
20+
}
21+
22+
@Id
23+
@GeneratedValue
24+
public long getId() {
25+
return id;
26+
}
27+
28+
public void setId(long id) {
29+
this.id = id;
30+
}
31+
32+
public String getName() {
33+
return name;
34+
}
35+
36+
public void setName(String name) {
37+
this.name = name;
38+
}
39+
40+
@Override
41+
public String toString() {
42+
return "MyEntity:" + name;
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.quarkus.hibernate.reactive;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.function.BiConsumer;
6+
import java.util.function.Function;
7+
8+
import org.hibernate.reactive.mutiny.Mutiny;
9+
10+
import io.quarkus.test.vertx.UniAsserter;
11+
12+
/**
13+
* Very simple reusable tests that simply check that persistence doesn't seem to explode.
14+
* <p>
15+
* Ideally we should rather use an abstract base class and subclass it with an inner class annotated with
16+
* {@link org.junit.jupiter.api.Nested @Nested} in relevant tests.
17+
* But unfortunately {@code @Nested} doesn't work with {@link io.quarkus.test.QuarkusUnitTest} at the moment.
18+
*/
19+
public final class SmokeTestUtils {
20+
21+
private SmokeTestUtils() {
22+
}
23+
24+
public static <T> void testSimplePersistRetrieveUpdateDelete(UniAsserter asserter, Mutiny.SessionFactory sessionFactory,
25+
Class<T> entityType, Function<String, T> constructor, Function<T, Long> idGetter,
26+
BiConsumer<T, String> setter, Function<T, String> getter) {
27+
String initialName = "someName";
28+
T persistedEntity = constructor.apply(initialName);
29+
setter.accept(persistedEntity, initialName);
30+
asserter.assertThat(() -> sessionFactory.withTransaction(session -> session.persist(persistedEntity)
31+
.call(session::flush)
32+
.invoke(session::clear)
33+
.chain(() -> session.find(entityType, idGetter.apply(persistedEntity)))),
34+
retrievedEntity -> assertThat(retrievedEntity)
35+
.extracting(getter)
36+
.isEqualTo(initialName));
37+
38+
// Test updates in order to check dirty properties are correctly detected.
39+
// This is important for XML mapping in particular since Hibernate ORM's bytecode enhancement ignores XML mapping.
40+
String updatedName = "someOtherName";
41+
asserter.assertThat(
42+
() -> sessionFactory.withTransaction(session -> session.find(entityType, idGetter.apply(persistedEntity))
43+
.invoke(entity -> setter.accept(entity, updatedName))
44+
.call(session::flush)
45+
.invoke(session::clear)
46+
.chain(() -> session.find(entityType, idGetter.apply(persistedEntity)))),
47+
retrievedEntity -> assertThat(retrievedEntity)
48+
.extracting(getter)
49+
.isEqualTo(updatedName));
50+
51+
// Test updates in order to check dirty properties are correctly detected.
52+
// This is important for XML mapping in particular since Hibernate ORM's bytecode enhancement ignores XML mapping.
53+
asserter.assertThat(
54+
() -> sessionFactory.withTransaction(session -> session.find(entityType, idGetter.apply(persistedEntity))
55+
.call(session::remove)
56+
.call(session::flush)
57+
.invoke(session::clear)
58+
.chain(() -> session.find(entityType, idGetter.apply(persistedEntity)))),
59+
retrievedEntity -> assertThat(retrievedEntity).isNull());
60+
}
61+
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.quarkus.hibernate.reactive.dialect;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import jakarta.inject.Inject;
6+
7+
import org.hibernate.reactive.mutiny.Mutiny;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import io.quarkus.hibernate.reactive.DialectUtils;
12+
import io.quarkus.hibernate.reactive.MyEntity;
13+
import io.quarkus.hibernate.reactive.SmokeTestUtils;
14+
import io.quarkus.test.QuarkusUnitTest;
15+
import io.quarkus.test.vertx.RunOnVertxContext;
16+
import io.quarkus.test.vertx.UniAsserter;
17+
18+
/**
19+
* Tests that DB version checks can be disabled explicitly.
20+
* <p>
21+
* This was originally introduced to work around problems with version checks,
22+
* such as https://github.com/quarkusio/quarkus/issues/43703 /
23+
* https://github.com/quarkusio/quarkus/issues/42255
24+
*/
25+
public class DbVersionCheckDisabledExplicitlyTest {
26+
27+
// We will set the DB version to something higher than the actual version: this is invalid.
28+
private static final String CONFIGURED_DB_VERSION = "999.999.0";
29+
30+
@RegisterExtension
31+
static QuarkusUnitTest runner = new QuarkusUnitTest()
32+
.withApplicationRoot((jar) -> jar
33+
.addClass(SmokeTestUtils.class)
34+
.addClass(DialectUtils.class)
35+
.addClass(MyEntity.class))
36+
.withConfigurationResource("application.properties")
37+
.overrideConfigKey("quarkus.datasource.db-version", "999.999")
38+
// We disable the version check explicitly, so Quarkus should boot just fine
39+
.overrideConfigKey("quarkus.hibernate-orm.database.version-check.enabled", "false");
40+
41+
@Inject
42+
Mutiny.SessionFactory sessionFactory;
43+
44+
@Test
45+
public void dialectVersion() {
46+
var dialectVersion = DialectUtils.getConfiguredVersion(sessionFactory);
47+
assertThat(dialectVersion).isEqualTo(CONFIGURED_DB_VERSION);
48+
}
49+
50+
@Test
51+
@RunOnVertxContext
52+
public void smokeTest(UniAsserter asserter) {
53+
SmokeTestUtils.testSimplePersistRetrieveUpdateDelete(asserter, sessionFactory,
54+
MyEntity.class, MyEntity::new,
55+
MyEntity::getId,
56+
MyEntity::setName, MyEntity::getName);
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.quarkus.hibernate.reactive.dialect;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import jakarta.inject.Inject;
6+
7+
import org.hibernate.reactive.mutiny.Mutiny;
8+
import org.junit.jupiter.api.Assertions;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.RegisterExtension;
11+
12+
import io.quarkus.hibernate.reactive.DialectUtils;
13+
import io.quarkus.hibernate.reactive.MyEntity;
14+
import io.quarkus.test.QuarkusUnitTest;
15+
16+
public class DbVersionInvalidTest {
17+
18+
// We will set the DB version to something higher than the actual version: this is invalid.
19+
private static final String CONFIGURED_DB_VERSION = "999.999";
20+
private static final String CONFIGURED_DB_VERSION_REPORTED;
21+
static {
22+
// For some reason Hibernate ORM infers a micro version of 0; no big deal.
23+
CONFIGURED_DB_VERSION_REPORTED = CONFIGURED_DB_VERSION + ".0";
24+
}
25+
26+
@RegisterExtension
27+
static QuarkusUnitTest runner = new QuarkusUnitTest()
28+
.withApplicationRoot((jar) -> jar
29+
.addClass(DialectUtils.class)
30+
.addClass(MyEntity.class))
31+
.withConfigurationResource("application.properties")
32+
.overrideConfigKey("quarkus.datasource.db-version", CONFIGURED_DB_VERSION)
33+
.assertException(throwable -> assertThat(throwable)
34+
.rootCause()
35+
.hasMessageContainingAll(
36+
"Persistence unit '<default>' was configured to run with a database version"
37+
+ " of at least '" + CONFIGURED_DB_VERSION_REPORTED + "', but the actual version is '",
38+
"Consider upgrading your database",
39+
"Alternatively, rebuild your application with 'quarkus.datasource.db-version=",
40+
"this may disable some features and/or impact performance negatively",
41+
"disable the check with 'quarkus.hibernate-orm.database.version-check.enabled=false'"));
42+
43+
@Inject
44+
Mutiny.SessionFactory sessionFactory;
45+
46+
@Test
47+
public void test() {
48+
Assertions.fail("Bootstrap should have failed");
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.quarkus.hibernate.reactive.dialect;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import jakarta.inject.Inject;
6+
7+
import org.hibernate.dialect.PostgreSQLDialect;
8+
import org.hibernate.reactive.mutiny.Mutiny;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.RegisterExtension;
11+
12+
import io.quarkus.hibernate.reactive.DialectUtils;
13+
import io.quarkus.hibernate.reactive.MyEntity;
14+
import io.quarkus.hibernate.reactive.SmokeTestUtils;
15+
import io.quarkus.test.QuarkusUnitTest;
16+
import io.quarkus.test.vertx.RunOnVertxContext;
17+
import io.quarkus.test.vertx.UniAsserter;
18+
19+
public class DbVersionValidTest {
20+
21+
// We will set the DB version to something lower than the actual version: this is valid.
22+
private static final String CONFIGURED_DB_VERSION = "16.0";
23+
private static final String CONFIGURED_DB_VERSION_REPORTED;
24+
static {
25+
// For some reason Hibernate ORM infers a micro version of 0; no big deal.
26+
CONFIGURED_DB_VERSION_REPORTED = CONFIGURED_DB_VERSION + ".0";
27+
}
28+
static {
29+
assertThat(DialectUtils.getDefaultVersion(PostgreSQLDialect.class))
30+
.as("Test setup - we need the required version to be different from the default")
31+
.doesNotStartWith(CONFIGURED_DB_VERSION + ".");
32+
}
33+
34+
@RegisterExtension
35+
static QuarkusUnitTest runner = new QuarkusUnitTest()
36+
.withApplicationRoot((jar) -> jar
37+
.addClass(SmokeTestUtils.class)
38+
.addClass(DialectUtils.class)
39+
.addClass(MyEntity.class))
40+
.withConfigurationResource("application.properties")
41+
.overrideConfigKey("quarkus.datasource.db-version", CONFIGURED_DB_VERSION);
42+
43+
@Inject
44+
Mutiny.SessionFactory sessionFactory;
45+
46+
@Test
47+
public void dialectVersion() {
48+
var dialectVersion = DialectUtils.getConfiguredVersion(sessionFactory);
49+
assertThat(dialectVersion).isEqualTo(CONFIGURED_DB_VERSION_REPORTED);
50+
}
51+
52+
@Test
53+
@RunOnVertxContext
54+
public void smokeTest(UniAsserter asserter) {
55+
SmokeTestUtils.testSimplePersistRetrieveUpdateDelete(asserter, sessionFactory,
56+
MyEntity.class, MyEntity::new,
57+
MyEntity::getId,
58+
MyEntity::setName, MyEntity::getName);
59+
}
60+
}

extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String
179179
}
180180
}
181181

182+
// Allow detection of driver/database capabilities on runtime init (was disabled during static init)
183+
runtimeSettingsBuilder.put(AvailableSettings.ALLOW_METADATA_ON_BOOT, "true");
184+
// Remove database version information, if any;
185+
// it was necessary during static init to force creation of a dialect,
186+
// but now the dialect is there, and we'll reuse it.
187+
// Keeping this information would prevent us from getting the actual information from the database on start.
188+
runtimeSettingsBuilder.put(AvailableSettings.JAKARTA_HBM2DDL_DB_VERSION, null);
189+
182190
if (!puConfig.unsupportedProperties().isEmpty()) {
183191
log.warnf("Persistence-unit [%s] sets unsupported properties."
184192
+ " These properties may not work correctly, and even if they do,"
@@ -211,7 +219,8 @@ private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String
211219
RuntimeSettings runtimeSettings = runtimeSettingsBuilder.build();
212220

213221
StandardServiceRegistry standardServiceRegistry = rewireMetadataAndExtractServiceRegistry(
214-
persistenceUnitName, recordedState, runtimeSettings, puConfig);
222+
persistenceUnitName, persistenceUnit.getConfigurationName(),
223+
recordedState, runtimeSettings, puConfig);
215224

216225
final Object cdiBeanManager = Arc.container().beanManager();
217226
final Object validatorFactory = Arc.container().instance("quarkus-hibernate-validator-factory").get();
@@ -230,10 +239,11 @@ private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String
230239
}
231240

232241
private StandardServiceRegistry rewireMetadataAndExtractServiceRegistry(String persistenceUnitName,
242+
String persistenceUnitConfigurationName,
233243
RecordedState recordedState,
234244
RuntimeSettings runtimeSettings, HibernateOrmRuntimeConfigPersistenceUnit puConfig) {
235245
PreconfiguredReactiveServiceRegistryBuilder serviceRegistryBuilder = new PreconfiguredReactiveServiceRegistryBuilder(
236-
persistenceUnitName, recordedState, puConfig);
246+
persistenceUnitConfigurationName, recordedState, puConfig);
237247

238248
Optional<String> dataSourceName = recordedState.getBuildTimeSettings().getSource().getDataSource();
239249
if (dataSourceName.isPresent()) {

0 commit comments

Comments
 (0)