HHH-18411 - Add ability to specify a custom UuidGenerator.ValueGenerator

This commit is contained in:
Steve Ebersole 2024-07-24 11:06:58 -05:00
parent 55108d0740
commit fdef3b52eb
9 changed files with 295 additions and 16 deletions

View File

@ -11,6 +11,9 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.UUID;
import org.hibernate.Incubating;
import org.hibernate.id.uuid.UuidValueGenerator;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@ -21,9 +24,10 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* <p>
* The type of the identifier attribute may be {@link UUID} or {@link String}.
*
* @author Steve Ebersole
*
* @see org.hibernate.id.uuid.UuidGenerator
* @since 6.0
*
* @author Steve Ebersole
*/
@IdGeneratorType(org.hibernate.id.uuid.UuidGenerator.class)
@ValueGenerationType(generatedBy = org.hibernate.id.uuid.UuidGenerator.class)
@ -58,4 +62,13 @@ public @interface UuidGenerator {
* Specifies which {@linkplain Style style} of UUID generation should be used.
*/
Style style() default Style.AUTO;
/**
* Allows to provide a specific, generally custom, value generation implementation.
*
* @apiNote If algorithm is specified, it is expected that {@linkplain #style()} be
* {@linkplain Style#AUTO}.
*/
@Incubating
Class<? extends UuidValueGenerator> algorithm() default UuidValueGenerator.class;
}

View File

@ -23,7 +23,7 @@ import org.hibernate.internal.util.BytesHelper;
*
* @author Steve Ebersole
*/
public class CustomVersionOneStrategy implements UUIDGenerationStrategy, UuidGenerator.ValueGenerator {
public class CustomVersionOneStrategy implements UUIDGenerationStrategy, UuidValueGenerator {
@Override
public int getGeneratedVersion() {
return 1;

View File

@ -16,7 +16,7 @@ import org.hibernate.id.UUIDGenerationStrategy;
*
* @author Steve Ebersole
*/
public class StandardRandomStrategy implements UUIDGenerationStrategy, UuidGenerator.ValueGenerator {
public class StandardRandomStrategy implements UUIDGenerationStrategy, UuidValueGenerator {
public static final StandardRandomStrategy INSTANCE = new StandardRandomStrategy();
/**

View File

@ -8,39 +8,56 @@ package org.hibernate.id.uuid;
import java.lang.reflect.Member;
import java.util.EnumSet;
import java.util.Locale;
import java.util.UUID;
import org.hibernate.HibernateException;
import org.hibernate.Internal;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.generator.BeforeExecutionGenerator;
import org.hibernate.generator.EventType;
import org.hibernate.generator.EventTypeSets;
import org.hibernate.generator.GeneratorCreationContext;
import org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext;
import org.hibernate.generator.BeforeExecutionGenerator;
import org.hibernate.type.descriptor.java.UUIDJavaType;
import org.hibernate.type.descriptor.java.UUIDJavaType.ValueTransformer;
import static org.hibernate.annotations.UuidGenerator.Style.AUTO;
import static org.hibernate.annotations.UuidGenerator.Style.TIME;
import static org.hibernate.generator.EventTypeSets.INSERT_ONLY;
import static org.hibernate.internal.util.ReflectHelper.getPropertyType;
/**
* Generates {@link UUID}s.
* {@linkplain org.hibernate.generator.Generator} for producing {@link UUID} values.
* <p/>
* Uses a {@linkplain UuidValueGenerator} and {@linkplain ValueTransformer} to
* generate the values.
*
* @see org.hibernate.annotations.UuidGenerator
*/
public class UuidGenerator implements BeforeExecutionGenerator {
interface ValueGenerator {
UUID generateUuid(SharedSessionContractImplementor session);
}
private final ValueGenerator generator;
private final UuidValueGenerator generator;
private final ValueTransformer valueTransformer;
private UuidGenerator(
org.hibernate.annotations.UuidGenerator config,
Member idMember) {
if ( config.style() == TIME ) {
if ( config.algorithm() != UuidValueGenerator.class ) {
if ( config.style() != AUTO ) {
throw new MappingException(
String.format(
Locale.ROOT,
"Style [%s] should not be specified with custom UUID value generator : %s.%s",
config.style().name(),
idMember.getDeclaringClass().getName(),
idMember.getName()
)
);
}
generator = instantiateCustomGenerator( config.algorithm() );
}
else if ( config.style() == TIME ) {
generator = new CustomVersionOneStrategy();
}
else {
@ -63,6 +80,15 @@ public class UuidGenerator implements BeforeExecutionGenerator {
}
}
private static UuidValueGenerator instantiateCustomGenerator(Class<? extends UuidValueGenerator> algorithmClass) {
try {
return algorithmClass.getDeclaredConstructor().newInstance();
}
catch (Exception e) {
throw new HibernateException( "Unable to instantiate " + algorithmClass.getName(), e );
}
}
public UuidGenerator(
org.hibernate.annotations.UuidGenerator config,
Member idMember,
@ -89,4 +115,14 @@ public class UuidGenerator implements BeforeExecutionGenerator {
public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
return valueTransformer.transform( generator.generateUuid( session ) );
}
@Internal
public UuidValueGenerator getValueGenerator() {
return generator;
}
@Internal
public ValueTransformer getValueTransformer() {
return valueTransformer;
}
}

View File

@ -0,0 +1,24 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.id.uuid;
import java.util.UUID;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
/**
* Represents a specific algorithm for producing UUID values. Used in
* conjunction with {@linkplain UuidGenerator} and
*
* @author Steve Ebersole
*/
public interface UuidValueGenerator {
/**
* Generate the UUID value
*/
UUID generateUuid(SharedSessionContractImplementor session);
}

View File

@ -0,0 +1,49 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.id.uuid.annotation;
import java.util.UUID;
import org.hibernate.annotations.UuidGenerator;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Basic;
/**
* @author Steve Ebersole
*/
@Entity
public class AnotherEntity {
@Id
@GeneratedValue
@UuidGenerator( algorithm = CustomUuidValueGenerator.class )
private UUID id;
@Basic
private String name;
protected AnotherEntity() {
// for Hibernate use
}
public AnotherEntity(String name) {
this.name = name;
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -0,0 +1,25 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.id.uuid.annotation;
import java.util.UUID;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.uuid.UuidValueGenerator;
/**
* @author Steve Ebersole
*/
public class CustomUuidValueGenerator implements UuidValueGenerator {
private long counter = 0;
@Override
public UUID generateUuid(SharedSessionContractImplementor session) {
final UUID sessionIdentifier = session.getSessionIdentifier();
return new UUID( sessionIdentifier.getMostSignificantBits(), ++counter );
}
}

View File

@ -0,0 +1,71 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.id.uuid.annotation;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.id.factory.IdentifierGeneratorFactory;
import org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.RootClass;
import org.hibernate.service.ServiceRegistry;
/**
* @author Steve Ebersole
*/
public class IdGeneratorCreationContext implements CustomIdGeneratorCreationContext {
private final MetadataImplementor domainModel;
private final RootClass entityMapping;
public IdGeneratorCreationContext(MetadataImplementor domainModel, RootClass entityMapping) {
this.domainModel = domainModel;
this.entityMapping = entityMapping;
assert entityMapping.getIdentifierProperty() != null;
}
@Override
public IdentifierGeneratorFactory getIdentifierGeneratorFactory() {
return domainModel.getMetadataBuildingOptions().getIdentifierGeneratorFactory();
}
@Override
public RootClass getRootClass() {
return entityMapping;
}
@Override
public Database getDatabase() {
return domainModel.getDatabase();
}
@Override
public ServiceRegistry getServiceRegistry() {
return domainModel.getMetadataBuildingOptions().getServiceRegistry();
}
@Override
public String getDefaultCatalog() {
return "";
}
@Override
public String getDefaultSchema() {
return "";
}
@Override
public PersistentClass getPersistentClass() {
return entityMapping;
}
@Override
public Property getProperty() {
return entityMapping.getIdentifierProperty();
}
}

View File

@ -6,34 +6,70 @@
*/
package org.hibernate.orm.test.id.uuid.annotation;
import java.util.UUID;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.dialect.SybaseDialect;
import org.hibernate.generator.Generator;
import org.hibernate.id.factory.IdentifierGeneratorFactory;
import org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext;
import org.hibernate.id.uuid.StandardRandomStrategy;
import org.hibernate.id.uuid.UuidGenerator;
import org.hibernate.mapping.BasicValue;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.RootClass;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.DomainModelScope;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.SkipForDialect;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@DomainModel( annotatedClasses = {TheEntity.class, TheOtherEntity.class} )
@SuppressWarnings("JUnitMalformedDeclaration")
@DomainModel( annotatedClasses = {TheEntity.class, TheOtherEntity.class, AnotherEntity.class} )
@SessionFactory
@SkipForDialect( dialectClass = SybaseDialect.class, matchSubTypes = true,
reason = "Skipped for Sybase to avoid problems with UUIDs potentially ending with a trailing 0 byte")
public class UuidGeneratorAnnotationTests {
@Test
public void verifyModel(DomainModelScope scope) {
public void verifyRandomUuidGeneratorModel(DomainModelScope scope) {
scope.withHierarchy( TheEntity.class, (descriptor) -> {
final Property idProperty = descriptor.getIdentifierProperty();
final BasicValue value = (BasicValue) idProperty.getValue();
assertThat( value.getCustomIdGeneratorCreator() ).isNotNull();
final Generator generator = value.getCustomIdGeneratorCreator().createGenerator( new IdGeneratorCreationContext(
scope.getDomainModel(),
descriptor
));
final String strategy = value.getIdentifierGeneratorStrategy();
assertThat( strategy ).isEqualTo( "assigned" );
assertThat( generator ).isInstanceOf( UuidGenerator.class );
final UuidGenerator uuidGenerator = (UuidGenerator) generator;
assertThat( uuidGenerator.getValueGenerator() ).isInstanceOf( StandardRandomStrategy.class );
} );
}
@Test
public void verifyCustomUuidGeneratorModel(DomainModelScope scope) {
scope.withHierarchy( AnotherEntity.class, (descriptor) -> {
final Property idProperty = descriptor.getIdentifierProperty();
final BasicValue value = (BasicValue) idProperty.getValue();
assertThat( value.getCustomIdGeneratorCreator() ).isNotNull();
final Generator generator = value.getCustomIdGeneratorCreator().createGenerator( new IdGeneratorCreationContext(
scope.getDomainModel(),
descriptor
));
assertThat( generator ).isInstanceOf( UuidGenerator.class );
final UuidGenerator uuidGenerator = (UuidGenerator) generator;
assertThat( uuidGenerator.getValueGenerator() ).isInstanceOf( CustomUuidValueGenerator.class );
} );
}
@ -56,4 +92,29 @@ public class UuidGeneratorAnnotationTests {
assertThat( gavin.id ).isNotNull();
} );
}
@Test
void testCustomValueGenerator(SessionFactoryScope sessionFactoryScope) {
sessionFactoryScope.inTransaction( (session) -> {
final UUID sessionIdentifier = session.getSessionIdentifier();
final AnotherEntity anotherEntity = new AnotherEntity( "johnny" );
session.persist( anotherEntity );
assertThat( anotherEntity.getId() ).isNotNull();
session.flush();
assertThat( anotherEntity.getId() ).isNotNull();
assertThat( anotherEntity.getId().getMostSignificantBits() ).isEqualTo( sessionIdentifier.getMostSignificantBits() );
assertThat( anotherEntity.getId().getLeastSignificantBits() ).isEqualTo( 1L );
} );
}
@AfterEach
void dropTestData(SessionFactoryScope sessionFactoryScope) {
sessionFactoryScope.inTransaction( (session) -> {
session.createMutationQuery( "delete TheEntity" ).executeUpdate();
session.createMutationQuery( "delete TheOtherEntity" ).executeUpdate();
session.createMutationQuery( "delete AnotherEntity" ).executeUpdate();
} );
}
}