From b1135b537c1c98417d0aff6b147f9348bd7b5487 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Sat, 19 Oct 2024 10:21:34 -0500 Subject: [PATCH] HHH-18728 - Allow mixed discriminator-value mappings for ANY --- .../annotations/AnyDiscriminator.java | 10 + .../boot/model/internal/AnyBinder.java | 6 + .../internal/AnyDiscriminatorAnnotation.java | 13 ++ .../main/java/org/hibernate/mapping/Any.java | 26 +++ .../mapping/DiscriminatorConverter.java | 6 +- .../EmbeddableDiscriminatorConverter.java | 21 +- .../ExplicitDiscriminatorConverter.java | 22 +- .../ImplicitDiscriminatorConverter.java | 22 +- .../internal/MixedDiscriminatorConverter.java | 20 +- .../type/AnyDiscriminatorValueStrategy.java | 3 + .../AnyDiscriminatorValueHandlingTests.java | 121 ++++++++++ .../AnyDiscriminatorValueStrategyTests.java | 217 ++++++++++++++++++ .../orm/test/any/mixed/CardPayment.java | 50 ++++ .../orm/test/any/mixed/CashPayment.java | 40 ++++ .../orm/test/any/mixed/CheckPayment.java | 38 +++ .../hibernate/orm/test/any/mixed/Order.java | 57 +++++ .../hibernate/orm/test/any/mixed/Payment.java | 12 + 17 files changed, 664 insertions(+), 20 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/AnyDiscriminatorValueHandlingTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/AnyDiscriminatorValueStrategyTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CardPayment.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CashPayment.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CheckPayment.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/Order.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/Payment.java diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/AnyDiscriminator.java b/hibernate-core/src/main/java/org/hibernate/annotations/AnyDiscriminator.java index 4cfd3f08ad37..afc6b38c0210 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/AnyDiscriminator.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/AnyDiscriminator.java @@ -8,11 +8,13 @@ import java.lang.annotation.Target; import jakarta.persistence.DiscriminatorType; +import org.hibernate.type.AnyDiscriminatorValueStrategy; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.hibernate.type.AnyDiscriminatorValueStrategy.AUTO; /** * A simplified way to specify the type of the discriminator in an {@link Any} @@ -44,4 +46,12 @@ * or {@link JdbcTypeCode}. */ DiscriminatorType value() default DiscriminatorType.STRING; + + /** + * How the discriminator value should be handled in regard to explicit + * {@linkplain AnyDiscriminatorValue} mappings, if any. + * + * @since 7.0 + */ + AnyDiscriminatorValueStrategy valueStrategy() default AUTO; } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnyBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnyBinder.java index d9c4e07e1109..8199e10fe011 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnyBinder.java @@ -8,6 +8,7 @@ import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; +import org.hibernate.annotations.AnyDiscriminator; import org.hibernate.annotations.Cascade; import org.hibernate.annotations.Columns; import org.hibernate.annotations.Formula; @@ -107,6 +108,11 @@ private static void bindAny( context ); + final AnyDiscriminator anyDiscriminator = property.getDirectAnnotationUsage( AnyDiscriminator.class ); + if ( anyDiscriminator != null ) { + value.setDiscriminatorValueStrategy( anyDiscriminator.valueStrategy() ); + } + final PropertyBinder binder = new PropertyBinder(); binder.setName( inferredData.getPropertyName() ); binder.setValue( value ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/AnyDiscriminatorAnnotation.java b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/AnyDiscriminatorAnnotation.java index 026d7f1776aa..12404277da4d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/AnyDiscriminatorAnnotation.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/AnyDiscriminatorAnnotation.java @@ -9,17 +9,20 @@ import org.hibernate.annotations.AnyDiscriminator; import org.hibernate.models.spi.SourceModelBuildingContext; +import org.hibernate.type.AnyDiscriminatorValueStrategy; @SuppressWarnings({ "ClassExplicitlyAnnotation", "unused" }) @jakarta.annotation.Generated("org.hibernate.orm.build.annotations.ClassGeneratorProcessor") public class AnyDiscriminatorAnnotation implements AnyDiscriminator { private jakarta.persistence.DiscriminatorType value; + private AnyDiscriminatorValueStrategy valueStrategy; /** * Used in creating dynamic annotation instances (e.g. from XML) */ public AnyDiscriminatorAnnotation(SourceModelBuildingContext modelContext) { this.value = jakarta.persistence.DiscriminatorType.STRING; + this.valueStrategy = AnyDiscriminatorValueStrategy.AUTO; } /** @@ -27,6 +30,7 @@ public AnyDiscriminatorAnnotation(SourceModelBuildingContext modelContext) { */ public AnyDiscriminatorAnnotation(AnyDiscriminator annotation, SourceModelBuildingContext modelContext) { this.value = annotation.value(); + this.valueStrategy = annotation.valueStrategy(); } /** @@ -50,5 +54,14 @@ public void value(jakarta.persistence.DiscriminatorType value) { this.value = value; } + @Override + public AnyDiscriminatorValueStrategy valueStrategy() { + return valueStrategy; + } + + public void valueStrategy(AnyDiscriminatorValueStrategy valueStrategy) { + this.valueStrategy = valueStrategy; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Any.java b/hibernate-core/src/main/java/org/hibernate/mapping/Any.java index c7eb58d9196b..60d01794e793 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Any.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Any.java @@ -9,6 +9,7 @@ import java.util.Objects; import java.util.function.Consumer; +import org.hibernate.Incubating; import org.hibernate.MappingException; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.engine.jdbc.spi.JdbcServices; @@ -34,6 +35,7 @@ public class Any extends SimpleValue { // common private Map metaValueToEntityNameMap; + private AnyDiscriminatorValueStrategy discriminatorValueStrategy = AnyDiscriminatorValueStrategy.AUTO; private boolean lazy = true; private AnyType resolvedType; @@ -73,6 +75,7 @@ public Any(Any original) { this.metaValueToEntityNameMap = original.metaValueToEntityNameMap == null ? null : new HashMap<>(original.metaValueToEntityNameMap); + this.discriminatorValueStrategy = original.discriminatorValueStrategy; this.lazy = original.lazy; } @@ -128,6 +131,28 @@ public void setIdentifierType(String identifierType) { this.keyMapping.setTypeName( identifierType ); } + /** + * Current strategy for interpreting {@linkplain org.hibernate.annotations.AnyDiscriminatorValue} definitions, + * especially in terms of implicit, explicit and potentially missing values. + * + * @since 7.0 + */ + @Incubating + public AnyDiscriminatorValueStrategy getDiscriminatorValueStrategy() { + return discriminatorValueStrategy; + } + + /** + * Set the strategy + * + * @see #getDiscriminatorValueStrategy + * @since 7.0 + */ + @Incubating + public void setDiscriminatorValueStrategy(AnyDiscriminatorValueStrategy discriminatorValueStrategy) { + this.discriminatorValueStrategy = discriminatorValueStrategy; + } + @Override public AnyType getType() throws MappingException { if ( resolvedType == null ) { @@ -150,6 +175,7 @@ public AnyType getType() throws MappingException { resolvedType = MappingHelper.anyMapping( discriminatorType, identifierType, + discriminatorValueStrategy, metaValueToEntityNameMap, isLazy(), getBuildingContext() diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorConverter.java index 99460aebb888..312c331eb987 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorConverter.java @@ -4,7 +4,9 @@ */ package org.hibernate.metamodel.mapping; +import org.hibernate.Incubating; import org.hibernate.metamodel.RepresentationMode; +import org.hibernate.type.AnyDiscriminatorValueStrategy; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.java.JavaType; @@ -16,7 +18,6 @@ * @author Gavin King */ public abstract class DiscriminatorConverter implements BasicValueConverter { - private final String discriminatorName; private final JavaType domainJavaType; private final JavaType relationalJavaType; @@ -30,6 +31,9 @@ public DiscriminatorConverter( this.relationalJavaType = relationalJavaType; } + @Incubating + public abstract AnyDiscriminatorValueStrategy getValueStrategy(); + public String getDiscriminatorName() { return discriminatorName; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EmbeddableDiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EmbeddableDiscriminatorConverter.java index 588cadd8654e..d16a4be2a2db 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EmbeddableDiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EmbeddableDiscriminatorConverter.java @@ -4,21 +4,22 @@ */ package org.hibernate.metamodel.mapping; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Function; - import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.metamodel.mapping.internal.EmbeddableDiscriminatorValueDetailsImpl; import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.AnyDiscriminatorValueStrategy; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + /** * Handles conversion of discriminator values for embeddable subtype classes * to their domain typed form. @@ -65,6 +66,12 @@ public EmbeddableDiscriminatorConverter( } ); } + @Override + public AnyDiscriminatorValueStrategy getValueStrategy() { + // discriminators for embeddables are always explicit + return AnyDiscriminatorValueStrategy.EXPLICIT; + } + @Override public O toDomainValue(R relationalForm) { assert relationalForm == null || getRelationalJavaType().isInstance( relationalForm ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ExplicitDiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ExplicitDiscriminatorConverter.java index e3dc1b213fae..7cd3ab5f04e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ExplicitDiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ExplicitDiscriminatorConverter.java @@ -12,6 +12,7 @@ import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyDiscriminatorValueStrategy; import org.hibernate.type.descriptor.java.CharacterJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.StringJavaType; @@ -60,6 +61,19 @@ public ExplicitDiscriminatorConverter( } ); } + @Override + public AnyDiscriminatorValueStrategy getValueStrategy() { + return AnyDiscriminatorValueStrategy.EXPLICIT; + } + + public Map getDetailsByValue() { + return detailsByValue; + } + + public Map getDetailsByEntityName() { + return detailsByEntityName; + } + @Override public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object relationalForm) { if ( relationalForm == null ) { @@ -80,13 +94,13 @@ public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object relation if ( relationalForm.getClass().isEnum() ) { final Object enumValue; if ( getRelationalJavaType() instanceof StringJavaType ) { - enumValue = ( (Enum) relationalForm ).name(); + enumValue = ( (Enum) relationalForm ).name(); } else if ( getRelationalJavaType() instanceof CharacterJavaType ) { - enumValue = ( (Enum) relationalForm ).name().charAt( 0 ); + enumValue = ( (Enum) relationalForm ).name().charAt( 0 ); } else { - enumValue = ( (Enum) relationalForm ).ordinal(); + enumValue = ( (Enum) relationalForm ).ordinal(); } final DiscriminatorValueDetails enumMatch = detailsByValue.get( enumValue ); if ( enumMatch != null ) { @@ -108,7 +122,7 @@ public DiscriminatorValueDetails getDetailsForEntityName(String entityName) { if ( valueDetails != null) { return valueDetails; } - throw new HibernateException( "Unknown entity name (" + discriminatorRole + ") : " + entityName ); + throw new HibernateException( "Entity not explicitly mapped for ANY discriminator (" + discriminatorRole + ") : " + entityName ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ImplicitDiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ImplicitDiscriminatorConverter.java index 90f769398223..cf0c5ee0f048 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ImplicitDiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ImplicitDiscriminatorConverter.java @@ -12,6 +12,7 @@ import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyDiscriminatorValueStrategy; import org.hibernate.type.descriptor.java.JavaType; import java.util.Map; @@ -51,6 +52,19 @@ public ImplicitDiscriminatorConverter( this.detailsByEntityName = CollectionHelper.concurrentMap( 8 ); } + @Override + public AnyDiscriminatorValueStrategy getValueStrategy() { + return AnyDiscriminatorValueStrategy.IMPLICIT; + } + + public Map getDetailsByValue() { + return detailsByValue; + } + + public Map getDetailsByEntityName() { + return detailsByEntityName; + } + @Override public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object value) { if ( value instanceof String incoming ) { @@ -58,10 +72,8 @@ public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object value) { if ( existingDetails != null ) { return existingDetails; } - final String entityName = mappingMetamodel.getImportedName( incoming ); - final EntityPersister persister = mappingMetamodel.findEntityDescriptor( entityName ); + final EntityPersister persister = mappingMetamodel.findEntityDescriptor( incoming ); if ( persister != null ) { - assert persister.getImportedName().equals( incoming ); return register( incoming, persister ); } } @@ -76,7 +88,7 @@ public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object value) { private DiscriminatorValueDetails register(Object value, EntityPersister entityDescriptor) { final DiscriminatorValueDetails details = new DiscriminatorValueDetailsImpl( value, entityDescriptor ); detailsByValue.put( value, details ); - detailsByEntityName.put( entityDescriptor.getImportedName(), details ); + detailsByEntityName.put( entityDescriptor.getEntityName(), details ); return details; } @@ -88,7 +100,7 @@ public DiscriminatorValueDetails getDetailsForEntityName(String entityName) { } final EntityPersister persister = mappingMetamodel.findEntityDescriptor( entityName ); if ( persister!= null ) { - return register( persister.getImportedName(), persister ); + return register( persister.getEntityName(), persister ); } throw new HibernateException( String.format( ROOT, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MixedDiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MixedDiscriminatorConverter.java index 5c1188a65f60..c4c9d99c8f6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MixedDiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MixedDiscriminatorConverter.java @@ -11,6 +11,7 @@ import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyDiscriminatorValueStrategy; import org.hibernate.type.descriptor.java.CharacterJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.StringJavaType; @@ -59,6 +60,19 @@ private DiscriminatorValueDetails register(Object value, EntityPersister entityD return details; } + @Override + public AnyDiscriminatorValueStrategy getValueStrategy() { + return AnyDiscriminatorValueStrategy.MIXED; + } + + public Map getDetailsByValue() { + return detailsByValue; + } + + public Map getDetailsByEntityName() { + return detailsByEntityName; + } + @Override public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object relationalForm) { if ( relationalForm == null ) { @@ -79,13 +93,13 @@ public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object relation if ( relationalForm.getClass().isEnum() ) { final Object enumValue; if ( getRelationalJavaType() instanceof StringJavaType ) { - enumValue = ( (Enum) relationalForm ).name(); + enumValue = ( (Enum) relationalForm ).name(); } else if ( getRelationalJavaType() instanceof CharacterJavaType ) { - enumValue = ( (Enum) relationalForm ).name().charAt( 0 ); + enumValue = ( (Enum) relationalForm ).name().charAt( 0 ); } else { - enumValue = ( (Enum) relationalForm ).ordinal(); + enumValue = ( (Enum) relationalForm ).ordinal(); } final DiscriminatorValueDetails enumMatch = detailsByValue.get( enumValue ); if ( enumMatch != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/AnyDiscriminatorValueStrategy.java b/hibernate-core/src/main/java/org/hibernate/type/AnyDiscriminatorValueStrategy.java index 1c2b982a0a93..b11dcbeee5d6 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AnyDiscriminatorValueStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AnyDiscriminatorValueStrategy.java @@ -4,6 +4,7 @@ */ package org.hibernate.type; +import org.hibernate.Incubating; import org.hibernate.annotations.AnyDiscriminatorValue; /** @@ -12,8 +13,10 @@ * * @see AnyDiscriminatorValue * + * @since 7.0 * @author Steve Ebersole */ +@Incubating public enum AnyDiscriminatorValueStrategy { /** * Pick between {@link #IMPLICIT} and {@link #EXPLICIT} based on diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/AnyDiscriminatorValueHandlingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/AnyDiscriminatorValueHandlingTests.java new file mode 100644 index 000000000000..a713f80205ba --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/AnyDiscriminatorValueHandlingTests.java @@ -0,0 +1,121 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.any.mixed; + +import org.hibernate.HibernateException; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link org.hibernate.type.AnyDiscriminatorValueStrategy} + * + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +public class AnyDiscriminatorValueHandlingTests { + @Test + @DomainModel(annotatedClasses = {Payment.class, CashPayment.class, CardPayment.class, CheckPayment.class, Order.class}) + @SessionFactory + void verifyImplicitMappingHandling(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final Order order = new Order( 1, "1" ); + final CashPayment cashPayment = new CashPayment( 1, 50.00 ); + final CardPayment cardPayment = new CardPayment( 1, 150.00, "123-456-789" ); + final CheckPayment checkPayment = new CheckPayment( 1, 250.00, 1001, "123", "987" ); + session.persist( order ); + session.persist( cashPayment ); + session.persist( cardPayment ); + session.persist( checkPayment ); + session.flush(); + + order.implicitPayment = cardPayment; + session.flush(); + + order.implicitPayment = checkPayment; + session.flush(); + + order.implicitPayment = cashPayment; + session.flush(); + } ); + } + + @Test + @DomainModel(annotatedClasses = {Payment.class, CashPayment.class, CardPayment.class, CheckPayment.class, Order.class}) + @SessionFactory + void verifyExplicitMappingHandling(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final Order order = new Order( 1, "1" ); + final CashPayment cashPayment = new CashPayment( 1, 50.00 ); + final CardPayment cardPayment = new CardPayment( 1, 150.00, "123-456-789" ); + final CheckPayment checkPayment = new CheckPayment( 1, 250.00, 1001, "123", "987" ); + session.persist( order ); + session.persist( cashPayment ); + session.persist( cardPayment ); + session.persist( checkPayment ); + session.flush(); + + order.explicitPayment = cardPayment; + session.flush(); + + order.explicitPayment = checkPayment; + session.flush(); + + // NOTE : cash is not explicitly mapped + try { + order.explicitPayment = cashPayment; + session.flush(); + fail( "Expecting an error" ); + } + catch (HibernateException expected) { + assertThat( expected ).hasMessageContaining( "Entity not explicitly mapped for ANY discriminator" ); + } + } ); + } + + @Test + @DomainModel(annotatedClasses = {Payment.class, CashPayment.class, CardPayment.class, CheckPayment.class, Order.class}) + @SessionFactory + void verifyMixedMappingHandling(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final Order order = new Order( 1, "1" ); + final CashPayment cashPayment = new CashPayment( 1, 50.00 ); + final CardPayment cardPayment = new CardPayment( 1, 150.00, "123-456-789" ); + final CheckPayment checkPayment = new CheckPayment( 1, 250.00, 1001, "123", "987" ); + session.persist( order ); + session.persist( cashPayment ); + session.persist( cardPayment ); + session.persist( checkPayment ); + session.flush(); + + order.mixedPayment = cardPayment; + session.flush(); + + order.mixedPayment = checkPayment; + session.flush(); + + order.mixedPayment = cashPayment; + session.flush(); + } ); + } + + @AfterEach + @DomainModel(annotatedClasses = {Payment.class, CashPayment.class, CardPayment.class, CheckPayment.class, Order.class}) + @SessionFactory + void dropTestData(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + session.createMutationQuery( "delete Order" ).executeUpdate(); + session.createMutationQuery( "delete CashPayment" ).executeUpdate(); + session.createMutationQuery( "delete CardPayment" ).executeUpdate(); + session.createMutationQuery( "delete CheckPayment" ).executeUpdate(); + } ); + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/AnyDiscriminatorValueStrategyTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/AnyDiscriminatorValueStrategyTests.java new file mode 100644 index 000000000000..aff913e79387 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/AnyDiscriminatorValueStrategyTests.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.any.mixed; + + +import org.hibernate.HibernateException; +import org.hibernate.metamodel.mapping.DiscriminatorConverter; +import org.hibernate.metamodel.mapping.DiscriminatorMapping; +import org.hibernate.metamodel.mapping.DiscriminatorValueDetails; +import org.hibernate.metamodel.mapping.internal.DiscriminatedAssociationAttributeMapping; +import org.hibernate.metamodel.mapping.internal.ExplicitDiscriminatorConverter; +import org.hibernate.metamodel.mapping.internal.ImplicitDiscriminatorConverter; +import org.hibernate.metamodel.mapping.internal.MixedDiscriminatorConverter; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.AnyDiscriminatorValueStrategy; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link org.hibernate.type.AnyDiscriminatorValueStrategy} + * + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +public class AnyDiscriminatorValueStrategyTests { + @Test + @DomainModel(annotatedClasses = {Payment.class, CashPayment.class, CardPayment.class, CheckPayment.class, Order.class}) + @SessionFactory + void verifyImplicitMappingModel(SessionFactoryScope sessions) { + sessions.withSessionFactory( (factory) -> { + final EntityPersister entityDescriptor = factory.getMappingMetamodel().getEntityDescriptor( Order.class ); + final DiscriminatedAssociationAttributeMapping implicitMapping = (DiscriminatedAssociationAttributeMapping) entityDescriptor.findAttributeMapping( "implicitPayment" ); + final DiscriminatorMapping discriminatorMapping = implicitMapping.getDiscriminatorMapping(); + final DiscriminatorConverter discriminatorConverter = discriminatorMapping.getValueConverter(); + assertThat( discriminatorConverter.getValueStrategy() ).isEqualTo( AnyDiscriminatorValueStrategy.IMPLICIT ); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // check discriminator -> entity + + final DiscriminatorValueDetails cash = discriminatorConverter.getDetailsForDiscriminatorValue( CashPayment.class.getName() ); + assertThat( cash.getIndicatedEntity().getEntityName() ).isEqualTo( CashPayment.class.getName() ); + assertThat( cash.getIndicatedEntityName() ).isEqualTo( CashPayment.class.getName() ); + + final DiscriminatorValueDetails card = discriminatorConverter.getDetailsForDiscriminatorValue( CardPayment.class.getName() ); + assertThat( card.getIndicatedEntity().getEntityName() ).isEqualTo( CardPayment.class.getName() ); + assertThat( card.getIndicatedEntityName() ).isEqualTo( CardPayment.class.getName() ); + + final DiscriminatorValueDetails check = discriminatorConverter.getDetailsForDiscriminatorValue( CheckPayment.class.getName() ); + assertThat( check.getIndicatedEntity().getEntityName() ).isEqualTo( CheckPayment.class.getName() ); + assertThat( check.getIndicatedEntityName() ).isEqualTo( CheckPayment.class.getName() ); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // check entity -> discriminator + + final DiscriminatorValueDetails cashDiscriminatorValue = discriminatorConverter.getDetailsForEntityName( CashPayment.class.getName() ); + assertThat( cashDiscriminatorValue.getValue() ).isEqualTo( CashPayment.class.getName() ); + + final DiscriminatorValueDetails cardDiscriminatorValue = discriminatorConverter.getDetailsForEntityName( CardPayment.class.getName() ); + assertThat( cardDiscriminatorValue.getValue() ).isEqualTo( CardPayment.class.getName() ); + + final DiscriminatorValueDetails checkDiscriminatorValue = discriminatorConverter.getDetailsForEntityName( CheckPayment.class.getName() ); + assertThat( checkDiscriminatorValue.getValue() ).isEqualTo( CheckPayment.class.getName() ); + + final Map detailsByEntityName = ((ImplicitDiscriminatorConverter) discriminatorConverter).getDetailsByEntityName(); + assertThat( detailsByEntityName.keySet() ).containsOnly( + CashPayment.class.getName(), + CardPayment.class.getName(), + CheckPayment.class.getName() + ); + + final Map detailsByValue = ((ImplicitDiscriminatorConverter) discriminatorConverter).getDetailsByValue(); + assertThat( detailsByValue.keySet() ).containsOnly( + CashPayment.class.getName(), + CardPayment.class.getName(), + CheckPayment.class.getName() + ); + } ); + } + + @Test + @DomainModel(annotatedClasses = {Payment.class, CashPayment.class, CardPayment.class, CheckPayment.class, Order.class}) + @SessionFactory + void verifyExplicitMappingModel(SessionFactoryScope sessions) { + sessions.withSessionFactory( (factory) -> { + final EntityPersister entityDescriptor = factory.getMappingMetamodel().getEntityDescriptor( Order.class ); + final DiscriminatedAssociationAttributeMapping explicitMapping = (DiscriminatedAssociationAttributeMapping) entityDescriptor.findAttributeMapping( "explicitPayment" ); + final DiscriminatorMapping discriminatorMapping = explicitMapping.getDiscriminatorMapping(); + final DiscriminatorConverter discriminatorConverter = discriminatorMapping.getValueConverter(); + assertThat( discriminatorConverter.getValueStrategy() ).isEqualTo( AnyDiscriminatorValueStrategy.EXPLICIT ); + + // NOTE : cash is NOT mapped + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // check discriminator -> entity + + try { + discriminatorConverter.getDetailsForDiscriminatorValue( "CASH" ); + fail( "Expecting an error" ); + } + catch (HibernateException expected) { + assertThat( expected ).hasMessageContaining( "Unknown discriminator value" ); + } + + try { + discriminatorConverter.getDetailsForDiscriminatorValue( CashPayment.class.getName() ); + fail( "Expecting an error" ); + } + catch (HibernateException expected) { + assertThat( expected ).hasMessageContaining( "Unknown discriminator value" ); + } + + final DiscriminatorValueDetails card = discriminatorConverter.getDetailsForDiscriminatorValue( "CARD" ); + assertThat( card.getIndicatedEntity().getEntityName() ).isEqualTo( CardPayment.class.getName() ); + assertThat( card.getIndicatedEntityName() ).isEqualTo( CardPayment.class.getName() ); + + final DiscriminatorValueDetails check = discriminatorConverter.getDetailsForDiscriminatorValue( "CHECK" ); + assertThat( check.getIndicatedEntity().getEntityName() ).isEqualTo( CheckPayment.class.getName() ); + assertThat( check.getIndicatedEntityName() ).isEqualTo( CheckPayment.class.getName() ); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // check entity -> discriminator + + try { + discriminatorConverter.getDetailsForDiscriminatorValue( CashPayment.class.getName() ); + fail( "Expecting an error" ); + } + catch (HibernateException expected) { + assertThat( expected ).hasMessageContaining( "Unknown discriminator value" ); + } + + final DiscriminatorValueDetails cardDiscriminatorValue = discriminatorConverter.getDetailsForEntityName( CardPayment.class.getName() ); + assertThat( cardDiscriminatorValue.getValue() ).isEqualTo( "CARD" ); + + final DiscriminatorValueDetails checkDiscriminatorValue = discriminatorConverter.getDetailsForEntityName( CheckPayment.class.getName() ); + assertThat( checkDiscriminatorValue.getValue() ).isEqualTo( "CHECK" ); + + + final Map detailsByEntityName = ((ExplicitDiscriminatorConverter) discriminatorConverter).getDetailsByEntityName(); + assertThat( detailsByEntityName.keySet() ).containsOnly( + CardPayment.class.getName(), + CheckPayment.class.getName() + ); + + final Map detailsByValue = ((ExplicitDiscriminatorConverter) discriminatorConverter).getDetailsByValue(); + assertThat( detailsByValue.keySet() ).containsOnly( + "CARD", + "CHECK" + ); + } ); + } + + @Test + @DomainModel(annotatedClasses = {Payment.class, CashPayment.class, CardPayment.class, CheckPayment.class, Order.class}) + @SessionFactory + void verifyMixedMappingModel(SessionFactoryScope sessions) { + sessions.withSessionFactory( (factory) -> { + final EntityPersister entityDescriptor = factory.getMappingMetamodel().getEntityDescriptor( Order.class ); + final DiscriminatedAssociationAttributeMapping mixedMapping = (DiscriminatedAssociationAttributeMapping) entityDescriptor.findAttributeMapping( "mixedPayment" ); + final DiscriminatorMapping discriminatorMapping = mixedMapping.getDiscriminatorMapping(); + final DiscriminatorConverter discriminatorConverter = discriminatorMapping.getValueConverter(); + // historically this operated as if EXPLICIT + assertThat( discriminatorConverter.getValueStrategy() ).isEqualTo( AnyDiscriminatorValueStrategy.MIXED ); + + // NOTE : cash is NOT mapped + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // check discriminator -> entity + + final DiscriminatorValueDetails cash = discriminatorConverter.getDetailsForDiscriminatorValue( CashPayment.class.getName() ); + assertThat( cash.getIndicatedEntity().getEntityName() ).isEqualTo( CashPayment.class.getName() ); + assertThat( cash.getIndicatedEntityName() ).isEqualTo( CashPayment.class.getName() ); + + final DiscriminatorValueDetails card = discriminatorConverter.getDetailsForDiscriminatorValue( "CARD" ); + assertThat( card.getIndicatedEntity().getEntityName() ).isEqualTo( CardPayment.class.getName() ); + assertThat( card.getIndicatedEntityName() ).isEqualTo( CardPayment.class.getName() ); + + final DiscriminatorValueDetails check = discriminatorConverter.getDetailsForDiscriminatorValue( "CHECK" ); + assertThat( check.getIndicatedEntity().getEntityName() ).isEqualTo( CheckPayment.class.getName() ); + assertThat( check.getIndicatedEntityName() ).isEqualTo( CheckPayment.class.getName() ); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // check entity -> discriminator + + final DiscriminatorValueDetails cashDiscriminatorValue = discriminatorConverter.getDetailsForEntityName( CashPayment.class.getName() ); + assertThat( cashDiscriminatorValue.getValue() ).isEqualTo( CashPayment.class.getName() ); + + final DiscriminatorValueDetails cardDiscriminatorValue = discriminatorConverter.getDetailsForEntityName( CardPayment.class.getName() ); + assertThat( cardDiscriminatorValue.getValue() ).isEqualTo( "CARD" ); + + final DiscriminatorValueDetails checkDiscriminatorValue = discriminatorConverter.getDetailsForEntityName( CheckPayment.class.getName() ); + assertThat( checkDiscriminatorValue.getValue() ).isEqualTo( "CHECK" ); + + final Map detailsByEntityName = ((MixedDiscriminatorConverter) discriminatorConverter).getDetailsByEntityName(); + assertThat( detailsByEntityName.keySet() ).containsOnly( + CashPayment.class.getName(), + CardPayment.class.getName(), + CheckPayment.class.getName() + ); + + final Map detailsByValue = ((MixedDiscriminatorConverter) discriminatorConverter).getDetailsByValue(); + assertThat( detailsByValue.keySet() ).containsOnly( + CashPayment.class.getName(), + "CARD", + "CHECK" + ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CardPayment.java b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CardPayment.java new file mode 100644 index 000000000000..e7726006ced4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CardPayment.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.any.mixed; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("unused") +@Entity(name = "CardPayment") +public class CardPayment implements Payment { + @Id + private Integer id; + private Double amount; + private String authorizationCode; + + public CardPayment() { + } + + public CardPayment(Integer id, Double amount, String authorizationCode) { + this.id = id; + this.amount = amount; + this.authorizationCode = authorizationCode; + } + + public Integer getId() { + return id; + } + + @Override + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public String getAuthorizationCode() { + return authorizationCode; + } + + public void setAuthorizationCode(String authorizationCode) { + this.authorizationCode = authorizationCode; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CashPayment.java b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CashPayment.java new file mode 100644 index 000000000000..7d338a22f40a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CashPayment.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.any.mixed; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("unused") +@Entity(name = "CashPayment") +public class CashPayment implements Payment { + @Id + private Integer id; + private Double amount; + + public CashPayment() { + } + + public CashPayment(Integer id, Double amount) { + this.id = id; + this.amount = amount; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CheckPayment.java b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CheckPayment.java new file mode 100644 index 000000000000..b0aabb7fc206 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/CheckPayment.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.any.mixed; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("unused") +@Entity +public class CheckPayment implements Payment { + @Id + public Integer id; + public Double amount; + public int checkNumber; + public String routingNumber; + public String accountNumber; + + public CheckPayment() { + } + + public CheckPayment(Integer id, Double amount, int checkNumber, String routingNumber, String accountNumber) { + this.id = id; + this.amount = amount; + this.checkNumber = checkNumber; + this.routingNumber = routingNumber; + this.accountNumber = accountNumber; + } + + @Override + public Double getAmount() { + return amount; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/Order.java b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/Order.java new file mode 100644 index 000000000000..741375fa78a5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/Order.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.any.mixed; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Basic; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; +import org.hibernate.annotations.Any; +import org.hibernate.annotations.AnyDiscriminator; +import org.hibernate.annotations.AnyDiscriminatorValue; +import org.hibernate.annotations.AnyKeyJavaClass; +import org.hibernate.type.AnyDiscriminatorValueStrategy; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "orders") +public class Order { + @Id + public Integer id; + @Basic + public String name; + + @Any + @AnyKeyJavaClass( Integer.class ) + @JoinColumn(name = "explicit_fk") + @AnyDiscriminatorValue( discriminator = "CARD", entity = CardPayment.class ) + @AnyDiscriminatorValue( discriminator = "CHECK", entity = CheckPayment.class ) + public Payment explicitPayment; + + @Any + @AnyKeyJavaClass( Integer.class ) + @JoinColumn(name = "implicit_fk") + public Payment implicitPayment; + + @Any + @AnyKeyJavaClass( Integer.class ) + @JoinColumn(name = "mixed_fk") + @AnyDiscriminator(valueStrategy = AnyDiscriminatorValueStrategy.MIXED) + @AnyDiscriminatorValue( discriminator = "CARD", entity = CardPayment.class ) + @AnyDiscriminatorValue( discriminator = "CHECK", entity = CheckPayment.class ) + public Payment mixedPayment; + + protected Order() { + // for Hibernate use + } + + public Order(Integer id, String name) { + this.id = id; + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/Payment.java b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/Payment.java new file mode 100644 index 000000000000..76d47ae3344e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/any/mixed/Payment.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.any.mixed; + +/** + * @author Steve Ebersole + */ +public interface Payment { + Double getAmount(); +}