diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ClassPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ClassPropertyHolder.java index 2ec9461d3656..c82f11364864 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ClassPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ClassPropertyHolder.java @@ -13,6 +13,7 @@ import org.hibernate.PropertyNotFoundException; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.SecondPass; +import org.hibernate.mapping.BasicValue; import org.hibernate.mapping.Collection; import org.hibernate.mapping.Component; import org.hibernate.mapping.IndexedCollection; @@ -29,6 +30,8 @@ import jakarta.persistence.Convert; import jakarta.persistence.JoinTable; +import org.hibernate.models.spi.TypeDetails; +import org.hibernate.type.internal.ParameterizedTypeImpl; import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.internal.util.collections.CollectionHelper.mapOfSize; @@ -419,6 +422,34 @@ else if ( value instanceof SimpleValue simpleValue ) { } } + static void setType(Value value, TypeDetails type) { + final var typeName = type.getName(); + if ( value instanceof ToOne toOne ) { + toOne.setReferencedEntityName( typeName ); + toOne.setTypeName( typeName ); + } + else if ( value instanceof Component component ) { + // Avoid setting type name for generic components + if ( !component.isGeneric() ) { + component.setComponentClassName( typeName ); + } + if ( component.getTypeName() != null ) { + component.setTypeName( typeName ); + } + } + else if ( value instanceof SimpleValue simpleValue ) { + if ( value instanceof BasicValue basicValue ) { + basicValue.setImplicitJavaTypeAccess( typeConfiguration -> + type.getTypeKind() == TypeDetails.Kind.PARAMETERIZED_TYPE + ? ParameterizedTypeImpl.from( type.asParameterizedType() ) + : type.determineRawClass().toJavaClass() ); + } + else { + simpleValue.setTypeName( typeName ); + } + } + } + private void addPropertyToJoin(Property property, MemberDetails memberDetails, ClassDetails declaringClass, Join join) { if ( declaringClass != null ) { final var inheritanceState = inheritanceStatePerClass.get( declaringClass ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index e553f89e1c2b..59e443f0c557 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -48,6 +48,7 @@ import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.PropertyData; +import org.hibernate.boot.spi.SecondPass; import org.hibernate.cfg.AvailableSettings; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.internal.util.StringHelper; @@ -59,6 +60,7 @@ import org.hibernate.mapping.Join; import org.hibernate.mapping.JoinedSubclass; import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.SingleTableSubclass; @@ -71,6 +73,7 @@ import org.hibernate.models.internal.ClassTypeDetailsImpl; import org.hibernate.models.spi.AnnotationTarget; import org.hibernate.models.spi.ClassDetails; +import org.hibernate.models.spi.MemberDetails; import org.hibernate.models.spi.ModelsContext; import org.hibernate.models.spi.TypeDetails; import org.hibernate.spi.NavigablePath; @@ -97,6 +100,7 @@ import static org.hibernate.boot.model.internal.BinderHelper.hasToOneAnnotation; import static org.hibernate.boot.model.internal.BinderHelper.toAliasEntityMap; import static org.hibernate.boot.model.internal.BinderHelper.toAliasTableMap; +import static org.hibernate.boot.model.internal.ClassPropertyHolder.setType; import static org.hibernate.boot.model.internal.DialectOverridesAnnotationHelper.getOverridableAnnotation; import static org.hibernate.boot.model.internal.DialectOverridesAnnotationHelper.getOverrideAnnotation; import static org.hibernate.boot.model.internal.EmbeddableBinder.fillEmbeddable; @@ -122,6 +126,7 @@ import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty; import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty; import static org.hibernate.jpa.event.internal.CallbackDefinitionResolver.resolveLifecycleCallbacks; +import static org.hibernate.models.spi.TypeDetailsHelper.resolveRelativeType; import static org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies.EMBEDDED; @@ -1088,6 +1093,7 @@ private void processIdPropertiesIfNotAlready( missingIdProperties.remove( propertyName ); } } + addGenericProperties( persistentClass, inheritanceState, inheritanceStates ); if ( !missingIdProperties.isEmpty() ) { throw new AnnotationException( "Entity '" + persistentClass.getEntityName() @@ -1101,6 +1107,77 @@ else if ( !missingEntityProperties.isEmpty() ) { } } + private void addGenericProperties( + PersistentClass persistentClass, + InheritanceState inheritanceState, + Map inheritanceStates) { + if ( persistentClass.isAbstract() == null || !persistentClass.isAbstract() ) { + var superclass = persistentClass.getSuperPersistentClass(); + while ( superclass != null ) { + for ( Property declaredProperty : superclass.getDeclaredProperties() ) { + if ( declaredProperty.isGeneric() ) { + final var memberDetails = getMemberDetails( inheritanceState, inheritanceStates, declaredProperty, superclass ); + final var typeDetails = resolveRelativeType( memberDetails.getType(), inheritanceState.getClassDetails() ); + final var returnedClassName = typeDetails.getName(); + final var actualProperty = declaredProperty.copy(); + actualProperty.setGeneric( false ); + actualProperty.setGenericSpecialization( true ); + actualProperty.setReturnedClassName( returnedClassName ); + final var value = actualProperty.getValue().copy(); + setType( value, typeDetails ); + actualProperty.setValue( value ); + persistentClass.addProperty( actualProperty ); + if ( value instanceof BasicValue basicValue ) { + final InFlightMetadataCollector metadataCollector = context.getMetadataCollector(); + final BasicValue originalBasicValue = (BasicValue) declaredProperty.getValue(); + metadataCollector.addSecondPass( new SecondPass() { + @Override + public void doSecondPass(Map persistentClasses) + throws MappingException { + basicValue.setExplicitTypeParams( originalBasicValue.getExplicitTypeParams() ); + basicValue.setTypeParameters( originalBasicValue.getTypeParameters() ); + basicValue.setJpaAttributeConverterDescriptor( originalBasicValue.getJpaAttributeConverterDescriptor() ); + // Don't copy over the implicit java type access, since we figure that out in ClassPropertyHolder#setType +// basicValue.setImplicitJavaTypeAccess( originalBasicValue.getImplicitJavaTypeAccess() ); + basicValue.setExplicitJavaTypeAccess( originalBasicValue.getExplicitJavaTypeAccess() ); + basicValue.setExplicitJdbcTypeAccess( originalBasicValue.getExplicitJdbcTypeAccess() ); + basicValue.setExplicitMutabilityPlanAccess( originalBasicValue.getExplicitMutabilityPlanAccess() ); + basicValue.setEnumerationStyle( originalBasicValue.getEnumeratedType() ); + basicValue.setTimeZoneStorageType( originalBasicValue.getTimeZoneStorageType() ); + basicValue.setTemporalPrecision( originalBasicValue.getTemporalPrecision() ); + if ( originalBasicValue.isLob() ) { + basicValue.makeLob(); + } + if ( originalBasicValue.isNationalized() ) { + basicValue.makeNationalized(); + } + } + } ); + metadataCollector.registerValueMappingResolver( basicValue::resolve ); + } + } + } + + superclass = superclass.getSuperPersistentClass(); + } + } + } + + private static MemberDetails getMemberDetails(InheritanceState inheritanceState, Map inheritanceStates, Property declaredProperty, PersistentClass superclass) { + var superclassDetails = inheritanceState.getClassDetails().getSuperClass(); + while ( !superclass.getClassName().equals( superclassDetails.getClassName()) ) { + superclassDetails = superclassDetails.getSuperClass(); + } + final var superclassInheritanceState = inheritanceStates.get( superclassDetails ); + final var elementsToProcess = superclassInheritanceState.getElementsToProcess(); + for ( PropertyData element : elementsToProcess.getElements() ) { + if ( declaredProperty.getName().equals( element.getPropertyName() ) ) { + return element.getAttributeMember(); + } + } + throw new IllegalArgumentException("Couldn't find PropertyData for [" + declaredProperty.getName() + "] in class: " + declaredProperty.getPersistentClass().getClassName() ); + } + private static String getMissingPropertiesString(Set propertyNames) { final var missingProperties = new StringBuilder(); for ( String propertyName : propertyNames ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java index 4ed35b871378..0150cca6510a 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java @@ -223,7 +223,7 @@ public Boolean hasIdClassOrEmbeddedId() { * guessing from @Id or @EmbeddedId presence if not specified. * Change EntityBinder by side effect */ - private ElementsToProcess getElementsToProcess() { + ElementsToProcess getElementsToProcess() { if ( elementsToProcess == null ) { final var inheritanceState = inheritanceStatePerClass.get( classDetails ); assert !inheritanceState.isEmbeddableSuperclass(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java index e9c32b3564cc..dc3084f43870 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java @@ -109,6 +109,8 @@ public class PropertyBinder { private String name; private String returnedClassName; + private boolean generic; + private boolean genericSpecialization; private boolean lazy; private String lazyGroup; private AccessType accessType; @@ -166,6 +168,14 @@ private void setReturnedClassName(String returnedClassName) { this.returnedClassName = returnedClassName; } + public void setGeneric(boolean generic) { + this.generic = generic; + } + + public void setGenericSpecialization(boolean genericSpecialization) { + this.genericSpecialization = genericSpecialization; + } + public void setLazy(boolean lazy) { this.lazy = lazy; } @@ -441,6 +451,8 @@ public Property makeProperty() { property.setCascade( cascadeTypes ); property.setPropertyAccessorName( accessType.getType() ); property.setReturnedClassName( returnedClassName ); + property.setGeneric( generic ); + property.setGenericSpecialization( genericSpecialization ); // property.setPropertyAccessStrategy( propertyAccessStrategy ); handleValueGeneration( property ); handleNaturalId( property ); @@ -1002,6 +1014,11 @@ private AnnotatedColumns bindBasicOrComposite( ClassDetails returnedClass) { final var memberDetails = inferredData.getAttributeMember(); + if ( propertyHolder.isEntity() && propertyHolder.getPersistentClass().isAbstract() ) { + // When the type of the member is a type variable, we mark it as generic for abstract classes + setGeneric( inferredData.getClassOrElementType().getTypeKind() == TypeDetails.Kind.TYPE_VARIABLE ); + } + // overrides from @MapsId or @IdClass if needed final PropertyData overridingProperty = overridingProperty( propertyHolder, isIdentifierMapper, memberDetails ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index ff4db8c1906e..2052a3849fe1 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -145,6 +145,7 @@ public BasicValue(BasicValue original) { this.isSoftDelete = original.isSoftDelete; this.softDeleteStrategy = original.softDeleteStrategy; this.aggregateColumn = original.aggregateColumn; + this.jdbcTypeCode = original.jdbcTypeCode; } @Override @@ -215,6 +216,22 @@ public void setImplicitJavaTypeAccess(Function> getExplicitJavaTypeAccess() { + return explicitJavaTypeAccess; + } + + public Function getExplicitJdbcTypeAccess() { + return explicitJdbcTypeAccess; + } + + public Function> getExplicitMutabilityPlanAccess() { + return explicitMutabilityPlanAccess; + } + + public Function getImplicitJavaTypeAccess() { + return implicitJavaTypeAccess; + } + public Selectable getColumn() { return getColumnSpan() == 0 ? null : getColumn( 0 ); } @@ -1030,6 +1047,10 @@ public void setExplicitTypeParams(Map explicitLocalTypeParams) { this.explicitLocalTypeParams = explicitLocalTypeParams; } + public Map getExplicitTypeParams() { + return explicitLocalTypeParams; + } + public void setExplicitTypeName(String typeName) { this.explicitTypeName = typeName; } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java b/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java index bfe537b75570..26ddb606ffc6 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java @@ -106,7 +106,7 @@ public static void checkPropertyColumnDuplication( List properties, String owner) throws MappingException { for ( var property : properties ) { - if ( property.isUpdatable() || property.isInsertable() ) { + if ( ( property.isUpdatable() || property.isInsertable() ) && !property.isGenericSpecialization() ) { property.getValue().checkColumnDuplication( distinctColumns, owner ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java index b53e780c0b5a..a21f1f734f66 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java @@ -362,6 +362,8 @@ public boolean isExplicitPolymorphism() { public abstract List getPropertyClosure(); + public abstract List getAllPropertyClosure(); + public abstract List getTableClosure(); public abstract List getKeyClosure(); @@ -722,6 +724,23 @@ public int getJoinClosureSpan() { } public int getPropertyClosureSpan() { + int span = 0; + for ( Property property : properties ) { + if ( !property.isGeneric() ) { + span += 1; + } + } + for ( var join : joins ) { + for ( Property property : join.getProperties() ) { + if ( !property.isGeneric() ) { + span += 1; + } + } + } + return span; + } + + public int getAllPropertyClosureSpan() { int span = properties.size(); for ( var join : joins ) { span += join.getPropertySpan(); @@ -754,6 +773,23 @@ public int getJoinNumber(Property prop) { * @return A list over the "normal" properties. */ public List getProperties() { + final ArrayList list = new ArrayList<>(); + for ( Property property : properties ) { + if ( !property.isGeneric() ) { + list.add( property ); + } + } + for ( var join : joins ) { + for ( Property property : join.getProperties() ) { + if ( !property.isGeneric() ) { + list.add( property ); + } + } + } + return list; + } + + public List getAllProperties() { final ArrayList> list = new ArrayList<>(); list.add( properties ); for ( var join : joins ) { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java index c5e35b1def13..68b859a4351c 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java @@ -71,6 +71,7 @@ public class Property implements Serializable, MetaAttributable { private PersistentClass persistentClass; private boolean naturalIdentifier; private boolean isGeneric; + private boolean isGenericSpecialization; private boolean lob; private java.util.List callbackDefinitions; private String returnedClassName; @@ -490,6 +491,14 @@ public void setGeneric(boolean generic) { this.isGeneric = generic; } + public boolean isGenericSpecialization() { + return isGenericSpecialization; + } + + public void setGenericSpecialization(boolean genericSpecialization) { + isGenericSpecialization = genericSpecialization; + } + public boolean isLob() { return lob; } @@ -554,6 +563,7 @@ public Property copy() { property.setPersistentClass( getPersistentClass() ); property.setNaturalIdentifier( isNaturalIdentifier() ); property.setGeneric( isGeneric() ); + property.setGenericSpecialization( isGenericSpecialization() ); property.setLob( isLob() ); property.addCallbackDefinitions( getCallbackDefinitions() ); property.setReturnedClassName( getReturnedClassName() ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java index 6503fa477807..e32a2d8f0eed 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java @@ -134,6 +134,11 @@ public List getPropertyClosure() { return getProperties(); } + @Override + public List getAllPropertyClosure() { + return getAllProperties(); + } + @Override public List
getTableClosure() { return List.of( getTable() ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java index 4cabe13f4194..2e3359ee3613 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java @@ -121,7 +121,13 @@ public SimpleValue(MetadataBuildingContext buildingContext, Table table) { protected SimpleValue(SimpleValue original) { this.buildingContext = original.buildingContext; this.metadata = original.metadata; - this.columns.addAll( original.columns ); + for ( Selectable selectable : original.columns ) { + if ( selectable instanceof Column column ) { + final Column newColumn = column.clone(); + newColumn.setValue( this ); + this.columns.add( newColumn ); + } + } this.insertability.addAll( original.insertability ); this.updatability.addAll( original.updatability ); this.partitionKey = original.partitionKey; diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java b/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java index 8a420fb15bbe..d6c322557a24 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java @@ -138,6 +138,11 @@ public List getPropertyClosure() { return new JoinedList<>( getSuperclass().getPropertyClosure(), getProperties() ); } + @Override + public List getAllPropertyClosure() { + return new JoinedList<>( getSuperclass().getAllPropertyClosure(), getAllProperties() ); + } + @Override public List
getTableClosure() { return new JoinedList<>( @@ -238,6 +243,11 @@ public int getPropertyClosureSpan() { return getSuperclass().getPropertyClosureSpan() + super.getPropertyClosureSpan(); } + @Override + public int getAllPropertyClosureSpan() { + return getSuperclass().getAllPropertyClosureSpan() + super.getAllPropertyClosureSpan(); + } + @Override public List getJoinClosure() { return new JoinedList<>( getSuperclass().getJoinClosure(), super.getJoinClosure() ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java index 9277ea7e1098..a072bf5217b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java @@ -699,7 +699,7 @@ private static Member resolveVirtualIdentifierMember( Property property, EntityP private static Member resolveEntityMember(Property property, EntityPersister declaringEntity) { final String propertyName = property.getName(); return !propertyName.equals( declaringEntity.getIdentifierPropertyName() ) - && declaringEntity.findAttributeMapping( propertyName ) == null + && declaringEntity.findAttributeMapping( propertyName ) == null && !property.isGeneric() // just like in #determineIdentifierJavaMember , this *should* indicate we have an IdClass mapping ? resolveVirtualIdentifierMember( property, declaringEntity ) : getter( declaringEntity, property, propertyName, property.getType().getReturnedClass() ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java index 32b3a1216e61..ccd81ab420df 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java @@ -157,7 +157,7 @@ else if ( entityPersister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoad private Map buildPropertyAccessMap(PersistentClass bootDescriptor) { final Map propertyAccessMap = new LinkedHashMap<>(); - for ( var property : bootDescriptor.getPropertyClosure() ) { + for ( var property : bootDescriptor.getAllPropertyClosure() ) { propertyAccessMap.put( property.getName(), makePropertyAccess( property ) ); } return propertyAccessMap; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java index c0c3949b7bf9..acd56efea11c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java @@ -319,6 +319,10 @@ public void wrapUp() { // skip the version property, it was already handled previously. continue; } + if ( property.isGenericSpecialization() ) { + // Skip generic properties since they may only be declared on abstract classes + continue; + } buildAttribute( property, jpaMapping ); } @@ -655,6 +659,16 @@ private void applyGenericProperties(PersistentClass persistentClass, EntityD } mappedSuperclass = getMappedSuperclass( mappedSuperclass ); } + if ( persistentClass.isAbstract() == null || !persistentClass.isAbstract() ) { + for ( var property : persistentClass.getDeclaredProperties() ) { + if ( property.isGenericSpecialization() ) { + final var managedType = (ManagedDomainType) entityType; + final var attributeContainer = (AttributeContainer) managedType; + attributeContainer.getInFlightAccess() + .addConcreteGenericAttribute( buildAttribute( entityType, property ) ); + } + } + } } private MappedSuperclass getMappedSuperclass(PersistentClass persistentClass) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index d64878d30bde..e7d377252862 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -427,6 +427,7 @@ public abstract class AbstractEntityPersister private AttributeMappingsList attributeMappings; protected AttributeMappingsMap declaredAttributeMappings = AttributeMappingsMap.builder().build(); + protected AttributeMappingsMap declaredGenericAttributeMappings = AttributeMappingsMap.builder().build(); protected AttributeMappingsList staticFetchableList; // We build a cache for getters and setters to avoid megamorphic calls private Getter[] getterCache; @@ -4694,28 +4695,68 @@ private void inheritSupertypeSpecialAttributeMappings() { private void buildDeclaredAttributeMappings (MappingModelCreationProcess creationProcess, PersistentClass bootEntityDescriptor) { + final var allPropertyClosure = bootEntityDescriptor.getAllPropertyClosure(); final var properties = getProperties(); final var mappingsBuilder = AttributeMappingsMap.builder(); + final var genericMappingsBuilder = AttributeMappingsMap.builder(); int stateArrayPosition = getStateArrayInitialPosition( creationProcess ); int fetchableIndex = getFetchableIndexOffset(); - for ( int i = 0; i < getPropertySpan(); i++ ) { - final var runtimeAttributeDefinition = properties[i]; - final String attributeName = runtimeAttributeDefinition.getName(); - final var bootProperty = bootEntityDescriptor.getProperty( attributeName ); - if ( superMappingType == null + int i = 0; + for ( var property : allPropertyClosure ) { + if ( !property.isGeneric() ) { + final var runtimeAttributeDefinition = properties[i]; + final String attributeName = runtimeAttributeDefinition.getName(); + final var bootProperty = bootEntityDescriptor.getProperty( attributeName ); + if ( superMappingType == null || superMappingType.findAttributeMapping( bootProperty.getName() ) == null ) { - mappingsBuilder.put( - attributeName, + mappingsBuilder.put( + attributeName, + generateNonIdAttributeMapping( + runtimeAttributeDefinition, + bootProperty, + stateArrayPosition++, + fetchableIndex++, + creationProcess + ) + ); + } + declaredAttributeMappings = mappingsBuilder.build(); + i++; + } + else { + final int span = property.getColumnSpan(); + final String[] colNames = new String[span]; + final var selectables = property.getSelectables(); + final Dialect dialect = getDialect(); + final TypeConfiguration typeConfiguration = creationProcess.getCreationContext().getTypeConfiguration(); + for ( int k = 0; k < selectables.size(); k++ ) { + final var selectable = selectables.get(k); + if ( selectable instanceof Formula formula ) { + formula.setFormula( substituteBrackets( formula.getFormula() ) ); + colNames[k] = selectable.getTemplate( dialect, typeConfiguration ); + } + else if ( selectable instanceof Column column ) { + colNames[k] = column.getQuotedName( dialect ); + } + } + final String tableName = determineTableName( property.getValue().getTable() ); + genericMappingsBuilder.put( + property.getName(), generateNonIdAttributeMapping( - runtimeAttributeDefinition, - bootProperty, - stateArrayPosition++, - fetchableIndex++, + property.getName(), + property.getType(), + property.getCascadeStyle(), + -1, + tableName, + colNames, + property, + -1, + -1, creationProcess ) ); + declaredGenericAttributeMappings = genericMappingsBuilder.build(); } - declaredAttributeMappings = mappingsBuilder.build(); // otherwise, it's defined on the supertype, skip it here } } @@ -5227,15 +5268,37 @@ protected AttributeMapping generateNonIdAttributeMapping( int stateArrayPosition, int fetchableIndex, MappingModelCreationProcess creationProcess) { - final var creationContext = creationProcess.getCreationContext(); - - final String attrName = tupleAttrDefinition.getName(); - final Type attrType = tupleAttrDefinition.getType(); - + final Type type = tupleAttrDefinition.getType(); final int propertyIndex = getPropertyIndex( bootProperty.getName() ); + final String[] attrColumnExpression = type instanceof BasicType && bootProperty.getSelectables().get( 0 ).isFormula() + ? propertyColumnFormulaTemplates[ propertyIndex ] + : getPropertyColumnNames( propertyIndex ) ; + return generateNonIdAttributeMapping( + tupleAttrDefinition.getName(), + type, + tupleAttrDefinition.getCascadeStyle(), + propertyIndex, + getTableName( getPropertyTableNumbers()[propertyIndex] ), + attrColumnExpression, + bootProperty, + stateArrayPosition, + fetchableIndex, + creationProcess + ); + } - final String tableExpression = getTableName( getPropertyTableNumbers()[propertyIndex] ); - final String[] attrColumnNames = getPropertyColumnNames( propertyIndex ); + protected AttributeMapping generateNonIdAttributeMapping( + String attrName, + Type attrType, + CascadeStyle cascadeStyle, + int propertyIndex, + String tableExpression, + String[] attrColumnNames, + Property bootProperty, + int stateArrayPosition, + int fetchableIndex, + MappingModelCreationProcess creationProcess) { + final var creationContext = creationProcess.getCreationContext(); final var propertyAccess = getRepresentationStrategy().resolvePropertyAccess( bootProperty ); @@ -5267,7 +5330,7 @@ protected AttributeMapping generateNonIdAttributeMapping( value.isColumnInsertable( 0 ), value.isColumnUpdateable( 0 ), propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, creationProcess ); } @@ -5305,7 +5368,7 @@ protected AttributeMapping generateNonIdAttributeMapping( else { final var basicBootValue = (BasicValue) value; - if ( attrColumnNames[ 0 ] != null ) { + if ( !value.getSelectables().get( 0 ).isFormula() ) { attrColumnExpression = attrColumnNames[ 0 ]; isAttrColumnExpressionFormula = false; @@ -5338,8 +5401,7 @@ protected AttributeMapping generateNonIdAttributeMapping( resolveAggregateColumnBasicType( creationProcess, role, column ); } else { - final String[] attrColumnFormulaTemplate = propertyColumnFormulaTemplates[ propertyIndex ]; - attrColumnExpression = attrColumnFormulaTemplate[ 0 ]; + attrColumnExpression = attrColumnNames[ 0 ]; isAttrColumnExpressionFormula = true; customReadExpr = null; customWriteExpr = null; @@ -5379,7 +5441,7 @@ protected AttributeMapping generateNonIdAttributeMapping( value.isColumnInsertable( 0 ), value.isColumnUpdateable( 0 ), propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, creationProcess ); } @@ -5424,7 +5486,7 @@ else if ( attrType instanceof CompositeType ) { tableExpression, null, propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, creationProcess ); } @@ -5436,7 +5498,7 @@ else if ( attrType instanceof CollectionType ) { bootProperty, this, propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, getFetchMode( stateArrayPosition ), creationProcess ); @@ -5452,7 +5514,7 @@ else if ( attrType instanceof EntityType entityType ) { this, entityType, propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, creationProcess ); } @@ -5715,6 +5777,11 @@ else if ( treatTargetType != this ) { } private ModelPart findSubPartInSubclassMappings(String name) { + final var declaredGenericAttribute = declaredGenericAttributeMappings.get( name ); + if ( declaredGenericAttribute != null ) { + return declaredGenericAttribute; + } + ModelPart attribute = null; if ( isNotEmpty( subclassMappingTypes ) ) { for ( var subMappingType : subclassMappingTypes.values() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java index eceee2682a1b..3816ca6724bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java @@ -18,7 +18,7 @@ public abstract class AbstractJsonFormatMapper implements FormatMapper { @Override public final T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { final Type type = javaType.getJavaType(); - if ( type == String.class || type == Object.class ) { + if ( type == String.class ) { return (T) charSequence.toString(); } return fromString( charSequence, type ); @@ -27,7 +27,7 @@ public final T fromString(CharSequence charSequence, JavaType javaType, W @Override public final String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { final Type type = javaType.getJavaType(); - if ( type == String.class || type == Object.class ) { + if ( value instanceof String ) { return (String) value; } return toString( value, type ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/SingleTableAndGenericsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/SingleTableAndGenericsTest.java index 87c1806eb87b..1b90a442fe24 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/SingleTableAndGenericsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/SingleTableAndGenericsTest.java @@ -20,25 +20,31 @@ import jakarta.persistence.Inheritance; import jakarta.persistence.Table; +import java.util.Map; + import static jakarta.persistence.InheritanceType.SINGLE_TABLE; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hibernate.type.SqlTypes.JSON; +import static org.junit.jupiter.api.Assertions.assertTrue; @DomainModel( annotatedClasses = { // the order is important to reproduce the issue SingleTableAndGenericsTest.B.class, SingleTableAndGenericsTest.A.class, + SingleTableAndGenericsTest.C.class, } ) @SessionFactory @JiraKey("HHH-17644") +@JiraKey("HHH-19978") public class SingleTableAndGenericsTest { @Test public void testIt(SessionFactoryScope scope) { - String payload = "{\"book\": \"1\"}"; + Map payload = Map.of("book","1"); String aId = "1"; + String cId = "2"; scope.inTransaction( session -> { @@ -46,6 +52,11 @@ public void testIt(SessionFactoryScope scope) { a.setId( aId ); session.persist( a ); a.setPayload( payload ); + + C c = new C(); + c.setId( cId ); + session.persist( c ); + c.setPayload( "{\"book\":\"2\"}" ); } ); @@ -53,9 +64,35 @@ public void testIt(SessionFactoryScope scope) { session -> { A a = session.find( A.class, aId ); assertThat( a ).isNotNull(); - String payload1 = a.getPayload(); + Map payload1 = a.getPayload(); + assertThat( payload1 ).isNotNull(); + assertTrue( payload1.containsKey("book") ); + + C c = session.find( C.class, cId ); + assertThat( c ).isNotNull(); + String payload2 = c.getPayload(); + assertThat( payload2 ).isNotNull(); + assertThat( payload2 ).contains( "book" ); + assertThat( payload2 ).contains( "2" ); + } + ); + + scope.inTransaction( + session -> { + Object aPayload = session.createQuery( "select a.payload from A a where a.id = :id").setParameter( "id", aId ).getSingleResult(); + assertThat( aPayload ).isNotNull(); + assertThat( aPayload ).isInstanceOf( Map.class ); + Map payload1 = (Map) aPayload; assertThat( payload1 ).isNotNull(); - assertThat( payload1 ).contains( "book" ); + assertTrue( payload1.containsKey("book") ); + + Object cPayload = session.createQuery( "select c.payload from C c where c.id = :id").setParameter( "id", cId ).getSingleResult(); + assertThat( cPayload ).isNotNull(); + assertThat( cPayload ).isInstanceOf( String.class ); + String payload2 = (String) cPayload; + assertThat( payload2 ).isNotNull(); + assertThat( payload2 ).contains( "book" ); + assertThat( payload2 ).contains( "2" ); } ); } @@ -91,8 +128,13 @@ public void setPayload(T payload) { } } - @Entity(name = "C") + @Entity(name = "A") @DiscriminatorValue("child") - public static class A extends B { + public static class A extends B> { + } + + @Entity(name = "C") + @DiscriminatorValue("child2") + public static class C extends B { } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java index 7f5ecf8be859..a6440785f92c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java @@ -11,6 +11,7 @@ import jakarta.persistence.Table; import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Root; +import org.assertj.core.api.AssertionsForClassTypes; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.cfg.AvailableSettings; import org.hibernate.community.dialect.AltibaseDialect; @@ -42,8 +43,12 @@ import java.nio.charset.StandardCharsets; import java.sql.Blob; import java.sql.Clob; +import java.util.ArrayDeque; +import java.util.Dictionary; +import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; @@ -53,12 +58,14 @@ import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.hibernate.type.SqlTypes.JSON; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author Christian Beikov * @author Yanming Zhou */ -@DomainModel(annotatedClasses = JsonMappingTests.EntityWithJson.class) +@DomainModel(annotatedClasses = {JsonMappingTests.EntityWithJson.class, JsonMappingTests.EntityWithObjectJson.class}) @SessionFactory public abstract class JsonMappingTests { @@ -76,6 +83,75 @@ public static class Jackson extends JsonMappingTests { public Jackson() { super( false ); } + + @Test + @JiraKey( "https://hibernate.atlassian.net/browse/HHH-19969" ) + public void jsonMappedToObjectTest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var entity = new EntityWithObjectJson(); + entity.id = 1L; + entity.json = Map.of("a", 1, "b", 2); + session.persist(entity); + + entity = new EntityWithObjectJson(); + entity.id = 2L; + entity.json = List.of("c", 11, 22, "d"); + session.persist(entity); + + entity = new EntityWithObjectJson(); + entity.id = 3L; + entity.json = Set.of("s1", 2, "s3"); + session.persist(entity); + + entity = new EntityWithObjectJson(); + entity.id = 4L; + Queue ad = new ArrayDeque<>(); + ad.add(2); + ad.add(1); + ad.add(3); + entity.json = ad; + session.persist(entity); + + entity = new EntityWithObjectJson(); + entity.id = 5L; + Dictionary ht = new Hashtable<>(); + ht.put(1, "one"); + ht.put(2, "two"); + ht.put(3, "three"); + entity.json = ht; + session.persist(entity); + } + ); + scope.inTransaction( + session -> { + var entity = session.find( EntityWithObjectJson.class, 1L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( Map.class ); + assertEquals( 2, ((Map)entity.json).size() ); + + entity = session.find( EntityWithObjectJson.class, 2L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( List.class ); + assertEquals( 4, ((List)entity.json).size() ); + + entity = session.find( EntityWithObjectJson.class, 3L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( List.class ); + assertEquals( 3, ((List)entity.json).size() ); + + entity = session.find( EntityWithObjectJson.class, 4L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( List.class ); + assertEquals( 3, ((List)entity.json).size() ); + + entity = session.find( EntityWithObjectJson.class, 5L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( Map.class ); + assertEquals( 3, ((Map)entity.json).size() ); + } + ); + } } private final Map stringMap; @@ -228,15 +304,13 @@ public void verifyComparisonWorks(SessionFactoryScope scope) { .get( 0 ); final String jsonText; try { - if ( nativeJson instanceof Blob ) { - final Blob blob = (Blob) nativeJson; + if ( nativeJson instanceof Blob blob ) { jsonText = new String( blob.getBytes( 1L, (int) blob.length() ), StandardCharsets.UTF_8 ); } - else if ( nativeJson instanceof Clob ) { - final Clob jsonClob = (Clob) nativeJson; + else if ( nativeJson instanceof Clob jsonClob ) { jsonText = jsonClob.getSubString( 1L, (int) jsonClob.length() ); } else { @@ -363,4 +437,21 @@ public int hashCode() { return string != null ? string.hashCode() : 0; } } + + @Entity + public static class EntityWithObjectJson { + @Id + long id; + + @JdbcTypeCode(JSON) + Object json; + + public EntityWithObjectJson() { + } + + public EntityWithObjectJson(long id, Object json) { + this.id = id; + this.json = json; + } + } }