HHH-16153 - Support JPA 3.2 `@EnumeratedValue`
This commit is contained in:
parent
bf6a66d9ce
commit
6383f9d8e2
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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.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;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class EnumeratedValueConverter<E extends Enum<E>,R> implements BasicValueConverter<E,R> {
|
||||
private final EnumJavaType<E> enumJavaType;
|
||||
private final JavaType<R> relationalJavaType;
|
||||
|
||||
private final Map<R,E> relationalToEnumMap;
|
||||
private final Map<E,R> enumToRelationalMap;
|
||||
|
||||
public EnumeratedValueConverter(
|
||||
EnumJavaType<E> enumJavaType,
|
||||
JavaType<R> relationalJavaType,
|
||||
Field valueField) {
|
||||
this.enumJavaType = enumJavaType;
|
||||
this.relationalJavaType = relationalJavaType;
|
||||
|
||||
ReflectHelper.ensureAccessibility( valueField );
|
||||
|
||||
final Class<E> enumJavaTypeClass = enumJavaType.getJavaTypeClass();
|
||||
final E[] enumConstants = enumJavaTypeClass.getEnumConstants();
|
||||
relationalToEnumMap = CollectionHelper.mapOfSize( enumConstants.length );
|
||||
enumToRelationalMap = CollectionHelper.mapOfSize( enumConstants.length );
|
||||
for ( int i = 0; i < enumConstants.length; i++ ) {
|
||||
final E enumConstant = enumConstants[i];
|
||||
try {
|
||||
//noinspection unchecked
|
||||
final R relationalValue = (R) valueField.get( enumConstant );
|
||||
|
||||
relationalToEnumMap.put( relationalValue, enumConstant );
|
||||
enumToRelationalMap.put( enumConstant, relationalValue );
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Set<R> getRelationalValueSet() {
|
||||
return relationalToEnumMap.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable E toDomainValue(@Nullable R relationalForm) {
|
||||
if ( relationalForm == null ) {
|
||||
return null;
|
||||
}
|
||||
return relationalToEnumMap.get( relationalForm );
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable R toRelationalValue(@Nullable E domainForm) {
|
||||
if ( domainForm == null ) {
|
||||
return null;
|
||||
}
|
||||
return enumToRelationalMap.get( domainForm );
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType<E> getDomainJavaType() {
|
||||
return enumJavaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType<R> getRelationalJavaType() {
|
||||
return relationalJavaType;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,9 @@
|
|||
package org.hibernate.boot.model.process.internal;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
|
@ -24,6 +26,7 @@ import org.hibernate.type.AdjustableBasicType;
|
|||
import org.hibernate.type.BasicPluralType;
|
||||
import org.hibernate.type.BasicType;
|
||||
import org.hibernate.type.SerializableType;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
import org.hibernate.type.descriptor.java.BasicJavaType;
|
||||
import org.hibernate.type.descriptor.java.BasicPluralJavaType;
|
||||
import org.hibernate.type.descriptor.java.EnumJavaType;
|
||||
|
@ -32,14 +35,19 @@ import org.hibernate.type.descriptor.java.JavaType;
|
|||
import org.hibernate.type.descriptor.java.MutabilityPlan;
|
||||
import org.hibernate.type.descriptor.java.SerializableJavaType;
|
||||
import org.hibernate.type.descriptor.java.TemporalJavaType;
|
||||
import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcType;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
|
||||
import org.hibernate.type.descriptor.jdbc.ObjectJdbcType;
|
||||
import org.hibernate.type.internal.BasicTypeImpl;
|
||||
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
|
||||
import org.hibernate.type.internal.ConvertedBasicTypeImpl;
|
||||
import org.hibernate.type.spi.TypeConfiguration;
|
||||
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.EnumeratedValue;
|
||||
import jakarta.persistence.TemporalType;
|
||||
|
||||
import static org.hibernate.type.SqlTypes.SMALLINT;
|
||||
import static org.hibernate.type.descriptor.java.JavaTypeHelper.isTemporal;
|
||||
|
||||
/**
|
||||
|
@ -292,14 +300,50 @@ public class InferredBasicValueResolver {
|
|||
JdbcType explicitJdbcType,
|
||||
JdbcTypeIndicators stdIndicators,
|
||||
BootstrapContext bootstrapContext) {
|
||||
final JdbcType jdbcType = explicitJdbcType == null
|
||||
? enumJavaType.getRecommendedJdbcType( stdIndicators )
|
||||
: explicitJdbcType;
|
||||
final BasicType<E> basicType = bootstrapContext.getTypeConfiguration().getBasicTypeRegistry().resolve(
|
||||
enumJavaType,
|
||||
jdbcType
|
||||
);
|
||||
final Class<E> enumJavaTypeClass = enumJavaType.getJavaTypeClass();
|
||||
final Field enumeratedValueField = determineEnumeratedValueField( enumJavaTypeClass );
|
||||
if ( enumeratedValueField != null ) {
|
||||
validateEnumeratedValue( enumeratedValueField, stdIndicators );
|
||||
}
|
||||
|
||||
final JdbcType jdbcType;
|
||||
if ( explicitJdbcType != null ) {
|
||||
jdbcType = explicitJdbcType;
|
||||
}
|
||||
else if ( enumeratedValueField != null ) {
|
||||
final JdbcTypeRegistry jdbcTypeRegistry = bootstrapContext.getTypeConfiguration().getJdbcTypeRegistry();
|
||||
final Class<?> fieldType = enumeratedValueField.getType();
|
||||
if ( String.class.equals( fieldType ) ) {
|
||||
jdbcType = jdbcTypeRegistry.getDescriptor( SqlTypes.VARCHAR );
|
||||
}
|
||||
else if ( byte.class.equals( fieldType ) ) {
|
||||
jdbcType = jdbcTypeRegistry.getDescriptor( SqlTypes.TINYINT );
|
||||
}
|
||||
else if ( short.class.equals( fieldType )
|
||||
|| int.class.equals( fieldType ) ) {
|
||||
jdbcType = jdbcTypeRegistry.getDescriptor( SMALLINT );
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
jdbcType = enumJavaType.getRecommendedJdbcType( stdIndicators );
|
||||
}
|
||||
|
||||
final BasicType<E> basicType;
|
||||
if ( enumeratedValueField != null ) {
|
||||
basicType = createEnumeratedValueJdbcMapping( enumeratedValueField, enumJavaType, jdbcType, bootstrapContext );
|
||||
}
|
||||
else {
|
||||
basicType = bootstrapContext.getTypeConfiguration().getBasicTypeRegistry().resolve(
|
||||
enumJavaType,
|
||||
jdbcType
|
||||
);
|
||||
}
|
||||
|
||||
bootstrapContext.registerAdHocBasicType( basicType );
|
||||
|
||||
return new InferredBasicValueResolution<>(
|
||||
basicType,
|
||||
enumJavaType,
|
||||
|
@ -310,6 +354,65 @@ public class InferredBasicValueResolver {
|
|||
);
|
||||
}
|
||||
|
||||
private static <E extends Enum<E>> BasicType<E> createEnumeratedValueJdbcMapping(
|
||||
Field enumeratedValueField,
|
||||
EnumJavaType<E> enumJavaType,
|
||||
JdbcType jdbcType,
|
||||
BootstrapContext bootstrapContext) {
|
||||
final JavaTypeRegistry javaTypeRegistry = bootstrapContext.getTypeConfiguration().getJavaTypeRegistry();
|
||||
final Class<?> fieldType = enumeratedValueField.getType();
|
||||
final JavaType<?> relationalJavaType = javaTypeRegistry.getDescriptor( fieldType );
|
||||
return new ConvertedBasicTypeImpl<>(
|
||||
ConvertedBasicTypeImpl.EXTERNALIZED_PREFIX + enumJavaType.getTypeName(),
|
||||
"EnumeratedValue conversion for " + enumJavaType.getTypeName(),
|
||||
jdbcType,
|
||||
new EnumeratedValueConverter<>( enumJavaType, relationalJavaType, enumeratedValueField )
|
||||
);
|
||||
}
|
||||
|
||||
private static <E extends Enum<E>> Field determineEnumeratedValueField(Class<E> enumJavaTypeClass) {
|
||||
for ( Field field : enumJavaTypeClass.getDeclaredFields() ) {
|
||||
if ( field.isAnnotationPresent( EnumeratedValue.class ) ) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void validateEnumeratedValue(Field enumeratedValueField, JdbcTypeIndicators stdIndicators) {
|
||||
final Class<?> fieldType = enumeratedValueField.getType();
|
||||
if ( stdIndicators.getEnumeratedType() == EnumType.STRING ) {
|
||||
// JPA says only String is valid here
|
||||
// todo (7.0) : support char/Character as well
|
||||
if ( !String.class.equals( fieldType ) ) {
|
||||
throw new MappingException(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"@EnumeratedValue for EnumType.STRING must be placed on a field whose type is String: %s.%s",
|
||||
enumeratedValueField.getDeclaringClass().getName(),
|
||||
enumeratedValueField.getName()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
assert stdIndicators.getEnumeratedType() == null || stdIndicators.getEnumeratedType() == EnumType.ORDINAL;
|
||||
// JPA says only byte, short, or int are valid here
|
||||
if ( !byte.class.equals( fieldType )
|
||||
&& !short.class.equals( fieldType )
|
||||
&& !int.class.equals( fieldType ) ) {
|
||||
throw new MappingException(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"@EnumeratedValue for EnumType.ORDINAL must be placed on a field whose type is byte, short, or int: %s.%s",
|
||||
enumeratedValueField.getDeclaringClass().getName(),
|
||||
enumeratedValueField.getName()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> BasicValue.Resolution<T> fromTemporal(
|
||||
TemporalJavaType<T> reflectedJtd,
|
||||
BasicJavaType<?> explicitJavaType,
|
||||
|
|
|
@ -238,8 +238,10 @@ import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE;
|
|||
import static org.hibernate.type.SqlTypes.TINYINT;
|
||||
import static org.hibernate.type.SqlTypes.VARBINARY;
|
||||
import static org.hibernate.type.SqlTypes.VARCHAR;
|
||||
import static org.hibernate.type.SqlTypes.isCharacterType;
|
||||
import static org.hibernate.type.SqlTypes.isEnumType;
|
||||
import static org.hibernate.type.SqlTypes.isFloatOrRealOrDouble;
|
||||
import static org.hibernate.type.SqlTypes.isIntegral;
|
||||
import static org.hibernate.type.SqlTypes.isNumericOrDecimal;
|
||||
import static org.hibernate.type.SqlTypes.isVarbinaryType;
|
||||
import static org.hibernate.type.SqlTypes.isVarcharType;
|
||||
|
@ -885,6 +887,39 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun
|
|||
return check.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a check condition for column with the given set of values.
|
||||
*
|
||||
* @apiNote Only supports TINYINT, SMALLINT and (VAR)CHAR
|
||||
*/
|
||||
public String getCheckCondition(String columnName, Set<?> valueSet, JdbcType jdbcType) {
|
||||
final boolean isCharacterJdbcType = isCharacterType( jdbcType.getJdbcTypeCode() );
|
||||
assert isCharacterJdbcType || isIntegral( jdbcType.getJdbcTypeCode() );
|
||||
|
||||
StringBuilder check = new StringBuilder();
|
||||
check.append( columnName ).append( " in (" );
|
||||
String separator = "";
|
||||
boolean nullIsValid = false;
|
||||
for ( Object value : valueSet ) {
|
||||
if ( value == null ) {
|
||||
nullIsValid = true;
|
||||
continue;
|
||||
}
|
||||
if ( isCharacterJdbcType ) {
|
||||
check.append( separator ).append('\'').append( value ).append('\'');
|
||||
}
|
||||
else {
|
||||
check.append( separator ).append( value );
|
||||
}
|
||||
separator = ",";
|
||||
}
|
||||
check.append( ')' );
|
||||
if ( nullIsValid ) {
|
||||
check.append( " or " ).append( columnName ).append( " is null" );
|
||||
}
|
||||
return check.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contributeFunctions(FunctionContributions functionContributions) {
|
||||
initializeFunctionRegistry( functionContributions );
|
||||
|
|
|
@ -355,8 +355,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
|
|||
|
||||
final Selectable selectable = getColumn();
|
||||
final Size size;
|
||||
if ( selectable instanceof Column ) {
|
||||
Column column = (Column) selectable;
|
||||
if ( selectable instanceof Column column ) {
|
||||
resolveColumn( column, getDialect() );
|
||||
size = column.calculateColumnSize( getDialect(), getBuildingContext().getMetadataCollector() );
|
||||
}
|
||||
|
@ -364,13 +363,12 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
|
|||
size = Size.nil();
|
||||
}
|
||||
|
||||
resolution.getJdbcType()
|
||||
.addAuxiliaryDatabaseObjects(
|
||||
resolution.getRelationalJavaType(),
|
||||
size,
|
||||
getBuildingContext().getMetadataCollector().getDatabase(),
|
||||
this
|
||||
);
|
||||
resolution.getJdbcType().addAuxiliaryDatabaseObjects(
|
||||
resolution.getRelationalJavaType(),
|
||||
size,
|
||||
getBuildingContext().getMetadataCollector().getDatabase(),
|
||||
this
|
||||
);
|
||||
|
||||
return resolution;
|
||||
}
|
||||
|
|
|
@ -6,16 +6,21 @@
|
|||
*/
|
||||
package org.hibernate.type.descriptor.java;
|
||||
|
||||
import jakarta.persistence.EnumType;
|
||||
import java.util.Set;
|
||||
|
||||
import org.hibernate.AssertionFailure;
|
||||
import org.hibernate.boot.model.process.internal.EnumeratedValueConverter;
|
||||
import org.hibernate.dialect.Dialect;
|
||||
import org.hibernate.internal.util.collections.CollectionHelper;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
import org.hibernate.type.descriptor.WrapperOptions;
|
||||
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcType;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
|
||||
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
|
||||
|
||||
import jakarta.persistence.EnumType;
|
||||
|
||||
import static jakarta.persistence.EnumType.ORDINAL;
|
||||
import static org.hibernate.type.SqlTypes.CHAR;
|
||||
import static org.hibernate.type.SqlTypes.ENUM;
|
||||
|
@ -262,8 +267,7 @@ public class EnumJavaType<T extends Enum<T>> extends AbstractClassJavaType<T> {
|
|||
@Override
|
||||
public String getCheckCondition(String columnName, JdbcType jdbcType, BasicValueConverter<?, ?> converter, Dialect dialect) {
|
||||
if ( converter != null ) {
|
||||
//TODO: actually convert the enum values to create the check constraint
|
||||
return null;
|
||||
return renderConvertedEnumCheckConstraint( columnName, jdbcType, converter, dialect );
|
||||
}
|
||||
else if ( jdbcType.isInteger() ) {
|
||||
int max = getJavaTypeClass().getEnumConstants().length - 1;
|
||||
|
@ -276,4 +280,36 @@ public class EnumJavaType<T extends Enum<T>> extends AbstractClassJavaType<T> {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private String renderConvertedEnumCheckConstraint(
|
||||
String columnName,
|
||||
JdbcType jdbcType,
|
||||
BasicValueConverter<?, ?> converter,
|
||||
Dialect dialect) {
|
||||
final Set valueSet;
|
||||
// for `@EnumeratedValue` we already have the possible values...
|
||||
if ( converter instanceof EnumeratedValueConverter enumeratedValueConverter ) {
|
||||
valueSet = enumeratedValueConverter.getRelationalValueSet();
|
||||
}
|
||||
else {
|
||||
if ( !SqlTypes.isIntegral( jdbcType.getJdbcTypeCode() )
|
||||
&& !SqlTypes.isCharacterType( jdbcType.getJdbcTypeCode() ) ) {
|
||||
// we only support adding check constraints for generalized conversions to
|
||||
// INTEGER, SMALLINT, TINYINT, (N)CHAR, (N)VARCHAR, LONG(N)VARCHAR
|
||||
return null;
|
||||
}
|
||||
|
||||
final Class<T> javaTypeClass = getJavaTypeClass();
|
||||
final T[] enumConstants = javaTypeClass.getEnumConstants();
|
||||
valueSet = CollectionHelper.setOfSize( enumConstants.length );
|
||||
for ( T enumConstant : enumConstants ) {
|
||||
//noinspection unchecked
|
||||
final Object relationalValue = ( (BasicValueConverter) converter ).toRelationalValue( enumConstant );
|
||||
valueSet.add( relationalValue );
|
||||
}
|
||||
}
|
||||
|
||||
return dialect.getCheckCondition( columnName, valueSet, jdbcType );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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.converted.enums;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.orm.test.mapping.enumeratedvalue.EnumeratedValueTests;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@SuppressWarnings("JUnitMalformedDeclaration")
|
||||
public class ConvertedEnumCheckConstraintsTests {
|
||||
@Test
|
||||
@DomainModel(annotatedClasses = Person.class)
|
||||
@SessionFactory
|
||||
void testBasicUsage(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
session.persist( new Person( 1, "John", Gender.MALE ) );
|
||||
} );
|
||||
|
||||
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(useCollectingStatementInspector = true)
|
||||
@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 = EnumeratedValueTests.Person.class)
|
||||
@SessionFactory(useCollectingStatementInspector = true)
|
||||
@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 );
|
||||
// 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' );
|
||||
private final char code;
|
||||
|
||||
Gender(char code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public char getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GenderConverter implements AttributeConverter<Gender,Character> {
|
||||
@Override
|
||||
public Character convertToDatabaseColumn(Gender attribute) {
|
||||
if ( attribute == null ) {
|
||||
return null;
|
||||
}
|
||||
return attribute.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Gender convertToEntityAttribute(Character dbData) {
|
||||
if ( dbData == null ) {
|
||||
return null;
|
||||
}
|
||||
switch ( dbData ) {
|
||||
case 'M' -> {
|
||||
return Gender.MALE;
|
||||
}
|
||||
case 'F' -> {
|
||||
return Gender.FEMALE;
|
||||
}
|
||||
case 'U' -> {
|
||||
return Gender.OTHER;
|
||||
}
|
||||
default -> throw new IllegalArgumentException( "Bad data: " + dbData );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "FieldCanBeLocal", "unused" })
|
||||
@Entity(name="Person")
|
||||
@Table(name="persons")
|
||||
public static class Person {
|
||||
@Id
|
||||
private Integer id;
|
||||
private String name;
|
||||
@JdbcTypeCode(SqlTypes.CHAR)
|
||||
@Column(length = 1)
|
||||
@Convert( converter = GenderConverter.class )
|
||||
private Gender gender;
|
||||
|
||||
public Person() {
|
||||
}
|
||||
|
||||
public Person(Integer id, String name, Gender gender) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.gender = gender;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 org.hibernate.MappingException;
|
||||
import org.hibernate.boot.MetadataSources;
|
||||
import org.hibernate.boot.registry.StandardServiceRegistry;
|
||||
|
||||
import org.hibernate.testing.orm.junit.ServiceRegistry;
|
||||
import org.hibernate.testing.orm.junit.ServiceRegistryScope;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@SuppressWarnings("JUnitMalformedDeclaration")
|
||||
public class BadEnumeratedValueTests {
|
||||
|
||||
@ServiceRegistry
|
||||
@Test
|
||||
void testMismatchedTypes(ServiceRegistryScope scope) {
|
||||
final StandardServiceRegistry serviceRegistry = scope.getRegistry();
|
||||
final MetadataSources metadataSources = new MetadataSources( serviceRegistry )
|
||||
.addAnnotatedClass( Person2.class );
|
||||
|
||||
try {
|
||||
metadataSources.buildMetadata();
|
||||
fail( "Expecting an exception" );
|
||||
}
|
||||
catch (MappingException expected) {
|
||||
assertThat( expected.getMessage() ).startsWith( "@EnumeratedValue" );
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(name="Person2")
|
||||
@Table(name="persons2")
|
||||
@SuppressWarnings({ "FieldCanBeLocal", "unused" })
|
||||
public static class Person2 {
|
||||
@Id
|
||||
private Integer id;
|
||||
private String name;
|
||||
@Enumerated
|
||||
private EnumeratedValueTests.Gender gender;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private EnumeratedValueTests.Status status;
|
||||
|
||||
public Person2() {
|
||||
}
|
||||
|
||||
public Person2(Integer id, String name, EnumeratedValueTests.Gender gender, EnumeratedValueTests.Status status) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.gender = gender;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import org.hibernate.testing.jdbc.SQLStatementInspector;
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Test for {@linkplain EnumeratedValue} introduced in JPA 3.2
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@SuppressWarnings("JUnitMalformedDeclaration")
|
||||
public class EnumeratedValueTests {
|
||||
|
||||
@DomainModel(annotatedClasses = Person.class)
|
||||
@SessionFactory(useCollectingStatementInspector = true)
|
||||
@Test
|
||||
void testBasicUsage(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
session.persist( new Person( 1, "John", Gender.MALE, Status.ACTIVE ) );
|
||||
} );
|
||||
|
||||
scope.inTransaction( (session) -> {
|
||||
session.doWork( (connection) -> {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
try (ResultSet resultSet = statement.executeQuery( "select gender, status from persons" )) {
|
||||
assertThat( resultSet.next() ).isTrue();
|
||||
final String storedGender = resultSet.getString( 1 );
|
||||
assertThat( storedGender ).isEqualTo( "M" );
|
||||
final int storedStatus = resultSet.getInt( 2 );
|
||||
assertThat( storedStatus ).isEqualTo( 200 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
@DomainModel(annotatedClasses = Person.class)
|
||||
@SessionFactory(useCollectingStatementInspector = true)
|
||||
@Test
|
||||
void testNulls(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
session.persist( new Person( 1, "John", null, null ) );
|
||||
} );
|
||||
|
||||
scope.inTransaction( (session) -> {
|
||||
session.doWork( (connection) -> {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
try (ResultSet resultSet = statement.executeQuery( "select gender, status from persons" )) {
|
||||
assertThat( resultSet.next() ).isTrue();
|
||||
final String storedGender = resultSet.getString( 1 );
|
||||
assertThat( resultSet.wasNull() ).isTrue();
|
||||
assertThat( storedGender ).isNull();
|
||||
final int storedStatus = resultSet.getInt( 2 );
|
||||
assertThat( resultSet.wasNull() ).isTrue();
|
||||
}
|
||||
}
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
@DomainModel(annotatedClasses = Person.class)
|
||||
@SessionFactory(useCollectingStatementInspector = true)
|
||||
@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 );
|
||||
// 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) {
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement( "insert into persons (id, status) values (?, ?)" ) ) {
|
||||
statement.setInt( 1, 101 );
|
||||
// this would work without check constraints or with check constraints based solely on EnumType#ORDINAL
|
||||
statement.setInt( 2, 1 );
|
||||
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 String code;
|
||||
|
||||
Gender(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
PENDING( 100 ),
|
||||
ACTIVE( 200 ),
|
||||
INACTIVE( 300 );
|
||||
|
||||
@EnumeratedValue
|
||||
private final int code;
|
||||
|
||||
Status(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(name="Person")
|
||||
@Table(name="persons")
|
||||
@SuppressWarnings({ "FieldCanBeLocal", "unused" })
|
||||
public static class Person {
|
||||
@Id
|
||||
private Integer id;
|
||||
private String name;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Gender gender;
|
||||
@Enumerated
|
||||
private Status status;
|
||||
|
||||
public Person() {
|
||||
}
|
||||
|
||||
public Person(Integer id, String name, Gender gender, Status status) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.gender = gender;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -106,6 +106,12 @@ class Book {
|
|||
----
|
||||
|
||||
|
||||
[[enum-checks]]
|
||||
== Enums and Check Constraints
|
||||
|
||||
Hibernate previously added support for generating check constraints for enums mapped using `@Enumerated`
|
||||
as part of schema generation. 7.0 adds the same capability for enums mapped using an `AttributeConverter`,
|
||||
by asking the converter to convert all the enum constants on start up.
|
||||
|
||||
|
||||
[[java-beans]]
|
||||
|
|
Loading…
Reference in New Issue