HHH-15898 Allow explicit configuration of the UDT column order

This commit is contained in:
Christian Beikov 2022-12-21 18:59:46 +01:00
parent 7250449aaa
commit 4e03f320fb
7 changed files with 111 additions and 33 deletions

View File

@ -47,4 +47,12 @@ public @interface Struct {
* The name of the UDT (user defined type). * The name of the UDT (user defined type).
*/ */
String name(); String name();
/**
* The ordered set of attributes of the UDT, as they appear physically in the DDL.
* It is important to specify the attributes in the same order for JDBC interactions to work correctly.
* If the annotated type is a record, the order of record components is used as the default order.
* If no default order can be inferred, attributes are assumed to be in alphabetical order.
*/
String[] attributes() default {};
} }

View File

@ -55,6 +55,7 @@ public final class AggregateComponentBinder {
() -> new EmbeddableAggregateJavaType<>( component.getComponentClass(), structName ) () -> new EmbeddableAggregateJavaType<>( component.getComponentClass(), structName )
); );
component.setStructName( structName ); component.setStructName( structName );
component.setStructColumnNames( determineStructAttributeNames( inferredData, returnedClassOrElement ) );
// Determine the aggregate column // Determine the aggregate column
BasicValueBinder basicValueBinder = new BasicValueBinder( BasicValueBinder.Kind.ATTRIBUTE, component, context ); BasicValueBinder basicValueBinder = new BasicValueBinder( BasicValueBinder.Kind.ATTRIBUTE, component, context );
@ -135,6 +136,21 @@ public final class AggregateComponentBinder {
return null; return null;
} }
private static String[] determineStructAttributeNames(PropertyData inferredData, XClass returnedClassOrElement) {
final XProperty property = inferredData.getProperty();
if ( property != null ) {
final Struct struct = property.getAnnotation( Struct.class );
if ( struct != null ) {
return struct.attributes();
}
}
final Struct struct = returnedClassOrElement.getAnnotation( Struct.class );
if ( struct != null ) {
return struct.attributes();
}
return null;
}
private static boolean isAggregate(XProperty property, XClass returnedClass) { private static boolean isAggregate(XProperty property, XClass returnedClass) {
if ( property != null ) { if ( property != null ) {
final Struct struct = property.getAnnotation( Struct.class ); final Struct struct = property.getAnnotation( Struct.class );

View File

@ -31,6 +31,7 @@ import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component; import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property; import org.hibernate.mapping.Property;
import org.hibernate.mapping.Selectable;
import org.hibernate.mapping.UserDefinedType; import org.hibernate.mapping.UserDefinedType;
import org.hibernate.mapping.Value; import org.hibernate.mapping.Value;
import org.hibernate.metamodel.internal.EmbeddableHelper; import org.hibernate.metamodel.internal.EmbeddableHelper;
@ -189,6 +190,8 @@ public class AggregateComponentSecondPass implements SecondPass {
private void orderColumns(UserDefinedType userDefinedType) { private void orderColumns(UserDefinedType userDefinedType) {
final Class<?> componentClass = component.getComponentClass(); final Class<?> componentClass = component.getComponentClass();
final int[] originalOrder = component.sortProperties(); final int[] originalOrder = component.sortProperties();
final String[] structColumnNames = component.getStructColumnNames();
if ( structColumnNames == null || structColumnNames.length == 0 ) {
final int[] propertyMappingIndex; final int[] propertyMappingIndex;
if ( ReflectHelper.isRecord( componentClass ) ) { if ( ReflectHelper.isRecord( componentClass ) ) {
if ( originalOrder == null ) { if ( originalOrder == null ) {
@ -196,35 +199,49 @@ public class AggregateComponentSecondPass implements SecondPass {
} }
else { else {
final String[] componentNames = ReflectHelper.getRecordComponentNames( componentClass ); final String[] componentNames = ReflectHelper.getRecordComponentNames( componentClass );
propertyMappingIndex = EmbeddableHelper.determinePropertyMappingIndex( propertyMappingIndex = EmbeddableHelper.determineMappingIndex(
component.getPropertyNames(), component.getPropertyNames(),
componentNames componentNames
); );
} }
} }
else { else if ( component.getInstantiatorPropertyNames() != null ) {
if ( component.getInstantiatorPropertyNames() == null ) { propertyMappingIndex = EmbeddableHelper.determineMappingIndex(
return;
}
propertyMappingIndex = EmbeddableHelper.determinePropertyMappingIndex(
component.getPropertyNames(), component.getPropertyNames(),
component.getInstantiatorPropertyNames() component.getInstantiatorPropertyNames()
); );
} }
else {
propertyMappingIndex = null;
}
if ( propertyMappingIndex == null ) {
// If there is default ordering possible, assume alphabetical ordering
final ArrayList<Column> orderedColumns = new ArrayList<>( userDefinedType.getColumnSpan() ); final ArrayList<Column> orderedColumns = new ArrayList<>( userDefinedType.getColumnSpan() );
final List<Property> properties = component.getProperties(); final List<Property> properties = component.getProperties();
if ( propertyMappingIndex == null ) {
for ( Property property : properties ) { for ( Property property : properties ) {
addColumns( orderedColumns, property.getValue() ); addColumns( orderedColumns, property.getValue() );
} }
userDefinedType.reorderColumns( orderedColumns );
} }
else { else {
final ArrayList<Column> orderedColumns = new ArrayList<>( userDefinedType.getColumnSpan() );
final List<Property> properties = component.getProperties();
for ( final int propertyIndex : propertyMappingIndex ) { for ( final int propertyIndex : propertyMappingIndex ) {
addColumns( orderedColumns, properties.get( propertyIndex ).getValue() ); addColumns( orderedColumns, properties.get( propertyIndex ).getValue() );
} }
userDefinedType.reorderColumns( orderedColumns );
}
}
else {
final ArrayList<Column> orderedColumns = new ArrayList<>( userDefinedType.getColumnSpan() );
for ( String structColumnName : structColumnNames ) {
if ( !addColumns( orderedColumns, component, structColumnName ) ) {
throw new MappingException( "Couldn't find column [" + structColumnName + "] that was defined in @Struct(attributes) in the component [" + component.getComponentClassName() + "]" );
}
} }
userDefinedType.reorderColumns( orderedColumns ); userDefinedType.reorderColumns( orderedColumns );
} }
}
private static void addColumns(ArrayList<Column> orderedColumns, Value value) { private static void addColumns(ArrayList<Column> orderedColumns, Value value) {
if ( value instanceof Component ) { if ( value instanceof Component ) {
@ -243,6 +260,33 @@ public class AggregateComponentSecondPass implements SecondPass {
} }
} }
private static boolean addColumns(ArrayList<Column> orderedColumns, Component component, String structColumnName) {
for ( Property property : component.getProperties() ) {
final Value value = property.getValue();
if ( value instanceof Component ) {
final Component subComponent = (Component) value;
if ( subComponent.getAggregateColumn() == null ) {
if ( addColumns( orderedColumns, subComponent, structColumnName ) ) {
return true;
}
}
else if ( structColumnName.equals( subComponent.getAggregateColumn().getName() ) ) {
orderedColumns.add( subComponent.getAggregateColumn() );
return true;
}
}
else {
for ( Selectable selectable : value.getSelectables() ) {
if ( selectable instanceof Column && structColumnName.equals( ( (Column) selectable ).getName() ) ) {
orderedColumns.add( (Column) selectable );
return true;
}
}
}
}
return false;
}
private void validateSupportedColumnTypes(String basePath, Component component) { private void validateSupportedColumnTypes(String basePath, Component component) {
for ( Property property : component.getProperties() ) { for ( Property property : component.getProperties() ) {
final Value value = property.getValue(); final Value value = property.getValue();

View File

@ -178,7 +178,7 @@ public class PostgreSQLStructJdbcType extends PostgreSQLPGObjectJdbcType impleme
} }
assert end == string.length(); assert end == string.length();
if ( returnEmbeddable ) { if ( returnEmbeddable ) {
final Object[] attributeValues = getAttributeValues( embeddableMappingType, array, options ); final Object[] attributeValues = getAttributeValues( embeddableMappingType, orderMapping, array, options );
//noinspection unchecked //noinspection unchecked
return (X) embeddableMappingType.getRepresentationStrategy().getInstantiator().instantiate( return (X) embeddableMappingType.getRepresentationStrategy().getInstantiator().instantiate(
() -> attributeValues, () -> attributeValues,
@ -420,8 +420,9 @@ public class PostgreSQLStructJdbcType extends PostgreSQLPGObjectJdbcType impleme
options options
); );
if ( returnEmbeddable ) { if ( returnEmbeddable ) {
final Object[] attributeValues = getAttributeValues( final Object[] attributeValues = structJdbcType.getAttributeValues(
structJdbcType.embeddableMappingType, structJdbcType.embeddableMappingType,
structJdbcType.orderMapping,
subValues, subValues,
options options
); );
@ -827,6 +828,7 @@ public class PostgreSQLStructJdbcType extends PostgreSQLPGObjectJdbcType impleme
private Object[] getAttributeValues( private Object[] getAttributeValues(
EmbeddableMappingType embeddableMappingType, EmbeddableMappingType embeddableMappingType,
int[] orderMapping,
Object[] rawJdbcValues, Object[] rawJdbcValues,
WrapperOptions options) throws SQLException { WrapperOptions options) throws SQLException {
final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings();
@ -879,7 +881,7 @@ public class PostgreSQLStructJdbcType extends PostgreSQLPGObjectJdbcType impleme
jdbcValueCount = embeddableMappingType.getJdbcValueCount(); jdbcValueCount = embeddableMappingType.getJdbcValueCount();
final Object[] subJdbcValues = new Object[jdbcValueCount]; final Object[] subJdbcValues = new Object[jdbcValueCount];
System.arraycopy( rawJdbcValues, jdbcIndex, subJdbcValues, 0, subJdbcValues.length ); System.arraycopy( rawJdbcValues, jdbcIndex, subJdbcValues, 0, subJdbcValues.length );
final Object[] subValues = getAttributeValues( embeddableMappingType, subJdbcValues, options ); final Object[] subValues = getAttributeValues( embeddableMappingType, null, subJdbcValues, options );
attributeValues[attributeIndex] = embeddableMappingType.getRepresentationStrategy() attributeValues[attributeIndex] = embeddableMappingType.getRepresentationStrategy()
.getInstantiator() .getInstantiator()
.instantiate( .instantiate(

View File

@ -82,6 +82,7 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable
private AggregateColumn aggregateColumn; private AggregateColumn aggregateColumn;
private AggregateColumn parentAggregateColumn; private AggregateColumn parentAggregateColumn;
private String structName; private String structName;
private String[] structColumnNames;
// lazily computed based on 'properties' field: invalidate by setting to null when properties are modified // lazily computed based on 'properties' field: invalidate by setting to null when properties are modified
private transient List<Selectable> cachedSelectables; private transient List<Selectable> cachedSelectables;
// lazily computed based on 'properties' field: invalidate by setting to null when properties are modified // lazily computed based on 'properties' field: invalidate by setting to null when properties are modified
@ -807,4 +808,11 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable
this.instantiatorPropertyNames = instantiatorPropertyNames; this.instantiatorPropertyNames = instantiatorPropertyNames;
} }
public String[] getStructColumnNames() {
return structColumnNames;
}
public void setStructColumnNames(String[] structColumnNames) {
this.structColumnNames = structColumnNames;
}
} }

View File

@ -9,11 +9,11 @@ package org.hibernate.metamodel.internal;
import java.util.Arrays; import java.util.Arrays;
public class EmbeddableHelper { public class EmbeddableHelper {
public static int[] determinePropertyMappingIndex(String[] propertyNames, String[] componentNames) { public static int[] determineMappingIndex(String[] sortedNames, String[] names) {
final int[] index = new int[propertyNames.length]; final int[] index = new int[sortedNames.length];
int i = 0; int i = 0;
for ( String componentName : componentNames ) { for ( String name : names ) {
final int mappingIndex = Arrays.binarySearch( propertyNames, componentName ); final int mappingIndex = Arrays.binarySearch( sortedNames, name );
if ( mappingIndex != -1 ) { if ( mappingIndex != -1 ) {
index[i++] = mappingIndex; index[i++] = mappingIndex;
} }

View File

@ -238,7 +238,6 @@ public class StructComponentInstantiatorTest extends BaseSessionFactoryFunctiona
@Id @Id
private Long id; private Long id;
@Struct(name = "my_point_type")
private Point thePoint; private Point thePoint;
public RecordStructHolder() { public RecordStructHolder() {
@ -268,6 +267,7 @@ public class StructComponentInstantiatorTest extends BaseSessionFactoryFunctiona
} }
@Embeddable @Embeddable
@Struct(name = "my_point_type", attributes = { "y", "x", "z" })
public static class Point { public static class Point {
private final String y; private final String y;