From 5e28128c5cefff4f5bdf0374f8ece6c5504312a3 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Tue, 22 Jul 2025 15:47:21 +0200 Subject: [PATCH 1/2] [#1906] Support @IdGeneratorType --- .../id/ReactiveIdentifierGenerator.java | 21 ++++++++++++++-- .../id/ReactiveOnExecutionGenerator.java | 24 +++++++++++++++++++ .../ReactiveAbstractReturningDelegate.java | 7 ++++++ .../impl/ReactiveStatelessSessionImpl.java | 3 ++- 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveOnExecutionGenerator.java diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java index 83ae3290a..ffb0867e3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java @@ -6,11 +6,14 @@ package org.hibernate.reactive.id; import org.hibernate.Incubating; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; -import org.hibernate.generator.Generator; import org.hibernate.id.IdentifierGenerator; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import java.lang.invoke.MethodHandles; import java.util.concurrent.CompletionStage; /** @@ -26,7 +29,7 @@ * @see IdentifierGenerator */ @Incubating -public interface ReactiveIdentifierGenerator extends Generator { +public interface ReactiveIdentifierGenerator extends IdentifierGenerator { /** * Returns a generated identifier, via a {@link CompletionStage}. @@ -38,4 +41,18 @@ public interface ReactiveIdentifierGenerator extends Generator { default CompletionStage generate(ReactiveConnectionSupplier session, Object owner, Object currentValue, EventType eventType) { return generate( session, owner ); } + + @Override + default Id generate( + SharedSessionContractImplementor session, + Object owner, + Object currentValue, + EventType eventType){ + throw LoggerFactory.make( Log.class, MethodHandles.lookup() ).nonReactiveMethodCall( "generate(ReactiveConnectionSupplier, Object, Object, EventType)" ); + } + + @Override + default Object generate(SharedSessionContractImplementor session, Object object){ + throw LoggerFactory.make( Log.class, MethodHandles.lookup() ).nonReactiveMethodCall( "generate(ReactiveConnectionSupplier, Object)" ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveOnExecutionGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveOnExecutionGenerator.java new file mode 100644 index 000000000..2fea352b1 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveOnExecutionGenerator.java @@ -0,0 +1,24 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.id; + +import org.hibernate.dialect.Dialect; +import org.hibernate.generator.OnExecutionGenerator; +import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.id.insert.ReactiveInsertReturningDelegate; + +public interface ReactiveOnExecutionGenerator extends OnExecutionGenerator { + + @Override + default InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(EntityPersister persister) { + Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); + // Hibernate ORM allows the selection of different strategies based on the property `hibernate.jdbc.use_get_generated_keys`. + // But that's a specific JDBC property and with Vert.x we only have one viable option for each supported database. + return new ReactiveInsertReturningDelegate( persister, dialect ); + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java index aed2555fe..5180dc557 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java @@ -12,6 +12,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.SQLServerDialect; @@ -86,6 +87,12 @@ && getPersister().getFactory().getJdbcServices().getDialect() instanceof Cockroa } private static String createInsert(String insertSql, String identifierColumnName, Dialect dialect) { + if ( dialect instanceof MariaDBDialect ) { + // The queries for MariDB seem to work fine, we don't have to change them. + // In particular, removing the " returning id" at the end will cause a failure when a column value + // is generated by the database + return insertSql; + } String sql = insertSql; final String sqlEnd = " returning " + identifierColumnName; if ( dialect instanceof MySQLDialect ) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java index d67cc2018..f49cf2110 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java @@ -70,6 +70,7 @@ import org.hibernate.reactive.query.sql.spi.ReactiveNativeQueryImplementor; import org.hibernate.reactive.query.sqm.internal.ReactiveQuerySqmImpl; import org.hibernate.reactive.query.sqm.internal.ReactiveSqmSelectionQueryImpl; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; import org.hibernate.reactive.session.ReactiveSqmQueryImplementor; import org.hibernate.reactive.session.ReactiveStatelessSession; import org.hibernate.reactive.util.impl.CompletionStages.Completable; @@ -438,7 +439,7 @@ private CompletionStage generatedIdBeforeInsert( private CompletionStage generateIdForInsert(Object entity, Generator generator, ReactiveEntityPersister persister) { if ( generator instanceof ReactiveIdentifierGenerator reactiveGenerator ) { - return reactiveGenerator.generate( this, this ) + return reactiveGenerator.generate( (ReactiveConnectionSupplier) this, this ) .thenApply( id -> castToIdentifierType( id, persister ) ); } From 4c74dc1878040b97639eddde1326db65cdc0f79c Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 17 Jul 2025 11:59:06 +0200 Subject: [PATCH 2/2] [#1906] Test @IdGeneratorType support --- .../BeforeExecutionIdGeneratorTypeTest.java | 156 ++++++++++++++++++ .../OnExecutionGeneratorTypeTest.java | 122 ++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/BeforeExecutionIdGeneratorTypeTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/OnExecutionGeneratorTypeTest.java diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BeforeExecutionIdGeneratorTypeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BeforeExecutionIdGeneratorTypeTest.java new file mode 100644 index 000000000..e71362b45 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BeforeExecutionIdGeneratorTypeTest.java @@ -0,0 +1,156 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicLong; + +import org.hibernate.annotations.IdGeneratorType; +import org.hibernate.generator.EventType; +import org.hibernate.reactive.id.ReactiveIdentifierGenerator; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.reactive.util.impl.CompletionStages; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Timeout(value = 10, timeUnit = MINUTES) +public class BeforeExecutionIdGeneratorTypeTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( Person.class ); + } + + @Test + public void testPersistWithoutTransaction(VertxTestContext context) { + final Person person = new Person( "Janet" ); + // The id should be set by the persist + assertThat( person.getId() ).isNull(); + test( context, getMutinySessionFactory() + // The value won't be persisted on the database, but the id should have been assigned anyway + .withSession( session -> session.persist( person ) ) + .invoke( () -> assertThat( person.getId() ).isGreaterThan( 0 ) ) + // Check that the value has not been saved + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .createNativeQuery( "select * from Person", Tuple.class ).getSingleResultOrNull() ) + ) + .invoke( result -> assertThat( result ).isNull() ) + ); + } + + @Test + public void testPersistWithTransaction(VertxTestContext context) { + final Person person = new Person( "Baldrick" ); + // The id should be set by the persist + assertThat( person.getId() ).isNull(); + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persist( person ) ) + .invoke( () -> assertThat( person.getId() ).isGreaterThan( 0 ) ) + // Check that the value has been saved + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .createNativeQuery( "select id,name from Person", Object[].class ).getSingleResult() ) + ) + .invoke( row -> { + // The raw type might not be a Long, so we have to cast it + assertThat( (Long) row[0] ).isEqualTo( person.id ); + assertThat( row[1] ).isEqualTo( person.name ); + } ) + + ); + } + + @Entity(name = "Person") + @Table(name = "Person") + public static class Person { + @Id + @SimpleId + Long id; + + String name; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Person person = (Person) o; + return Objects.equals( name, person.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return id + ":" + name; + } + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @IdGeneratorType(SimpleGenerator.class) + public @interface SimpleId { + } + + public static class SimpleGenerator implements ReactiveIdentifierGenerator { + + private AtomicLong sequence = new AtomicLong( 1 ); + + public SimpleGenerator() { + } + + @Override + public boolean generatedOnExecution() { + return false; + } + + @Override + public EnumSet getEventTypes() { + return EnumSet.of( EventType.INSERT ); + } + + + @Override + public CompletionStage generate(ReactiveConnectionSupplier session, Object entity) { + return CompletionStages.completedFuture( sequence.getAndIncrement() ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OnExecutionGeneratorTypeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OnExecutionGeneratorTypeTest.java new file mode 100644 index 000000000..bd2adaa28 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OnExecutionGeneratorTypeTest.java @@ -0,0 +1,122 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collection; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; + +import org.hibernate.annotations.IdGeneratorType; +import org.hibernate.annotations.ValueGenerationType; +import org.hibernate.dialect.Dialect; +import org.hibernate.generator.EventType; +import org.hibernate.generator.EventTypeSets; +import org.hibernate.reactive.id.ReactiveOnExecutionGenerator; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Timeout(value = 10, timeUnit = MINUTES) +public class OnExecutionGeneratorTypeTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( Tournament.class ); + } + + @Test + public void testPersist(VertxTestContext context) { + Tournament tournament = new Tournament( "Tekken World Tour" ); + test( + context, getSessionFactory() + .withTransaction( session -> session.persist( tournament ) ) + .thenAccept( v -> { + assertThat( tournament.getId() ).isNotNull(); + assertThat( tournament.getCreated() ).isNotNull(); + } ) + ); + } + + @Entity(name = "Tournament") + public static class Tournament { + @Id + @FunctionCreatedValueId + Date id; + + String name; + + @FunctionCreatedValue + Date created; + + public Tournament() { + } + + public Tournament(String name) { + this.name = name; + } + + public Date getId() { + return id; + } + + public String getName() { + return name; + } + + public Date getCreated() { + return created; + } + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @IdGeneratorType(FunctionCreationValueGeneration.class) + public @interface FunctionCreatedValueId { + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @ValueGenerationType(generatedBy = FunctionCreationValueGeneration.class) + public @interface FunctionCreatedValue { + } + + public static class FunctionCreationValueGeneration + implements ReactiveOnExecutionGenerator { + + @Override + public boolean referenceColumnsInSql(Dialect dialect) { + return true; + } + + @Override + public boolean writePropertyValue() { + return false; + } + + @Override + public String[] getReferencedColumnValues(Dialect dialect) { + return new String[] { dialect.currentTimestamp() }; + } + + @Override + public EnumSet getEventTypes() { + return EventTypeSets.INSERT_ONLY; + } + } + +}