HHH-17909 Use domain type for NAMED_ENUM

This commit is contained in:
LLEFEVRE 2024-05-22 17:19:31 +02:00 committed by Christian Beikov
parent af269ae182
commit dc82a3c5e3
4 changed files with 536 additions and 10 deletions

View File

@ -95,6 +95,8 @@ import org.hibernate.type.descriptor.jdbc.SqlTypedJdbcType;
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
import org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl;
import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl;
import org.hibernate.type.descriptor.sql.internal.NamedNativeEnumDdlTypeImpl;
import org.hibernate.type.descriptor.sql.internal.NamedNativeOrdinalEnumDdlTypeImpl;
import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
import org.hibernate.type.spi.TypeConfiguration;
@ -167,7 +169,7 @@ public class OracleDialect extends Dialect {
public static final String PREFER_LONG_RAW = "hibernate.dialect.oracle.prefer_long_raw";
private static final String yqmSelect =
"(trunc(%2$s, 'MONTH') + numtoyminterval(%1$s, 'MONTH') + (least(extract(day from %2$s), extract(day from last_day(trunc(%2$s, 'MONTH') + numtoyminterval(%1$s, 'MONTH')))) - 1))";
"(trunc(%2$s, 'MONTH') + numtoyminterval(%1$s, 'MONTH') + (least(extract(day from %2$s), extract(day from last_day(trunc(%2$s, 'MONTH') + numtoyminterval(%1$s, 'MONTH')))) - 1))";
private static final String ADD_YEAR_EXPRESSION = String.format( yqmSelect, "?2*12", "?3" );
private static final String ADD_QUARTER_EXPRESSION = String.format( yqmSelect, "?2*3", "?3" );
@ -716,10 +718,10 @@ public class OracleDialect extends Dialect {
switch ( sqlTypeCode ) {
case BOOLEAN:
if ( getVersion().isSameOrAfter( 23 ) ) {
return super.columnType( sqlTypeCode );
return super.columnType( sqlTypeCode );
}
else {
return "number(1,0)";
return "number(1,0)";
}
case TINYINT:
return "number(3,0)";
@ -746,8 +748,8 @@ public class OracleDialect extends Dialect {
return "date";
case TIME:
return "timestamp($p)";
// the only difference between date and timestamp
// on Oracle is that date has no fractional seconds
// the only difference between date and timestamp
// on Oracle is that date has no fractional seconds
case TIME_WITH_TIMEZONE:
return "timestamp($p) with time zone";
@ -781,6 +783,11 @@ public class OracleDialect extends Dialect {
ddlTypeRegistry.addDescriptor( new ArrayDdlTypeImpl( this, false ) );
ddlTypeRegistry.addDescriptor( TABLE, new ArrayDdlTypeImpl( this, false ) );
if(getVersion().isSameOrAfter(23)) {
ddlTypeRegistry.addDescriptor(new NamedNativeEnumDdlTypeImpl(this));
ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) );
}
}
@Override
@ -973,8 +980,14 @@ public class OracleDialect extends Dialect {
typeContributions.getTypeConfiguration()
.getJavaTypeRegistry()
.getDescriptor( Object.class )
)
)
);
if(getVersion().isSameOrAfter(23)) {
final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry();
jdbcTypeRegistry.addDescriptor(OracleEnumJdbcType.INSTANCE);
jdbcTypeRegistry.addDescriptor(OracleOrdinalEnumJdbcType.INSTANCE);
}
}
@Override
@ -1256,10 +1269,10 @@ public class OracleDialect extends Dialect {
}
return DISTINCT_KEYWORD_PATTERN.matcher( sql ).find()
|| GROUP_BY_KEYWORD_PATTERN.matcher( sql ).find()
|| UNION_KEYWORD_PATTERN.matcher( sql ).find()
|| ORDER_BY_KEYWORD_PATTERN.matcher( sql ).find() && queryOptions.hasLimit()
|| queryOptions.hasLimit() && queryOptions.getLimit().getFirstRow() != null;
|| GROUP_BY_KEYWORD_PATTERN.matcher( sql ).find()
|| UNION_KEYWORD_PATTERN.matcher( sql ).find()
|| ORDER_BY_KEYWORD_PATTERN.matcher( sql ).find() && queryOptions.hasLimit()
|| queryOptions.hasLimit() && queryOptions.getLimit().getFirstRow() != null;
}
@Override
@ -1638,4 +1651,29 @@ public class OracleDialect extends Dialect {
public int getDriverMinorVersion() {
return driverMinorVersion;
}
@Override
public String getEnumTypeDeclaration(String name, String[] values) {
return getVersion().isSameOrAfter(23) ? name : super.getEnumTypeDeclaration(name, values);
}
@Override
public String[] getCreateEnumTypeCommand(String name, String[] values) {
final StringBuilder domain = new StringBuilder();
domain.append( "create domain " )
.append( name )
.append( " as enum (" );
String separator = "";
for ( String value : values ) {
domain.append( separator ).append( value );
separator = ", ";
}
domain.append( ')' );
return new String[] { domain.toString() };
}
@Override
public String[] getDropEnumTypeCommand(String name) {
return new String[] { "drop domain if exists " + name + " force" };
}
}

View File

@ -0,0 +1,194 @@
/*
* 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.dialect;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.boot.model.relational.NamedAuxiliaryDatabaseObject;
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.java.JavaType;
import org.hibernate.type.descriptor.jdbc.BasicBinder;
import org.hibernate.type.descriptor.jdbc.BasicExtractor;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.spi.TypeConfiguration;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;
import static java.util.Collections.emptySet;
import static org.hibernate.type.SqlTypes.NAMED_ENUM;
/**
* Represents a named {@code enum} type on Oracle 23ai+.
* <p>
* Hibernate does <em>not</em> automatically use this for enums
* mapped as {@link jakarta.persistence.EnumType#STRING}, and
* instead this type must be explicitly requested using:
* <pre>
* &#64;JdbcTypeCode(SqlTypes.NAMED_ENUM)
* </pre>
*
* @see org.hibernate.type.SqlTypes#NAMED_ENUM
* @see OracleDialect#getEnumTypeDeclaration(String, String[])
* @see OracleDialect#getCreateEnumTypeCommand(String, String[])
*
* @author Loïc Lefèvre
*/
public class OracleEnumJdbcType implements JdbcType {
public static final OracleEnumJdbcType INSTANCE = new OracleEnumJdbcType();
@Override
public int getJdbcTypeCode() {
return Types.VARCHAR;
}
@Override
public int getDefaultSqlTypeCode() {
return NAMED_ENUM;
}
@Override
public <T> JdbcLiteralFormatter<T> getJdbcLiteralFormatter(JavaType<T> javaType) {
return (appender, value, dialect, wrapperOptions) -> appender.appendSql( dialect.getEnumTypeDeclaration( (Class<? extends Enum<?>>) javaType.getJavaType() )+"." + ((Enum<?>) value).name() );
}
@Override
public String getFriendlyName() {
return "ENUM";
}
@Override
public String toString() {
return "EnumTypeDescriptor";
}
@Override
public <X> ValueBinder<X> getBinder(JavaType<X> javaType) {
return new BasicBinder<>( javaType, this ) {
@Override
protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException {
st.setNull( index, getJdbcTypeCode() );
}
@Override
protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException {
st.setNull( name, getJdbcTypeCode() );
}
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
st.setString( index, ((Enum<?>) value).name() );
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
st.setString( name, ((Enum<?>) value).name() );
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(JavaType<X> javaType) {
return new BasicExtractor<>( javaType, this ) {
@Override
protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException {
return getJavaType().wrap( rs.getString( paramIndex ), options );
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return getJavaType().wrap( statement.getString( index ), options );
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException {
return getJavaType().wrap( statement.getString( name ), options );
}
};
}
@Override
public void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
Size columnSize,
Database database,
JdbcTypeIndicators context) {
addAuxiliaryDatabaseObjects( javaType, database, true );
}
@Override
public void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
Size columnSize,
Database database,
TypeConfiguration typeConfiguration) {
addAuxiliaryDatabaseObjects( javaType, database, true );
}
private void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
Database database,
boolean sortEnumValues) {
final Dialect dialect = database.getDialect();
final Class<? extends Enum<?>> enumClass = (Class<? extends Enum<?>>) javaType.getJavaType();
final String enumTypeName = enumClass.getSimpleName();
final String[] enumeratedValues = EnumHelper.getEnumeratedValues( enumClass );
if ( sortEnumValues ) {
Arrays.sort( enumeratedValues );
}
final String[] create = getCreateEnumTypeCommand(
javaType.getJavaTypeClass().getSimpleName(),
enumeratedValues
);
final String[] drop = dialect.getDropEnumTypeCommand( enumClass );
if ( create != null && create.length > 0 ) {
database.addAuxiliaryDatabaseObject(
new NamedAuxiliaryDatabaseObject(
enumTypeName,
database.getDefaultNamespace(),
create,
drop,
emptySet(),
true
)
);
}
}
/**
* Used to generate the CREATE DDL command for Data Use Case Domain based on VARCHAR2 values.
*
* @param name
* @param values
* @return the DDL command to create that enum
*/
public String[] getCreateEnumTypeCommand(String name, String[] values) {
final StringBuilder domain = new StringBuilder();
domain.append( "create domain " )
.append( name )
.append( " as enum (" );
String separator = "";
for ( String value : values ) {
domain.append( separator ).append( value ).append("='").append(value).append("'");
separator = ", ";
}
domain.append( ')' );
return new String[] { domain.toString() };
}
}

View File

@ -0,0 +1,163 @@
/*
* 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.dialect;
import jakarta.persistence.EnumType;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.boot.model.relational.NamedAuxiliaryDatabaseObject;
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.java.JavaType;
import org.hibernate.type.descriptor.jdbc.BasicBinder;
import org.hibernate.type.descriptor.jdbc.BasicExtractor;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.spi.TypeConfiguration;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;
import static java.util.Collections.emptySet;
import static org.hibernate.type.SqlTypes.NAMED_ORDINAL_ENUM;
/**
* Represents a named {@code enum} type on Oracle 23ai+.
* <p>
* Hibernate does <em>not</em> automatically use this for enums
* mapped as {@link EnumType#ORDINAL}, and
* instead this type must be explicitly requested using:
* <pre>
* &#64;JdbcTypeCode(SqlTypes.NAMED_ORDINAL_ENUM)
* </pre>
*
* @see org.hibernate.type.SqlTypes#NAMED_ORDINAL_ENUM
* @see OracleDialect#getEnumTypeDeclaration(String, String[])
* @see OracleDialect#getCreateEnumTypeCommand(String, String[])
*
* @author Loïc Lefèvre
*/
public class OracleOrdinalEnumJdbcType extends OracleEnumJdbcType {
public static final OracleOrdinalEnumJdbcType INSTANCE = new OracleOrdinalEnumJdbcType();
@Override
public int getJdbcTypeCode() {
return Types.INTEGER;
}
@Override
public int getDefaultSqlTypeCode() {
return NAMED_ORDINAL_ENUM;
}
@Override
public <X> ValueBinder<X> getBinder(JavaType<X> javaType) {
return new BasicBinder<>( javaType, this ) {
@Override
protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException {
st.setNull( index, getJdbcTypeCode() );
}
@Override
protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException {
st.setNull( name, getJdbcTypeCode() );
}
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
st.setInt( index, ((Enum<?>) value).ordinal()+1 );
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
st.setInt( name, ((Enum<?>) value).ordinal()+1 );
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(JavaType<X> javaType) {
return new BasicExtractor<>( javaType, this ) {
@Override
protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException {
final int value = rs.getInt( paramIndex );
if(rs.wasNull()) {
return getJavaType().wrap(null, options);
}
else {
return getJavaType().wrap(value - 1, options);
}
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return getJavaType().wrap( statement.getInt( index ), options );
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException {
return getJavaType().wrap( statement.getInt( name ), options );
}
};
}
@Override
public void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
Size columnSize,
Database database,
JdbcTypeIndicators context) {
addAuxiliaryDatabaseObjects( javaType, database, false );
}
@Override
public void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
Size columnSize,
Database database,
TypeConfiguration typeConfiguration) {
addAuxiliaryDatabaseObjects( javaType, database, false );
}
private void addAuxiliaryDatabaseObjects(
JavaType<?> javaType,
Database database,
boolean sortEnumValues) {
final Dialect dialect = database.getDialect();
final Class<? extends Enum<?>> enumClass = (Class<? extends Enum<?>>) javaType.getJavaType();
final String enumTypeName = enumClass.getSimpleName();
final String[] enumeratedValues = EnumHelper.getEnumeratedValues( enumClass );
if ( sortEnumValues ) {
Arrays.sort( enumeratedValues );
}
final String[] create = dialect.getCreateEnumTypeCommand(
javaType.getJavaTypeClass().getSimpleName(),
enumeratedValues
);
final String[] drop = dialect.getDropEnumTypeCommand( enumClass );
if ( create != null && create.length > 0 ) {
database.addAuxiliaryDatabaseObject(
new NamedAuxiliaryDatabaseObject(
enumTypeName,
database.getDefaultNamespace(),
create,
drop,
emptySet(),
true
)
);
}
}
}

View File

@ -0,0 +1,131 @@
/*
* 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.type;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.type.SqlTypes;
import org.junit.jupiter.api.Test;
import java.sql.ResultSet;
import java.sql.Statement;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@SessionFactory
@DomainModel(annotatedClasses = {OracleEnumTest.Timeslot.class, OracleEnumTest.Activity.class, OracleEnumTest.Weather.class, OracleEnumTest.Sky.class})
@RequiresDialect(value = OracleDialect.class, majorVersion = 23)
public class OracleEnumTest {
@Test public void testNamedEnum(SessionFactoryScope scope) {
Timeslot timeslot = new Timeslot();
Activity activity = new Activity();
activity.activityType = ActivityType.Play;
timeslot.activity = activity;
scope.inTransaction( s -> s.persist( timeslot ) );
Timeslot ts = scope.fromTransaction( s-> s.createQuery("from Timeslot where activity.activityType = Play", Timeslot.class ).getSingleResult() );
assertEquals( ts.activity.activityType, ActivityType.Play );
}
@Test public void testOrdinalEnum(SessionFactoryScope scope) {
Weather weather = new Weather();
Sky sky = new Sky();
sky.skyType = SkyType.Sunny;
weather.sky = sky;
scope.inTransaction( s -> s.persist( weather ) );
Weather w = scope.fromTransaction( s-> s.createQuery("from Weather where sky.skyType = Sunny", Weather.class ).getSingleResult() );
assertEquals( w.sky.skyType, SkyType.Sunny );
}
@Test public void testSchema(SessionFactoryScope scope) {
scope.inSession( s -> {
s.doWork(
c -> {
try(Statement stmt = c.createStatement()) {
try(ResultSet typeInfo = stmt.executeQuery("select name, decode(instr(data_display,'WHEN '''),0,'NUMBER','VARCHAR2') from user_domains where type='ENUMERATED'")) {
while (typeInfo.next()) {
String name = typeInfo.getString(1);
String baseType = typeInfo.getString(2);
if (name.equalsIgnoreCase("ActivityType") && baseType.equals("VARCHAR2")) {
return;
}
}
}
}
fail("named enum type not exported");
}
);
});
scope.inSession( s -> {
s.doWork(
c -> {
ResultSet tableInfo = c.getMetaData().getColumns(null, null, "ACTIVITY", "ACTIVITYTYPE" );
while ( tableInfo.next() ) {
String type = tableInfo.getString(6);
assertEquals( "VARCHAR2", type );
return;
}
fail("named enum column not exported");
}
);
});
}
public enum ActivityType {Work, Play, Sleep }
@Entity(name = "Activity")
public static class Activity {
@Id
@JdbcTypeCode(SqlTypes.NAMED_ENUM)
ActivityType activityType;
}
@Entity(name = "Timeslot")
public static class Timeslot {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@ManyToOne(cascade = CascadeType.PERSIST)
Activity activity;
}
public enum SkyType {Sunny, Cloudy}
@Entity(name = "Sky")
public static class Sky {
@Id
@JdbcTypeCode(SqlTypes.NAMED_ORDINAL_ENUM)
SkyType skyType;
}
@Entity(name = "Weather")
public static class Weather {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@ManyToOne(cascade = CascadeType.PERSIST)
Sky sky;
}
}