HHH-17249 record as a composite type embeddable results in a PropertyNotFoundException

This commit is contained in:
Gavin King 2024-10-09 20:19:34 +02:00
parent bcac1e3299
commit e9fbf23ec8
5 changed files with 287 additions and 4 deletions

View File

@ -42,7 +42,7 @@
import org.hibernate.models.spi.SourceModelBuildingContext;
import org.hibernate.models.spi.TypeDetails;
import org.hibernate.property.access.internal.PropertyAccessStrategyCompositeUserTypeImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyMixedImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyGetterImpl;
import org.hibernate.property.access.spi.PropertyAccessStrategy;
import org.hibernate.resource.beans.internal.FallbackBeanInstanceProducer;
import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
@ -786,7 +786,7 @@ private static void processCompositeUserType(Component component, CompositeUserT
for ( Property property : component.getProperties() ) {
sortedPropertyNames.add( property.getName() );
sortedPropertyTypes.add(
PropertyAccessStrategyMixedImpl.INSTANCE.buildPropertyAccess(
PropertyAccessStrategyGetterImpl.INSTANCE.buildPropertyAccess(
compositeUserType.embeddable(),
property.getName(),
false

View File

@ -23,7 +23,7 @@
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.boot.registry.classloading.spi.ClassLoadingException;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.property.access.internal.PropertyAccessStrategyMixedImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyGetterImpl;
import org.hibernate.property.access.spi.Getter;
import jakarta.persistence.Transient;
@ -279,7 +279,7 @@ public static Class<?> reflectedPropertyClass(Class<?> clazz, String name) throw
}
private static Getter getter(Class<?> clazz, String name) throws MappingException {
return PropertyAccessStrategyMixedImpl.INSTANCE.buildPropertyAccess( clazz, name, true ).getGetter();
return PropertyAccessStrategyGetterImpl.INSTANCE.buildPropertyAccess( clazz, name, true ).getGetter();
}
/**

View File

@ -0,0 +1,89 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.property.access.internal;
import jakarta.persistence.AccessType;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.property.access.spi.Getter;
import org.hibernate.property.access.spi.GetterFieldImpl;
import org.hibernate.property.access.spi.GetterMethodImpl;
import org.hibernate.property.access.spi.PropertyAccess;
import org.hibernate.property.access.spi.PropertyAccessBuildingException;
import org.hibernate.property.access.spi.PropertyAccessStrategy;
import org.hibernate.property.access.spi.Setter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static org.hibernate.internal.util.ReflectHelper.getterMethodOrNull;
/**
* A {@link PropertyAccess} based on mix of getter method or field.
*
* @author Gavin King
*/
public class PropertyAccessGetterImpl implements PropertyAccess {
private final PropertyAccessStrategy strategy;
private final Getter getter;
public PropertyAccessGetterImpl(PropertyAccessStrategy strategy, Class<?> containerJavaType, String propertyName) {
this.strategy = strategy;
final AccessType propertyAccessType = AccessStrategyHelper.getAccessType( containerJavaType, propertyName );
switch ( propertyAccessType ) {
case FIELD: {
Field field = AccessStrategyHelper.fieldOrNull( containerJavaType, propertyName );
if ( field == null ) {
throw new PropertyAccessBuildingException(
"Could not locate field for property named [" + containerJavaType.getName() + "#" + propertyName + "]"
);
}
this.getter = fieldGetter( containerJavaType, propertyName, field );
break;
}
case PROPERTY: {
Method getterMethod = getterMethodOrNull( containerJavaType, propertyName );
if ( getterMethod == null ) {
throw new PropertyAccessBuildingException(
"Could not locate getter for property named [" + containerJavaType.getName() + "#" + propertyName + "]"
);
}
this.getter = propertyGetter( containerJavaType, propertyName, getterMethod );
break;
}
default: {
throw new PropertyAccessBuildingException(
"Invalid access type " + propertyAccessType + " for property named [" + containerJavaType.getName() + "#" + propertyName + "]"
);
}
}
}
// --- //
private static Getter fieldGetter(Class<?> containerJavaType, String propertyName, Field field) {
return new GetterFieldImpl( containerJavaType, propertyName, field );
}
private static Getter propertyGetter(Class<?> containerJavaType, String propertyName, Method method) {
return new GetterMethodImpl( containerJavaType, propertyName, method );
}
@Override
public PropertyAccessStrategy getPropertyAccessStrategy() {
return strategy;
}
@Override
public Getter getGetter() {
return getter;
}
@Override
public @Nullable Setter getSetter() {
return null;
}
}

View File

@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.property.access.internal;
import org.hibernate.property.access.spi.PropertyAccess;
import org.hibernate.property.access.spi.PropertyAccessStrategy;
/**
* A PropertyAccessStrategy that selects between available getter method or field.
*
* @author Gavin King
*/
public class PropertyAccessStrategyGetterImpl implements PropertyAccessStrategy {
/**
* Singleton access
*/
public static final PropertyAccessStrategyGetterImpl INSTANCE = new PropertyAccessStrategyGetterImpl();
@Override
public PropertyAccess buildPropertyAccess(Class<?> containerJavaType, String propertyName, boolean setterRequired) {
return new PropertyAccessGetterImpl( this, containerJavaType, propertyName );
}
}

View File

@ -0,0 +1,169 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded.record;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import org.hibernate.HibernateException;
import org.hibernate.annotations.CompositeType;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.spi.ValueAccess;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.usertype.CompositeUserType;
import org.javamoney.moneta.FastMoney;
import org.junit.Test;
import javax.money.MonetaryAmount;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Objects;
import static org.junit.Assert.assertEquals;
public class RecordAsCompositeTypeEmbeddableTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { RecordAsCompositeTypeEmbeddableEntity.class };
}
@Test
public void test() {
MonetaryAmount amount = FastMoney.of( 1, "BRL" );
RecordAsCompositeTypeEmbeddableEntity entity = new RecordAsCompositeTypeEmbeddableEntity();
entity.setAmount( amount );
entity.setId( 1L );
inTransaction( session -> {
session.persist( entity );
} );
inTransaction( session -> {
RecordAsCompositeTypeEmbeddableEntity result = session.find(
RecordAsCompositeTypeEmbeddableEntity.class,
1L
);
assertEquals( result.getAmount(), entity.getAmount() );
} );
}
public static class MonetaryAmountType implements CompositeUserType<MonetaryAmount> {
public record MonetaryAmountMapper(
BigDecimal amount,
String currency
) {
}
;
public MonetaryAmountType() {
}
@Override
public Object getPropertyValue(MonetaryAmount component, int property) throws HibernateException {
//Alphabetical
switch ( property ) {
case 0:
return component.getNumber().numberValueExact( BigDecimal.class );
case 1:
return component.getCurrency().getCurrencyCode();
default:
return null;
}
}
@Override
public MonetaryAmount instantiate(ValueAccess values, SessionFactoryImplementor sessionFactory) {
//Alphabetical
BigDecimal amount = values.getValue( 0, BigDecimal.class );
String currency = values.getValue( 1, String.class );
return FastMoney.of( amount, currency );
}
@Override
public Class<?> embeddable() {
return MonetaryAmountMapper.class;
}
@Override
public Class<MonetaryAmount> returnedClass() {
return MonetaryAmount.class;
}
@Override
public boolean equals(MonetaryAmount x, MonetaryAmount y) {
return Objects.equals( x, y );
}
@Override
public int hashCode(MonetaryAmount x) {
return x.hashCode();
}
@Override
public MonetaryAmount deepCopy(MonetaryAmount value) {
return value;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(MonetaryAmount value) {
return (Serializable) value;
}
@Override
public MonetaryAmount assemble(Serializable cached, Object owner) {
return (MonetaryAmount) cached;
}
@Override
public MonetaryAmount replace(MonetaryAmount detached, MonetaryAmount managed, Object owner) {
return detached;
}
}
@Entity(name="RecordAsCompositeTypeEmbeddableEntity")
public static class RecordAsCompositeTypeEmbeddableEntity {
@Id
Long id;
@Embedded
@AttributeOverride(name = "currency", column = @Column(name = "CURRENCY"))
@AttributeOverride(name = "amount", column = @Column(name = "AMOUNT"))
@CompositeType(MonetaryAmountType.class)
MonetaryAmount amount;
public RecordAsCompositeTypeEmbeddableEntity() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public MonetaryAmount getAmount() {
return amount;
}
public void setAmount(MonetaryAmount amount) {
this.amount = amount;
}
}
}