From 96d6ab74174359af4dd891ae7ed12f2416f54b49 Mon Sep 17 00:00:00 2001 From: Filip Prochazka Date: Fri, 15 Nov 2024 20:22:46 +0100 Subject: [PATCH] add typed-ids-hibernate-62 --- gradle/libs.versions.toml | 8 +- .../typed-ids-hibernate-62/build.gradle.kts | 20 ++ .../uuid/hibernate/ObjectUuidArrayType.java | 127 ++++++++++++ .../uuid/hibernate/ObjectUuidJavaType.java | 183 ++++++++++++++++++ .../uuid/hibernate/ObjectUuidType.java | 173 +++++++++++++++++ .../hibernate/ObjectUuidTypesContributor.java | 113 +++++++++++ .../hibernate/jdbc/BinaryUuidJdbcType.java | 48 +++++ .../hibernate/jdbc/NativeUuidJdbcType.java | 35 ++++ .../ArticleDefaultExplicitMapping.java | 71 +++++++ .../hibernate/ObjectUuidTypeMySQLTest.java | 77 ++++++++ .../ObjectUuidTypePostgreSQLTest.java | 76 ++++++++ .../build.gradle.kts | 14 ++ .../hibernate/v63/ArticleTestingEntity.java | 69 +++++++ ...ctUuidTypeIndexedHibernate63MySQLTest.java | 70 +++++++ ...dTypeIndexedHibernate63PostgreSQLTest.java | 69 +++++++ 15 files changed, 1148 insertions(+), 5 deletions(-) create mode 100644 modules/typed-ids-hibernate-62/build.gradle.kts create mode 100644 modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidArrayType.java create mode 100644 modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidJavaType.java create mode 100644 modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidType.java create mode 100644 modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypesContributor.java create mode 100644 modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/jdbc/BinaryUuidJdbcType.java create mode 100644 modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/jdbc/NativeUuidJdbcType.java create mode 100644 modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ArticleDefaultExplicitMapping.java create mode 100644 modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypeMySQLTest.java create mode 100644 modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypePostgreSQLTest.java create mode 100644 testing/testing-typed-ids-hibernate-62-indexed/build.gradle.kts create mode 100644 testing/testing-typed-ids-hibernate-62-indexed/src/main/java/org/framefork/typedIds/uuid/hibernate/v63/ArticleTestingEntity.java create mode 100644 testing/testing-typed-ids-hibernate-62-indexed/src/test/java/org/framefork/typedIds/uuid/hibernate/v63/ObjectUuidTypeIndexedHibernate63MySQLTest.java create mode 100644 testing/testing-typed-ids-hibernate-62-indexed/src/test/java/org/framefork/typedIds/uuid/hibernate/v63/ObjectUuidTypeIndexedHibernate63PostgreSQLTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1e5f33b..34c14f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,9 +37,7 @@ hibernate-orm-v63 = { group = "org.hibernate.orm", name = "hibernate-core", vers hibernate-orm-v64 = { group = "org.hibernate.orm", name = "hibernate-core", version = "6.4.10.Final" } hibernate-orm-v65 = { group = "org.hibernate.orm", name = "hibernate-core", version = "6.5.3.Final" } hibernate-orm-v66 = { group = "org.hibernate.orm", name = "hibernate-core", version = "6.6.2.Final" } -hypersistence-utils-hibernate55 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version = "3.8.3" } -hypersistence-utils-hibernate60 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version = "3.8.3" } -hypersistence-utils-hibernate62 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version = "3.8.3" } +hypersistence-utils-hibernate55 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-55", version = "3.8.3" } +hypersistence-utils-hibernate60 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-60", version = "3.8.3" } +hypersistence-utils-hibernate62 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-62", version = "3.8.3" } hypersistence-utils-hibernate63 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version = "3.8.3" } - - diff --git a/modules/typed-ids-hibernate-62/build.gradle.kts b/modules/typed-ids-hibernate-62/build.gradle.kts new file mode 100644 index 0000000..5937be0 --- /dev/null +++ b/modules/typed-ids-hibernate-62/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("framefork.java-public") +} + +dependencies { + api(project(":typed-ids")) + api(libs.hibernate.orm.v62) + api(libs.hypersistence.utils.hibernate62) + api(libs.ateoClassindex) + + compileOnly(libs.jetbrains.annotations) + + compileOnly(libs.autoService.annotations) + annotationProcessor(libs.autoService.processor) + + testImplementation(project(":typed-ids-testing")) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +project.description = "TypeIds seamless integration into Hibernate ORMs type system" diff --git a/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidArrayType.java b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidArrayType.java new file mode 100644 index 0000000..1d7daca --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidArrayType.java @@ -0,0 +1,127 @@ +package org.framefork.typedIds.uuid.hibernate; + +import io.hypersistence.utils.hibernate.type.array.internal.AbstractArrayType; +import io.hypersistence.utils.hibernate.type.array.internal.AbstractArrayTypeDescriptor; +import io.hypersistence.utils.hibernate.type.array.internal.ArrayUtil; +import io.hypersistence.utils.hibernate.type.util.ParameterizedParameterType; +import org.framefork.typedIds.uuid.ObjectUuid; +import org.hibernate.HibernateException; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.sql.Array; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Properties; +import java.util.UUID; + +public class ObjectUuidArrayType> extends AbstractArrayType +{ + + public ObjectUuidArrayType( + final Class arrayClass, + final ObjectUuidType objectUuidType + ) + { + super( + new ObjectUUIDArrayTypeDescriptor<>(arrayClass, objectUuidType) + ); + var parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + parameters.put(SQL_ARRAY_TYPE, getJavaTypeDescriptor().getSqlArrayType()); + setParameterValues(parameters); + } + + public String[] getRegistrationKeys() + { + return new String[]{ + returnedClass().getName(), + returnedClass().getTypeName(), + returnedClass().getCanonicalName(), + }; + } + + @Override + public ObjectUUIDArrayTypeDescriptor getJavaTypeDescriptor() + { + return (ObjectUUIDArrayTypeDescriptor) super.getJavaTypeDescriptor(); + } + + public static > ObjectUuidArrayType create( + final Class elementClass, + final ObjectUuidType objectUuidType + ) + { + return new ObjectUuidArrayType<>( + ArrayUtil.toArrayClass(elementClass), + objectUuidType + ); + } + + @SuppressWarnings("unchecked") + public static final class ObjectUUIDArrayTypeDescriptor> extends AbstractArrayTypeDescriptor + { + + private final ObjectUuidJavaType objectUuidJavaType; + + public ObjectUUIDArrayTypeDescriptor( + final Class arrayObjectClass, + final ObjectUuidType objectUuidType + ) + { + super(arrayObjectClass); + this.objectUuidJavaType = objectUuidType.getExpressibleJavaType(); + } + + @Override + protected String getSqlArrayType() + { + return "uuid"; + } + + @Override + public X unwrap( + final T[] value, + final Class type, + final WrapperOptions options + ) + { + if (value.length > 0) { + @SuppressWarnings("unchecked") + var result = (X) Arrays.stream(value) + .map(item -> objectUuidJavaType.unwrap(item, UUID.class, options)) + .toArray(UUID[]::new); + + return result; + } + + return super.unwrap(value, type, options); + } + + @Override + public T[] wrap( + final X value, + final WrapperOptions options + ) + { + if (value instanceof Array array) { + try { + var uuidsArray = ArrayUtil.unwrapArray((Object[]) array.getArray(), Object[].class); + + var objectUuidsArray = Arrays.stream(uuidsArray) + .map(item -> objectUuidJavaType.wrap(item, options)) + .toArray(Object[]::new); + + return ArrayUtil.unwrapArray(objectUuidsArray, getJavaTypeClass()); + + } catch (SQLException e) { + throw new HibernateException(new IllegalArgumentException(e)); + } + } + + return super.wrap(value, options); + } + + } + +} diff --git a/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidJavaType.java b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidJavaType.java new file mode 100644 index 0000000..3ee5d77 --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidJavaType.java @@ -0,0 +1,183 @@ +package org.framefork.typedIds.uuid.hibernate; + +import org.framefork.typedIds.uuid.ObjectUuid; +import org.framefork.typedIds.uuid.ObjectUuidTypeUtils; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.BasicJavaType; +import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan; +import org.hibernate.type.descriptor.java.MutabilityPlan; +import org.hibernate.type.descriptor.java.UUIDJavaType; +import org.hibernate.type.descriptor.jdbc.AdjustableJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.VarbinaryJdbcType; +import org.hibernate.usertype.DynamicParameterizedType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Type; +import java.util.Objects; +import java.util.Properties; +import java.util.UUID; + +public class ObjectUuidJavaType implements BasicJavaType>, DynamicParameterizedType, Serializable +{ + + public static final int UUID_BYTE_LENGTH = 16; + + private final UUIDJavaType inner; + + @Nullable + private Class> identifierClass; + @Nullable + private MethodHandle constructor; + + public ObjectUuidJavaType() + { + this.inner = UUIDJavaType.INSTANCE; + } + + @SuppressWarnings("unchecked") + @Override + public void setParameterValues(final Properties parameters) + { + @Nullable var parameterType = (ParameterType) parameters.get(PARAMETER_TYPE); + if (parameterType != null) { + this.identifierClass = (Class>) parameterType.getReturnedClass(); + + } else { + String entityClass = Objects.requireNonNull(parameters.get(ENTITY), "parameters.get(ENTITY) must not be null").toString(); + String propertyName = Objects.requireNonNull(parameters.get(PROPERTY), "parameters.get(PROPERTY) must not be null").toString(); + + this.identifierClass = ObjectUuidTypeUtils.readIdentifierClass(entityClass, propertyName); + } + + if (!ObjectUuid.class.isAssignableFrom(identifierClass)) { + throw new IllegalArgumentException("Type %s is not a subtype of %s".formatted(identifierClass, ObjectUuid.class)); + } + + this.constructor = ObjectUuidTypeUtils.getMainConstructor(identifierClass); + } + + @Override + public Type getJavaType() + { + return getJavaTypeClass(); + } + + @Override + public Class> getJavaTypeClass() + { + return Objects.requireNonNull(identifierClass, "identifierClass must not be null"); + } + + @Override + public int extractHashCode(final ObjectUuid value) + { + return Objects.hashCode(value); + } + + @Override + public boolean areEqual( + final ObjectUuid one, + final ObjectUuid another + ) + { + return Objects.equals(one, another); + } + + @Override + public JdbcType getRecommendedJdbcType(final JdbcTypeIndicators indicators) + { + final JdbcType descriptor = indicators.getJdbcType(indicators.resolveJdbcTypeCode(SqlTypes.UUID)); + return descriptor instanceof AdjustableJdbcType + ? ((AdjustableJdbcType) descriptor).resolveIndicatedType(indicators, this) + : descriptor; + } + + @Override + public long getDefaultSqlLength(final Dialect dialect, final JdbcType jdbcType) + { + if (jdbcType instanceof VarbinaryJdbcType) { + return UUID_BYTE_LENGTH; + } + + return Size.DEFAULT_LENGTH; + } + + @Nullable + @Override + public X unwrap( + @Nullable final ObjectUuid value, + @NotNull final Class type, + @NotNull final WrapperOptions options + ) + { + if (value == null) { + return null; + } + + return inner.unwrap(value.toNativeUuid(), type, options); + } + + @Nullable + @Override + public ObjectUuid wrap( + @Nullable final X value, + @NotNull final WrapperOptions options + ) + { + if (value == null) { + return null; + } + + return wrapUuidToIdentifier(inner.wrap(value, options)); + } + + @Nullable + @Override + public ObjectUuid fromString(@Nullable final CharSequence string) + { + return (string == null) ? null : wrapUuidToIdentifier(UUID.fromString(string.toString())); + } + + @Override + public MutabilityPlan> getMutabilityPlan() + { + return ImmutableMutabilityPlan.instance(); + } + + private ObjectUuid wrapUuidToIdentifier(final UUID uuid) + { + return ObjectUuidTypeUtils.wrapUuidToIdentifier( + uuid, + Objects.requireNonNull(constructor, "constructor was not yet initialized") + ); + } + + @Override + public String toString() + { + return "object-uuid(%s)".formatted(identifierClass != null ? identifierClass.getName() : "???"); + } + + @SuppressWarnings("unused") + private void writeObject(final ObjectOutputStream stream) + { + throw new UnsupportedOperationException("Serialization not supported"); + } + + @SuppressWarnings("unused") + private void readObject(final ObjectInputStream stream) + { + throw new UnsupportedOperationException("Serialization not supported"); + } + +} diff --git a/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidType.java b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidType.java new file mode 100644 index 0000000..c2fce2d --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidType.java @@ -0,0 +1,173 @@ +package org.framefork.typedIds.uuid.hibernate; + +import io.hypersistence.utils.hibernate.type.ImmutableType; +import io.hypersistence.utils.hibernate.type.util.ParameterizedParameterType; +import org.framefork.typedIds.uuid.ObjectUuid; +import org.framefork.typedIds.uuid.ObjectUuidTypeUtils; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.query.BindableType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.java.MutabilityPlan; +import org.hibernate.type.descriptor.java.MutabilityPlanExposer; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.spi.TypeConfiguration; +import org.hibernate.type.spi.TypeConfigurationAware; +import org.hibernate.usertype.DynamicParameterizedType; +import org.jetbrains.annotations.Nullable; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; +import java.util.Properties; + +public class ObjectUuidType extends ImmutableType> implements + BindableType>, + DomainType>, + MutabilityPlanExposer>, + DynamicParameterizedType, + TypeConfigurationAware +{ + + public static final String NAME = "object-uuid"; + + private final ObjectUuidJavaType javaTypeDescriptor = new ObjectUuidJavaType(); + + @Nullable + private JdbcType jdbcTypeDescriptor; + + @Nullable + private TypeConfiguration typeConfiguration; + + public ObjectUuidType() + { + this((JdbcType) null); + } + + public ObjectUuidType(@Nullable final JdbcType uuidJdbcType) + { + super(ObjectUuidTypeUtils.getObjectUuidRawClass()); + this.jdbcTypeDescriptor = uuidJdbcType; + } + + public ObjectUuidType(final Class implClass) + { + this(implClass, null); + } + + public ObjectUuidType(final Class implClass, @Nullable final JdbcType uuidJdbcType) + { + this(uuidJdbcType); + + var parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(implClass)); + this.setParameterValues(parameters); + } + + @Override + public void setTypeConfiguration(final TypeConfiguration typeConfiguration) + { + this.typeConfiguration = typeConfiguration; + } + + @Nullable + @Override + public TypeConfiguration getTypeConfiguration() + { + return typeConfiguration; + } + + @Override + public String getName() + { + return NAME; + } + + @Override + public int getSqlType() + { + return SqlTypes.UUID; + } + + @Override + public long getDefaultSqlLength(final Dialect dialect, final JdbcType jdbcType) + { + return javaTypeDescriptor.getDefaultSqlLength(dialect, jdbcType); + } + + @Override + public JdbcType getJdbcType(final TypeConfiguration typeConfiguration) + { + return Objects.requireNonNull(jdbcTypeDescriptor, "jdbcTypeDescriptor is not yet initialized"); + } + + @Override + public ObjectUuidJavaType getExpressibleJavaType() + { + return javaTypeDescriptor; + } + + @Override + public void setParameterValues(final Properties parameters) + { + javaTypeDescriptor.setParameterValues(parameters); + if (jdbcTypeDescriptor == null && typeConfiguration != null) { + jdbcTypeDescriptor = typeConfiguration.getJdbcTypeRegistry().getDescriptor(getSqlType()); + } + } + + @Override + public Class> returnedClass() + { + return javaTypeDescriptor.getJavaTypeClass(); + } + + @Override + public Class> getBindableJavaType() + { + return returnedClass(); + } + + @Nullable + @Override + public ObjectUuid fromStringValue(@Nullable final CharSequence sequence) + { + return javaTypeDescriptor.fromString(sequence); + } + + @Override + public MutabilityPlan> getExposedMutabilityPlan() + { + return javaTypeDescriptor.getMutabilityPlan(); + } + + /** + * the nullSafeGet() is delegated here in the {@link ImmutableType} + */ + @Nullable + @Override + protected ObjectUuid get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException + { + return getJdbcType(session.getTypeConfiguration()) + .getExtractor(javaTypeDescriptor).extract(rs, position, session); + } + + /** + * the nullSafeSet() is delegated here in the {@link ImmutableType} + */ + @Override + protected void set(PreparedStatement st, @Nullable ObjectUuid value, int index, SharedSessionContractImplementor session) throws SQLException + { + getJdbcType(session.getTypeConfiguration()) + .getBinder(javaTypeDescriptor).bind(st, value, index, session); + } + + @Override + public String toString() + { + return "%s backed by %s".formatted(javaTypeDescriptor, jdbcTypeDescriptor != null ? jdbcTypeDescriptor : "???"); + } + +} diff --git a/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypesContributor.java b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypesContributor.java new file mode 100644 index 0000000..8259266 --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypesContributor.java @@ -0,0 +1,113 @@ +package org.framefork.typedIds.uuid.hibernate; + +import com.google.auto.service.AutoService; +import org.atteo.classindex.ClassIndex; +import org.framefork.typedIds.uuid.ObjectUuid; +import org.framefork.typedIds.uuid.hibernate.jdbc.BinaryUuidJdbcType; +import org.framefork.typedIds.uuid.hibernate.jdbc.NativeUuidJdbcType; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.boot.model.TypeContributor; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@AutoService(TypeContributor.class) +public class ObjectUuidTypesContributor implements TypeContributor +{ + + /** + * https://mariadb.com/kb/en/uuid-data-type/ + */ + private static final DatabaseVersion MARIADB_NATIVE_UUID_SINCE = DatabaseVersion.make(10, 7); + + private static final boolean CLASS_INDEX_PRESENT; + + static { + boolean found = false; + try { + Class.forName("org.atteo.classindex.ClassIndex"); + found = true; + } catch (ClassNotFoundException ignored) { + } + + CLASS_INDEX_PRESENT = found; + } + + @Override + public void contribute( + final TypeContributions typeContributions, + final ServiceRegistry serviceRegistry + ) + { + remapUuidType(typeContributions, serviceRegistry); + contributeIndexedTypes(typeContributions); + } + + private void remapUuidType(final TypeContributions typeContributions, final ServiceRegistry serviceRegistry) + { + JdbcServices jdbcServices = serviceRegistry.requireService(JdbcServices.class); + Dialect dialect = jdbcServices.getDialect(); + + DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); + if (!jdbcTypeRegistry.hasRegisteredDescriptor(SqlTypes.UUID)) { + if (dialect instanceof MariaDBDialect && dialect.getVersion().isSameOrAfter(ObjectUuidTypesContributor.MARIADB_NATIVE_UUID_SINCE)) { + typeContributions.contributeJdbcType(NativeUuidJdbcType.INSTANCE); + + } else if (dialect instanceof MySQLDialect) { + typeContributions.contributeJdbcType(BinaryUuidJdbcType.INSTANCE); + ddlTypeRegistry.addDescriptorIfAbsent(new DdlTypeImpl(SqlTypes.UUID, false, "binary(16)", "binary(16)", "binary(16)", dialect)); + + } else { + typeContributions.contributeJdbcType(NativeUuidJdbcType.INSTANCE); + } + } + } + + private void contributeIndexedTypes(final TypeContributions typeContributions) + { + if (!CLASS_INDEX_PRESENT) { + return; + } + + TypeConfiguration typeConfiguration = typeContributions.getTypeConfiguration(); + JdbcType uuidJdbcType = typeConfiguration.getJdbcTypeRegistry().getDescriptor(SqlTypes.UUID); + + var idTypes = getIndexedSubclassesFor(ObjectUuid.class); + for (var idType : idTypes) { + var objectUuidType = new ObjectUuidType(idType, uuidJdbcType); + + typeContributions.contributeType(objectUuidType); + + @SuppressWarnings("unchecked") + var objectUuidArrayType = (ObjectUuidArrayType) ObjectUuidArrayType.create(idType, objectUuidType); + typeContributions.contributeType(objectUuidArrayType); + typeConfiguration.getBasicTypeRegistry().register(objectUuidArrayType, objectUuidArrayType.getRegistrationKeys()); + } + } + + private List> getIndexedSubclassesFor(final Class typeClass) + { + Iterable> subclasses = ClassIndex.getSubclasses(typeClass); + + List> result = new ArrayList<>(); + subclasses.forEach(result::add); + result.sort(Comparator.comparing(Class::getName)); + + return result; + } + +} diff --git a/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/jdbc/BinaryUuidJdbcType.java b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/jdbc/BinaryUuidJdbcType.java new file mode 100644 index 0000000..6602a6a --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/jdbc/BinaryUuidJdbcType.java @@ -0,0 +1,48 @@ +package org.framefork.typedIds.uuid.hibernate.jdbc; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.VarbinaryJdbcType; + +public class BinaryUuidJdbcType extends VarbinaryJdbcType +{ + + public static final BinaryUuidJdbcType INSTANCE = new BinaryUuidJdbcType(); + + @Override + public String getFriendlyName() + { + return "uuid-bin(16)"; + } + + @Override + public int getJdbcTypeCode() + { + return SqlTypes.UUID; + } + + @Override + protected boolean shouldUseMaterializedLob(final JdbcTypeIndicators indicators) + { + return false; + } + + @Override + public boolean isLob() + { + return false; + } + + @Override + public boolean isLobOrLong() + { + return false; + } + + @Override + public String toString() + { + return getFriendlyName(); + } + +} diff --git a/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/jdbc/NativeUuidJdbcType.java b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/jdbc/NativeUuidJdbcType.java new file mode 100644 index 0000000..052a526 --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/main/java/org/framefork/typedIds/uuid/hibernate/jdbc/NativeUuidJdbcType.java @@ -0,0 +1,35 @@ +package org.framefork.typedIds.uuid.hibernate.jdbc; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; + +public class NativeUuidJdbcType extends UUIDJdbcType +{ + + public static final NativeUuidJdbcType INSTANCE = new NativeUuidJdbcType(); + + @Override + public String getFriendlyName() + { + return "uuid"; + } + + @Override + public int getJdbcTypeCode() + { + return SqlTypes.UUID; + } + + @Override + public int getDefaultSqlTypeCode() + { + return SqlTypes.UUID; + } + + @Override + public String toString() + { + return "uuid-native"; + } + +} diff --git a/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ArticleDefaultExplicitMapping.java b/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ArticleDefaultExplicitMapping.java new file mode 100644 index 0000000..274937a --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ArticleDefaultExplicitMapping.java @@ -0,0 +1,71 @@ +package org.framefork.typedIds.uuid.hibernate; + +import org.framefork.typedIds.uuid.ObjectUuid; +import org.hibernate.annotations.Type; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.util.UUID; + +@Entity +@Table(name = ArticleDefaultExplicitMapping.TABLE_NAME) +public class ArticleDefaultExplicitMapping +{ + + public static final String TABLE_NAME = "article_default_explicit_mapping"; + + @jakarta.persistence.Id + @Column(nullable = false) + @Type(ObjectUuidType.class) + private Id id; + + @Column(nullable = false) + private String title; + + public ArticleDefaultExplicitMapping(final String title) + { + this.id = Id.random(); + this.title = title; + } + + protected ArticleDefaultExplicitMapping() + { + } + + public Id getId() + { + return id; + } + + public String getTitle() + { + return title; + } + + public static final class Id extends ObjectUuid + { + + private Id(final UUID inner) + { + super(inner); + } + + public static Id random() + { + return ObjectUuid.randomUUID(Id::new); + } + + public static Id fromString(final String value) + { + return ObjectUuid.fromString(Id::new, value); + } + + public static Id fromUuid(final UUID value) + { + return ObjectUuid.fromUuid(Id::new, value); + } + + } + +} diff --git a/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypeMySQLTest.java b/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypeMySQLTest.java new file mode 100644 index 0000000..a058319 --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypeMySQLTest.java @@ -0,0 +1,77 @@ +package org.framefork.typedIds.uuid.hibernate; + +import org.framefork.typedIds.hibernate.tests.AbstractMySQLIntegrationTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class ObjectUuidTypeMySQLTest extends AbstractMySQLIntegrationTest +{ + + @Override + protected Class[] entities() + { + return new Class[]{ + ArticleDefaultExplicitMapping.class, + }; + } + + @Test + public void testSchema() + { + doInJPA(em -> { + var result = (Tuple) em.createNativeQuery("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = :table_name AND column_name = :column_name", Tuple.class) + .setParameter("table_name", ArticleDefaultExplicitMapping.TABLE_NAME) + .setParameter("column_name", "id") + .getSingleResult(); + + Assertions.assertEquals("binary", result.get("data_type", String.class).toLowerCase()); + Assertions.assertEquals("binary(16)", result.get("column_type", String.class).toLowerCase()); + }); + } + + @Test + public void testUsage() + { + Map idsByTitle = new HashMap<>(); + + doInJPA(em -> { + List articles = List.of( + new ArticleDefaultExplicitMapping("one"), + new ArticleDefaultExplicitMapping("two"), + new ArticleDefaultExplicitMapping("three") + ); + + articles.forEach(em::persist); + articles.forEach(article -> idsByTitle.put(article.getTitle(), article.getId())); + em.flush(); + }); + + var idOfTwo = Objects.requireNonNull(idsByTitle.get("two"), "id must not be null"); + + doInJPA(em -> { + var article = em.find(ArticleDefaultExplicitMapping.class, idOfTwo); + Assertions.assertEquals("two", article.getTitle()); + }); + + doInJPA(em -> { + var article = em.createQuery("SELECT a FROM ArticleDefaultExplicitMapping a WHERE a.id = :id", ArticleDefaultExplicitMapping.class) + .setParameter("id", idOfTwo) + .getSingleResult(); + Assertions.assertEquals("two", article.getTitle()); + }); + + doInJPA(em -> { + var articles = em.createQuery("SELECT a FROM ArticleDefaultExplicitMapping a WHERE a.id IN (:ids)", ArticleDefaultExplicitMapping.class) + .setParameter("ids", List.copyOf(idsByTitle.values())) + .getResultList(); + Assertions.assertEquals(3, articles.size()); + }); + } + +} diff --git a/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypePostgreSQLTest.java b/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypePostgreSQLTest.java new file mode 100644 index 0000000..39c7550 --- /dev/null +++ b/modules/typed-ids-hibernate-62/src/test/java/org/framefork/typedIds/uuid/hibernate/ObjectUuidTypePostgreSQLTest.java @@ -0,0 +1,76 @@ +package org.framefork.typedIds.uuid.hibernate; + +import org.framefork.typedIds.hibernate.tests.AbstractPostgreSQLIntegrationTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class ObjectUuidTypePostgreSQLTest extends AbstractPostgreSQLIntegrationTest +{ + + @Override + protected Class[] entities() + { + return new Class[]{ + ArticleDefaultExplicitMapping.class, + }; + } + + @Test + public void testSchema() + { + doInJPA(em -> { + var result = (Tuple) em.createNativeQuery("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = :table_name AND column_name = :column_name", Tuple.class) + .setParameter("table_name", ArticleDefaultExplicitMapping.TABLE_NAME) + .setParameter("column_name", "id") + .getSingleResult(); + + Assertions.assertEquals("uuid", result.get("data_type", String.class).toLowerCase()); + }); + } + + @Test + public void testUsage() + { + Map idsByTitle = new HashMap<>(); + + doInJPA(em -> { + List articles = List.of( + new ArticleDefaultExplicitMapping("one"), + new ArticleDefaultExplicitMapping("two"), + new ArticleDefaultExplicitMapping("three") + ); + + articles.forEach(em::persist); + articles.forEach(article -> idsByTitle.put(article.getTitle(), article.getId())); + em.flush(); + }); + + var idOfTwo = Objects.requireNonNull(idsByTitle.get("two"), "id must not be null"); + + doInJPA(em -> { + var article = em.find(ArticleDefaultExplicitMapping.class, idOfTwo); + Assertions.assertEquals("two", article.getTitle()); + }); + + doInJPA(em -> { + var article = em.createQuery("SELECT a FROM ArticleDefaultExplicitMapping a WHERE a.id = :id", ArticleDefaultExplicitMapping.class) + .setParameter("id", idOfTwo) + .getSingleResult(); + Assertions.assertEquals("two", article.getTitle()); + }); + + doInJPA(em -> { + var articles = em.createQuery("SELECT a FROM ArticleDefaultExplicitMapping a WHERE a.id IN (:ids)", ArticleDefaultExplicitMapping.class) + .setParameter("ids", List.copyOf(idsByTitle.values())) + .getResultList(); + Assertions.assertEquals(3, articles.size()); + }); + } + +} diff --git a/testing/testing-typed-ids-hibernate-62-indexed/build.gradle.kts b/testing/testing-typed-ids-hibernate-62-indexed/build.gradle.kts new file mode 100644 index 0000000..ef17a6d --- /dev/null +++ b/testing/testing-typed-ids-hibernate-62-indexed/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("framefork.java") +} + +dependencies { + implementation(project(":typed-ids-hibernate-62")) + implementation(libs.hibernate.orm.v62) + implementation(libs.hypersistence.utils.hibernate62) + + annotationProcessor(libs.ateoClassindex) + + testImplementation(project(":typed-ids-testing")) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} diff --git a/testing/testing-typed-ids-hibernate-62-indexed/src/main/java/org/framefork/typedIds/uuid/hibernate/v63/ArticleTestingEntity.java b/testing/testing-typed-ids-hibernate-62-indexed/src/main/java/org/framefork/typedIds/uuid/hibernate/v63/ArticleTestingEntity.java new file mode 100644 index 0000000..bea4633 --- /dev/null +++ b/testing/testing-typed-ids-hibernate-62-indexed/src/main/java/org/framefork/typedIds/uuid/hibernate/v63/ArticleTestingEntity.java @@ -0,0 +1,69 @@ +package org.framefork.typedIds.uuid.hibernate.v63; + +import org.framefork.typedIds.uuid.ObjectUuid; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.util.UUID; + +@Entity +@Table(name = ArticleTestingEntity.TABLE_NAME) +public class ArticleTestingEntity +{ + + public static final String TABLE_NAME = "article"; + + @jakarta.persistence.Id + @Column(nullable = false) + private Id id; + + @Column(nullable = false) + private String title; + + public ArticleTestingEntity(final String title) + { + this.id = Id.random(); + this.title = title; + } + + protected ArticleTestingEntity() + { + } + + public Id getId() + { + return id; + } + + public String getTitle() + { + return title; + } + + public static final class Id extends ObjectUuid + { + + private Id(final UUID inner) + { + super(inner); + } + + public static Id random() + { + return ObjectUuid.randomUUID(Id::new); + } + + public static Id fromString(final String value) + { + return ObjectUuid.fromString(Id::new, value); + } + + public static Id fromUuid(final UUID value) + { + return ObjectUuid.fromUuid(Id::new, value); + } + + } + +} diff --git a/testing/testing-typed-ids-hibernate-62-indexed/src/test/java/org/framefork/typedIds/uuid/hibernate/v63/ObjectUuidTypeIndexedHibernate63MySQLTest.java b/testing/testing-typed-ids-hibernate-62-indexed/src/test/java/org/framefork/typedIds/uuid/hibernate/v63/ObjectUuidTypeIndexedHibernate63MySQLTest.java new file mode 100644 index 0000000..4caf4e5 --- /dev/null +++ b/testing/testing-typed-ids-hibernate-62-indexed/src/test/java/org/framefork/typedIds/uuid/hibernate/v63/ObjectUuidTypeIndexedHibernate63MySQLTest.java @@ -0,0 +1,70 @@ +package org.framefork.typedIds.uuid.hibernate.v63; + +import org.framefork.typedIds.hibernate.tests.AbstractMySQLIntegrationTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class ObjectUuidTypeIndexedHibernate63MySQLTest extends AbstractMySQLIntegrationTest +{ + + @Override + protected Class[] entities() + { + return new Class[]{ + ArticleTestingEntity.class, + }; + } + + @Test + public void testSchema() + { + doInJPA(em -> { + var result = (Tuple) em.createNativeQuery("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = :table_name AND column_name = :column_name", Tuple.class) + .setParameter("table_name", ArticleTestingEntity.TABLE_NAME) + .setParameter("column_name", "id") + .getSingleResult(); + + Assertions.assertEquals("binary", result.get("data_type", String.class).toLowerCase()); + Assertions.assertEquals("binary(16)", result.get("column_type", String.class).toLowerCase()); + }); + } + + @Test + public void testUsage() + { + Map idsByTitle = new HashMap<>(); + + doInJPA(em -> { + List articles = List.of( + new ArticleTestingEntity("one"), + new ArticleTestingEntity("two"), + new ArticleTestingEntity("three") + ); + + articles.forEach(em::persist); + articles.forEach(article -> idsByTitle.put(article.getTitle(), article.getId())); + em.flush(); + }); + + var idOfTwo = Objects.requireNonNull(idsByTitle.get("two"), "id must not be null"); + + doInJPA(em -> { + var article = em.find(ArticleTestingEntity.class, idOfTwo); + Assertions.assertEquals("two", article.getTitle()); + }); + + doInJPA(em -> { + var article = em.createQuery("SELECT a FROM ArticleTestingEntity a WHERE a.id = :id", ArticleTestingEntity.class) + .setParameter("id", idOfTwo) + .getSingleResult(); + Assertions.assertEquals("two", article.getTitle()); + }); + } + +} diff --git a/testing/testing-typed-ids-hibernate-62-indexed/src/test/java/org/framefork/typedIds/uuid/hibernate/v63/ObjectUuidTypeIndexedHibernate63PostgreSQLTest.java b/testing/testing-typed-ids-hibernate-62-indexed/src/test/java/org/framefork/typedIds/uuid/hibernate/v63/ObjectUuidTypeIndexedHibernate63PostgreSQLTest.java new file mode 100644 index 0000000..929930a --- /dev/null +++ b/testing/testing-typed-ids-hibernate-62-indexed/src/test/java/org/framefork/typedIds/uuid/hibernate/v63/ObjectUuidTypeIndexedHibernate63PostgreSQLTest.java @@ -0,0 +1,69 @@ +package org.framefork.typedIds.uuid.hibernate.v63; + +import org.framefork.typedIds.hibernate.tests.AbstractPostgreSQLIntegrationTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class ObjectUuidTypeIndexedHibernate63PostgreSQLTest extends AbstractPostgreSQLIntegrationTest +{ + + @Override + protected Class[] entities() + { + return new Class[]{ + ArticleTestingEntity.class, + }; + } + + @Test + public void testSchema() + { + doInJPA(em -> { + var result = (Tuple) em.createNativeQuery("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = :table_name AND column_name = :column_name", Tuple.class) + .setParameter("table_name", ArticleTestingEntity.TABLE_NAME) + .setParameter("column_name", "id") + .getSingleResult(); + + Assertions.assertEquals("uuid", result.get("data_type", String.class).toLowerCase()); + }); + } + + @Test + public void testUsage() + { + Map idsByTitle = new HashMap<>(); + + doInJPA(em -> { + List articles = List.of( + new ArticleTestingEntity("one"), + new ArticleTestingEntity("two"), + new ArticleTestingEntity("three") + ); + + articles.forEach(em::persist); + articles.forEach(article -> idsByTitle.put(article.getTitle(), article.getId())); + em.flush(); + }); + + var idOfTwo = Objects.requireNonNull(idsByTitle.get("two"), "id must not be null"); + + doInJPA(em -> { + var article = em.find(ArticleTestingEntity.class, idOfTwo); + Assertions.assertEquals("two", article.getTitle()); + }); + + doInJPA(em -> { + var article = em.createQuery("SELECT a FROM ArticleTestingEntity a WHERE a.id = :id", ArticleTestingEntity.class) + .setParameter("id", idOfTwo) + .getSingleResult(); + Assertions.assertEquals("two", article.getTitle()); + }); + } + +}