HHH-18512 @EnumeratedValue and PostgreSQL named enum types

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-08-23 00:31:28 +02:00
parent 5dcbdf64f1
commit 64a98417e2
12 changed files with 238 additions and 39 deletions

View File

@ -7,18 +7,14 @@
package org.hibernate.boot.model.process.internal;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.hibernate.boot.spi.BootstrapContext;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.EnumJavaType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.checkerframework.checker.nullness.qual.Nullable;

View File

@ -12,7 +12,7 @@ import org.hibernate.engine.jdbc.Size;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.converter.internal.EnumHelper;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.BasicBinder;
import org.hibernate.type.descriptor.jdbc.BasicExtractor;
@ -30,6 +30,7 @@ import java.util.Arrays;
import static java.util.Collections.emptySet;
import static org.hibernate.type.SqlTypes.NAMED_ENUM;
import static org.hibernate.type.descriptor.converter.internal.EnumHelper.getEnumeratedValues;
/**
* Represents a named {@code enum} type on Oracle 23ai+.
@ -92,13 +93,13 @@ public class OracleEnumJdbcType implements JdbcType {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
st.setString( index, ((Enum<?>) value).name() );
st.setString( index, getJavaType().unwrap( value, String.class, options ) );
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
st.setString( name, ((Enum<?>) value).name() );
st.setString( name, getJavaType().unwrap( value, String.class, options ) );
}
};
}
@ -126,10 +127,11 @@ public class OracleEnumJdbcType implements JdbcType {
@Override
public void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
BasicValueConverter<?, ?> valueConverter,
Size columnSize,
Database database,
JdbcTypeIndicators context) {
addAuxiliaryDatabaseObjects( javaType, database, true );
addAuxiliaryDatabaseObjects( javaType, valueConverter, database, true );
}
@Override
@ -138,20 +140,26 @@ public class OracleEnumJdbcType implements JdbcType {
Size columnSize,
Database database,
TypeConfiguration typeConfiguration) {
addAuxiliaryDatabaseObjects( javaType, database, true );
addAuxiliaryDatabaseObjects( javaType, null, database, true );
}
private void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
BasicValueConverter<?, ?> valueConverter,
Database database,
boolean sortEnumValues) {
final Dialect dialect = database.getDialect();
@SuppressWarnings("unchecked")
final Class<? extends Enum<?>> enumClass = (Class<? extends Enum<?>>) javaType.getJavaType();
final String enumTypeName = enumClass.getSimpleName();
final String[] enumeratedValues = EnumHelper.getEnumeratedValues( enumClass );
@SuppressWarnings("unchecked")
final String[] enumeratedValues =
valueConverter == null
? getEnumeratedValues( enumClass )
: getEnumeratedValues( enumClass, (BasicValueConverter<Enum<?>,?>) valueConverter ) ;
if ( sortEnumValues ) {
Arrays.sort( enumeratedValues );
}
final Dialect dialect = database.getDialect();
final String[] create = getCreateEnumTypeCommand(
javaType.getJavaTypeClass().getSimpleName(),
enumeratedValues

View File

@ -14,6 +14,7 @@ import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.converter.internal.EnumHelper;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.BasicBinder;
import org.hibernate.type.descriptor.jdbc.BasicExtractor;
@ -116,6 +117,7 @@ public class OracleOrdinalEnumJdbcType extends OracleEnumJdbcType {
@Override
public void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
BasicValueConverter<?, ?> valueConverter,
Size columnSize,
Database database,
JdbcTypeIndicators context) {

View File

@ -12,7 +12,7 @@ import org.hibernate.engine.jdbc.Size;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.converter.internal.EnumHelper;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.BasicBinder;
import org.hibernate.type.descriptor.jdbc.BasicExtractor;
@ -28,11 +28,10 @@ import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;
import jakarta.persistence.EnumType;
import static java.util.Collections.emptySet;
import static org.hibernate.type.SqlTypes.NAMED_ENUM;
import static org.hibernate.type.SqlTypes.OTHER;
import static org.hibernate.type.descriptor.converter.internal.EnumHelper.getEnumeratedValues;
/**
* Represents a named {@code enum} type on PostgreSQL.
@ -66,8 +65,9 @@ public class PostgreSQLEnumJdbcType implements JdbcType {
@Override
public <T> JdbcLiteralFormatter<T> getJdbcLiteralFormatter(JavaType<T> javaType) {
return (appender, value, dialect, wrapperOptions) -> appender.appendSql( "'" + ((Enum<?>) value).name() + "'::"
+ dialect.getEnumTypeDeclaration( (Class<? extends Enum<?>>) javaType.getJavaType() ) );
return (appender, value, dialect, wrapperOptions)
-> appender.appendSql( "'" + ((Enum<?>) value).name() + "'::"
+ dialect.getEnumTypeDeclaration( (Class<? extends Enum<?>>) javaType.getJavaType() ) );
}
@Override
@ -96,13 +96,13 @@ public class PostgreSQLEnumJdbcType implements JdbcType {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
st.setObject( index, ((Enum<?>) value).name(), Types.OTHER );
st.setObject( index, getJavaType().unwrap( value, String.class, options ), Types.OTHER );
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
st.setObject( name, ((Enum<?>) value).name(), Types.OTHER );
st.setObject( name, getJavaType().unwrap( value, String.class, options ), Types.OTHER );
}
};
}
@ -130,10 +130,11 @@ public class PostgreSQLEnumJdbcType implements JdbcType {
@Override
public void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
BasicValueConverter<?, ?> valueConverter,
Size columnSize,
Database database,
JdbcTypeIndicators context) {
addAuxiliaryDatabaseObjects( javaType, database, true );
addAuxiliaryDatabaseObjects( javaType, valueConverter, database, true );
}
@Override
@ -142,20 +143,26 @@ public class PostgreSQLEnumJdbcType implements JdbcType {
Size columnSize,
Database database,
TypeConfiguration typeConfiguration) {
addAuxiliaryDatabaseObjects( javaType, database, true );
addAuxiliaryDatabaseObjects( javaType, null, database, true );
}
protected void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
BasicValueConverter<?, ?> valueConverter,
Database database,
boolean sortEnumValues) {
final Dialect dialect = database.getDialect();
@SuppressWarnings("unchecked")
final Class<? extends Enum<?>> enumClass = (Class<? extends Enum<?>>) javaType.getJavaType();
final String enumTypeName = enumClass.getSimpleName();
final String[] enumeratedValues = EnumHelper.getEnumeratedValues( enumClass );
@SuppressWarnings("unchecked")
final String[] enumeratedValues =
valueConverter == null
? getEnumeratedValues( enumClass )
: getEnumeratedValues( enumClass, (BasicValueConverter<Enum<?>,?>) valueConverter ) ;
if ( sortEnumValues ) {
Arrays.sort( enumeratedValues );
}
final Dialect dialect = database.getDialect();
final String[] create = dialect.getCreateEnumTypeCommand(
javaType.getJavaTypeClass().getSimpleName(),
enumeratedValues

View File

@ -8,6 +8,7 @@ package org.hibernate.dialect;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.engine.jdbc.Size;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.spi.TypeConfiguration;
@ -42,10 +43,10 @@ public class PostgreSQLOrdinalEnumJdbcType extends PostgreSQLEnumJdbcType {
@Override
public void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
Size columnSize,
BasicValueConverter<?, ?> valueConverter, Size columnSize,
Database database,
JdbcTypeIndicators context) {
addAuxiliaryDatabaseObjects( javaType, database, false );
addAuxiliaryDatabaseObjects( javaType, valueConverter, database, false );
}
@Override
@ -54,6 +55,6 @@ public class PostgreSQLOrdinalEnumJdbcType extends PostgreSQLEnumJdbcType {
Size columnSize,
Database database,
TypeConfiguration typeConfiguration) {
addAuxiliaryDatabaseObjects( javaType, database, false );
addAuxiliaryDatabaseObjects( javaType, null, database, false );
}
}

View File

@ -365,6 +365,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
resolution.getJdbcType().addAuxiliaryDatabaseObjects(
resolution.getRelationalJavaType(),
resolution.getValueConverter(),
size,
getBuildingContext().getMetadataCollector().getDatabase(),
this

View File

@ -8,9 +8,11 @@ package org.hibernate.type.descriptor.converter.internal;
import java.util.Arrays;
import org.hibernate.HibernateException;
import org.hibernate.type.BasicType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.Type;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.jdbc.JdbcType;
/**
@ -39,14 +41,28 @@ public class EnumHelper {
}
public static String[] getEnumeratedValues(Class<? extends Enum<?>> enumClass) {
Enum<?>[] values = enumClass.getEnumConstants();
String[] names = new String[values.length];
final Enum<?>[] values = enumClass.getEnumConstants();
final String[] names = new String[values.length];
for ( int i = 0; i < values.length; i++ ) {
names[i] = values[i].name();
}
return names;
}
public static String[] getEnumeratedValues(
Class<? extends Enum<?>> enumClass, BasicValueConverter<Enum<?>,?> converter) {
final Enum<?>[] values = enumClass.getEnumConstants();
final String[] names = new String[values.length];
for ( int i = 0; i < values.length; i++ ) {
final Object relationalValue = converter.toRelationalValue( values[i] );
if ( relationalValue == null ) {
throw new HibernateException( "Enum value converter returned null for enum class '" + enumClass.getName() + "'" );
}
names[i] = relationalValue.toString();
}
return names;
}
public static String[] getSortedEnumeratedValues(Class<? extends Enum<?>> enumClass) {
final String[] names = getEnumeratedValues( enumClass );
Arrays.sort( names );

View File

@ -7,9 +7,7 @@
package org.hibernate.type.descriptor.converter.spi;
import org.hibernate.Incubating;
import org.hibernate.dialect.Dialect;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.checkerframework.checker.nullness.qual.Nullable;

View File

@ -248,25 +248,20 @@ public class EnumJavaType<T extends Enum<T>> extends AbstractClassJavaType<T> {
* Convert a value of the enum type to its name value
*/
public String toName(T domainForm) {
if ( domainForm == null ) {
return null;
}
return domainForm.name();
return domainForm == null ? null : domainForm.name();
}
/**
* Interpret a string value as the named value of the enum type
*/
public T fromName(String relationalForm) {
if ( relationalForm == null ) {
return null;
}
return Enum.valueOf( getJavaTypeClass(), relationalForm.trim() );
return relationalForm == null ? null : Enum.valueOf( getJavaTypeClass(), relationalForm.trim() );
}
@Override
public String getCheckCondition(String columnName, JdbcType jdbcType, BasicValueConverter<?, ?> converter, Dialect dialect) {
if ( converter != null ) {
if ( converter != null
&& jdbcType.getDefaultSqlTypeCode() != NAMED_ENUM ) {
return renderConvertedEnumCheckConstraint( columnName, jdbcType, converter, dialect );
}
else if ( jdbcType.isInteger() ) {

View File

@ -373,7 +373,7 @@ public interface JdbcType extends Serializable {
}
/**
* @deprecated Use {@link #addAuxiliaryDatabaseObjects(JavaType, Size, Database, JdbcTypeIndicators)} instead
* @deprecated Use {@link #addAuxiliaryDatabaseObjects(JavaType, BasicValueConverter, Size, Database, JdbcTypeIndicators)} instead
*/
@Incubating
@Deprecated(forRemoval = true)
@ -392,6 +392,7 @@ public interface JdbcType extends Serializable {
@Incubating
default void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
BasicValueConverter<?, ?> valueConverter,
Size columnSize,
Database database,
JdbcTypeIndicators context) {

View File

@ -77,6 +77,7 @@ public class ArrayAggregateTest {
"StringArray"
).addAuxiliaryDatabaseObjects(
new ArrayJavaType<>( javaTypeRegistry.getDescriptor( String.class ) ),
null,
Size.nil(),
metadata.getDatabase(),
typeConfiguration.getCurrentBaseSqlTypeIndicators()

View File

@ -0,0 +1,173 @@
/*
* 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.mapping.enumeratedvalue;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.EnumeratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.dialect.PostgreSQLDialect;
import org.hibernate.dialect.SybaseDialect;
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.SkipForDialect;
import org.hibernate.type.SqlTypes;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* The spec says that for {@linkplain EnumType#STRING}, only {@linkplain String}
* is supported. But {@code char} / {@linkplain Character} make a lot of sense to support as well
*
* @author Steve Ebersole
*/
@SuppressWarnings("JUnitMalformedDeclaration")
@RequiresDialect(PostgreSQLDialect.class)
public class PostgresqlNamedEnumEnumerateValueTests {
@Test
@DomainModel(annotatedClasses = Person.class)
@SessionFactory
void testBasicUsage(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.persist( new Person( 1, "John", Gender.MALE ) );
} );
scope.inTransaction( (session) -> {
assertEquals( Gender.MALE, session.find( Person.class, 1 ).gender );
} );
scope.inTransaction( (session) -> {
session.doWork( (connection) -> {
try (Statement statement = connection.createStatement()) {
try (ResultSet resultSet = statement.executeQuery( "select gender from persons" )) {
assertThat( resultSet.next() ).isTrue();
final String storedGender = resultSet.getString( 1 );
assertThat( storedGender ).isEqualTo( "M" );
}
}
} );
} );
}
@DomainModel(annotatedClasses = Person.class)
@SessionFactory
@Test
void testNulls(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.persist( new Person( 1, "John", null ) );
} );
scope.inTransaction( (session) -> {
session.doWork( (connection) -> {
try (Statement statement = connection.createStatement()) {
try (ResultSet resultSet = statement.executeQuery( "select gender from persons" )) {
assertThat( resultSet.next() ).isTrue();
final String storedGender = resultSet.getString( 1 );
assertThat( resultSet.wasNull() ).isTrue();
assertThat( storedGender ).isNull();
}
}
} );
} );
}
@DomainModel(annotatedClasses = Person.class)
@SessionFactory
@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsColumnCheck.class )
@Test
void verifyCheckConstraints(SessionFactoryScope scope) {
scope.inTransaction( (session) -> session.doWork( (connection) -> {
try (PreparedStatement statement = connection.prepareStatement( "insert into persons (id, gender) values (?, ?)" ) ) {
statement.setInt( 1, 100 );
statement.setString( 2, "X" );
statement.executeUpdate();
fail( "Expecting a failure" );
}
catch (SQLException expected) {
}
} ) );
}
@DomainModel(annotatedClasses = Person.class)
@SessionFactory
@SkipForDialect( dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Sybase (at least jTDS driver) truncates the value so the constraint is not violated" )
@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsColumnCheck.class )
@Test
void verifyCheckConstraints2(SessionFactoryScope scope) {
scope.inTransaction( (session) -> session.doWork( (connection) -> {
try (PreparedStatement statement = connection.prepareStatement( "insert into persons (id, gender) values (?, ?)" ) ) {
statement.setInt( 1, 200 );
// this would work without check constraints or with check constraints based solely on EnumType#STRING
statement.setString( 2, "MALE" );
statement.executeUpdate();
fail( "Expecting a failure" );
}
catch (SQLException expected) {
}
} ) );
}
@AfterEach
void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> session.createMutationQuery( "delete Person" ).executeUpdate() );
}
public enum Gender {
MALE( 'M' ),
FEMALE( 'F' ),
OTHER( 'U' );
@EnumeratedValue
private final char code;
Gender(char code) {
this.code = code;
}
public char getCode() {
return code;
}
}
@SuppressWarnings({ "FieldCanBeLocal", "unused" })
@Entity(name="Person")
@Table(name="persons")
public static class Person {
@Id
private Integer id;
private String name;
@Enumerated(EnumType.STRING)
@JdbcTypeCode(SqlTypes.NAMED_ENUM)
@Column(length = 1)
private Gender gender;
public Person() {
}
public Person(Integer id, String name, Gender gender) {
this.id = id;
this.name = name;
this.gender = gender;
}
}
}