Skip to content

Support @IdGeneratorType #2407

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -26,7 +29,7 @@
* @see IdentifierGenerator
*/
@Incubating
public interface ReactiveIdentifierGenerator<Id> extends Generator {
public interface ReactiveIdentifierGenerator<Id> extends IdentifierGenerator {

/**
* Returns a generated identifier, via a {@link CompletionStage}.
Expand All @@ -38,4 +41,18 @@
default CompletionStage<Id> generate(ReactiveConnectionSupplier session, Object owner, Object currentValue, EventType eventType) {
return generate( session, owner );
}

@Override
default Id generate(

Check notice

Code scanning / CodeQL

Confusing overloading of methods Note

Method ReactiveIdentifierGenerator.generate(..) could be confused with overloaded method
generate
, since dispatch depends on static types.
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){

Check notice

Code scanning / CodeQL

Confusing overloading of methods Note

Method ReactiveIdentifierGenerator.generate(..) could be confused with overloaded method
generate
, since dispatch depends on static types.
throw LoggerFactory.make( Log.class, MethodHandles.lookup() ).nonReactiveMethodCall( "generate(ReactiveConnectionSupplier, Object)" );
}
}
Original file line number Diff line number Diff line change
@@ -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 );
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ) );
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Class<?>> 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<Long> {

private AtomicLong sequence = new AtomicLong( 1 );

public SimpleGenerator() {
}

@Override
public boolean generatedOnExecution() {
return false;
}

@Override
public EnumSet<EventType> getEventTypes() {
return EnumSet.of( EventType.INSERT );
}


@Override
public CompletionStage<Long> generate(ReactiveConnectionSupplier session, Object entity) {
return CompletionStages.completedFuture( sequence.getAndIncrement() );
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Class<?>> 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<EventType> getEventTypes() {
return EventTypeSets.INSERT_ONLY;
}
}

}
Loading