diff --git a/hibernate-core/src/main/java/org/hibernate/boot/jaxb/mapping/spi/JaxbLockableAttribute.java b/hibernate-core/src/main/java/org/hibernate/boot/jaxb/mapping/spi/JaxbLockableAttribute.java index 98a2ee6814ce..50619d918364 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/jaxb/mapping/spi/JaxbLockableAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/jaxb/mapping/spi/JaxbLockableAttribute.java @@ -10,6 +10,6 @@ * @author Steve Ebersole */ public interface JaxbLockableAttribute extends JaxbPersistentAttribute { - boolean isOptimisticLock(); + Boolean isOptimisticLock(); void setOptimisticLock(Boolean value); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/AttributeProcessor.java b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/AttributeProcessor.java index c579b42a51fe..e3e3ee18fb15 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/AttributeProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/AttributeProcessor.java @@ -10,6 +10,7 @@ import org.hibernate.boot.jaxb.mapping.spi.JaxbAssociationOverrideImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbAttributeOverrideImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbAttributesContainer; +import org.hibernate.boot.jaxb.mapping.spi.JaxbAttributesContainerImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbBaseAttributesContainer; import org.hibernate.boot.jaxb.mapping.spi.JaxbBasicImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbElementCollectionImpl; @@ -22,6 +23,7 @@ import org.hibernate.boot.jaxb.mapping.spi.JaxbPersistentAttribute; import org.hibernate.boot.jaxb.mapping.spi.JaxbPluralAnyMappingImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbTransientImpl; +import org.hibernate.boot.jaxb.mapping.spi.JaxbVersionImpl; import org.hibernate.boot.models.HibernateAnnotations; import org.hibernate.boot.models.xml.internal.attr.AnyMappingAttributeProcessing; import org.hibernate.boot.models.xml.internal.attr.BasicAttributeProcessing; @@ -151,12 +153,21 @@ public interface MemberAdjuster { } public static void processAttributes( - JaxbAttributesContainer attributesContainer, + JaxbAttributesContainerImpl attributesContainer, MutableClassDetails mutableClassDetails, AccessType classAccessType, XmlDocumentContext xmlDocumentContext) { processAttributes( attributesContainer, mutableClassDetails, classAccessType, null, xmlDocumentContext ); } + public static void processAttributes( + JaxbAttributesContainerImpl attributesContainer, + MutableClassDetails mutableClassDetails, + AccessType classAccessType, + MemberAdjuster memberAdjuster, + XmlDocumentContext xmlDocumentContext) { + processAttributes( (JaxbAttributesContainer) attributesContainer, mutableClassDetails, classAccessType, memberAdjuster, xmlDocumentContext ); + processVersionAttribute( attributesContainer.getVersion(), mutableClassDetails, classAccessType, xmlDocumentContext ); + } public static void processAttributes( JaxbAttributesContainer attributesContainer, @@ -265,4 +276,17 @@ public static void processAssociationOverrides( xmlDocumentContext ); } + + public static void processVersionAttribute( + JaxbVersionImpl version, + MutableClassDetails mutableClassDetails, AccessType classAccessType, + XmlDocumentContext xmlDocumentContext + ) { + XmlAnnotationHelper.applyVersion( + version, + mutableClassDetails, + classAccessType, + xmlDocumentContext + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlAnnotationHelper.java b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlAnnotationHelper.java index 9a944d0b0b50..36608dcaed68 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlAnnotationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlAnnotationHelper.java @@ -24,6 +24,7 @@ import java.util.UUID; import java.util.function.Consumer; +import jakarta.persistence.AccessType; import org.hibernate.AnnotationException; import org.hibernate.annotations.CascadeType; import org.hibernate.annotations.NotFoundAction; @@ -73,6 +74,7 @@ import org.hibernate.boot.jaxb.mapping.spi.JaxbUniqueConstraintImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbUserTypeImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbUuidGeneratorImpl; +import org.hibernate.boot.jaxb.mapping.spi.JaxbVersionImpl; import org.hibernate.boot.models.HibernateAnnotations; import org.hibernate.boot.models.JpaAnnotations; import org.hibernate.boot.models.XmlAnnotations; @@ -81,6 +83,7 @@ import org.hibernate.boot.models.annotations.spi.DatabaseObjectDetails; import org.hibernate.boot.models.JpaEventListenerStyle; import org.hibernate.boot.models.spi.JpaEventListener; +import org.hibernate.boot.models.xml.internal.attr.CommonAttributeProcessing; import org.hibernate.boot.models.xml.internal.db.ForeignKeyProcessing; import org.hibernate.boot.models.xml.internal.db.JoinColumnProcessing; import org.hibernate.boot.models.xml.internal.db.TableProcessing; @@ -136,6 +139,7 @@ import static org.hibernate.boot.models.JpaAnnotations.UNIQUE_CONSTRAINT; import static org.hibernate.boot.models.xml.internal.UserTypeCasesMapKey.MAP_KEY_USER_TYPE_CASES; import static org.hibernate.boot.models.xml.internal.UserTypeCasesStandard.STANDARD_USER_TYPE_CASES; +import static org.hibernate.internal.util.NullnessHelper.coalesce; import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.internal.util.StringHelper.unqualify; @@ -1701,4 +1705,32 @@ public static void applyCollectionClassification( ); collectionClassification.value( classification ); } + + public static void applyVersion(JaxbVersionImpl version, MutableClassDetails mutableClassDetails, AccessType classAccessType, XmlDocumentContext xmlDocumentContext) { + if ( version != null ) { + final AccessType accessType = coalesce( version.getAccess(), classAccessType ); + final String versionAttributeName = version.getName(); + + final MutableMemberDetails memberDetails = XmlProcessingHelper.getAttributeMember( + versionAttributeName, + accessType, + mutableClassDetails + ); + memberDetails.applyAnnotationUsage( + JpaAnnotations.VERSION, + xmlDocumentContext.getModelBuildingContext() + ); + CommonAttributeProcessing.applyAccess( accessType, memberDetails, xmlDocumentContext ); + CommonAttributeProcessing.applyAttributeAccessor( version, memberDetails, xmlDocumentContext ); + XmlAnnotationHelper.applyTemporal( version.getTemporal(), memberDetails, xmlDocumentContext ); + if ( version.getColumn() != null ) { + final ColumnJpaAnnotation columnAnn = (ColumnJpaAnnotation) memberDetails.applyAnnotationUsage( + JpaAnnotations.COLUMN, + xmlDocumentContext.getModelBuildingContext() + ); + columnAnn.apply( version.getColumn(), xmlDocumentContext ); + XmlAnnotationHelper.applyColumnTransformation( version.getColumn(), memberDetails, xmlDocumentContext ); + } + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/attr/CommonAttributeProcessing.java b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/attr/CommonAttributeProcessing.java index 47f483f13efa..d672ccec2708 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/attr/CommonAttributeProcessing.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/attr/CommonAttributeProcessing.java @@ -84,12 +84,15 @@ public static void applyOptimisticLock( JaxbLockableAttribute jaxbAttribute, MutableMemberDetails memberDetails, XmlDocumentContext xmlDocumentContext) { - final boolean includeInOptimisticLock = jaxbAttribute.isOptimisticLock(); - final OptimisticLockAnnotation optLockAnn = (OptimisticLockAnnotation) memberDetails.applyAnnotationUsage( - HibernateAnnotations.OPTIMISTIC_LOCK, - xmlDocumentContext.getModelBuildingContext() - ); - optLockAnn.excluded( !includeInOptimisticLock ); + final Boolean includeInOptimisticLock = jaxbAttribute.isOptimisticLock(); + + if ( includeInOptimisticLock != null ) { + final OptimisticLockAnnotation optLockAnn = (OptimisticLockAnnotation) memberDetails.applyAnnotationUsage( + HibernateAnnotations.OPTIMISTIC_LOCK, + xmlDocumentContext.getModelBuildingContext() + ); + optLockAnn.excluded( !includeInOptimisticLock ); + } } public static void applyFetching( diff --git a/hibernate-core/src/main/resources/org/hibernate/xsd/mapping/mapping-7.0.xsd b/hibernate-core/src/main/resources/org/hibernate/xsd/mapping/mapping-7.0.xsd index 058843f32b66..83f066cccec6 100644 --- a/hibernate-core/src/main/resources/org/hibernate/xsd/mapping/mapping-7.0.xsd +++ b/hibernate-core/src/main/resources/org/hibernate/xsd/mapping/mapping-7.0.xsd @@ -601,7 +601,7 @@ - + @@ -1027,7 +1027,7 @@ - + @@ -1109,7 +1109,7 @@ - + @@ -1614,7 +1614,7 @@ - + @@ -1661,7 +1661,7 @@ - + @@ -2057,7 +2057,7 @@ - + @@ -2113,7 +2113,7 @@ - + @@ -3139,7 +3139,7 @@ - + @@ -3208,7 +3208,7 @@ - + @@ -3532,4 +3532,4 @@ - \ No newline at end of file + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/BaseShop.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/BaseShop.java new file mode 100644 index 000000000000..75cfab1df2f8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/BaseShop.java @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.xml; + + + +public class BaseShop { + + private int id; + + private int version; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/Consumer.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/Consumer.java new file mode 100644 index 000000000000..b5e935259811 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/Consumer.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.xml; + +import jakarta.persistence.Id; +import jakarta.persistence.Version; + +import java.util.List; + +public class Consumer { + + @Id + private int id; + + private List consumerItems; + + @Version + private int version; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public List getConsumerItems() { + return consumerItems; + } + + public void setConsumerItems(List consumerItems) { + this.consumerItems = consumerItems; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/ConsumerItem.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/ConsumerItem.java new file mode 100644 index 000000000000..d984e44f275f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/ConsumerItem.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.xml; + +import jakarta.persistence.Version; + +public class ConsumerItem { + + private int id; + + private Consumer consumer; + + @Version + private int version; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Consumer getConsumer() { + return consumer; + } + + public void setConsumer(Consumer consumer) { + this.consumer = consumer; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/ExplicitOptimisticLockAnnotationOnCollectionXmlOnlyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/ExplicitOptimisticLockAnnotationOnCollectionXmlOnlyTest.java new file mode 100644 index 000000000000..8aae05a896ee --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/ExplicitOptimisticLockAnnotationOnCollectionXmlOnlyTest.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.xml; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + + +@JiraKey(value = "HHH-19528") +@Jpa( + xmlMappings = {"org/hibernate/orm/test/jpa/xml/ExplicitOptimisticLockAnnotationOnCollectionXmlOnlyTest.xml"}, + useCollectingStatementInspector = true +) +class ExplicitOptimisticLockAnnotationOnCollectionXmlOnlyTest { + + private static SQLStatementInspector statementInspector; + private static int consumerId; + + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( em -> { + Consumer consumer = new Consumer(); + em.persist( consumer ); + consumerId = consumer.getId(); + + ConsumerItem item1 = new ConsumerItem(); + item1.setConsumer( consumer ); + em.persist( item1 ); + + ConsumerItem item2 = new ConsumerItem(); + item2.setConsumer( consumer ); + em.persist( item2 ); + } ); + } + + @Test + void test(EntityManagerFactoryScope scope) { + statementInspector.clear(); + + scope.inTransaction( em -> { + Consumer consumer = em.find( Consumer.class, consumerId ); + ConsumerItem inventory = new ConsumerItem(); + inventory.setConsumer( consumer ); + consumer.getConsumerItems().add( inventory ); + } ); + statementInspector.assertUpdate(); + statementInspector.assertInsert(); + } + + @Test + void testVersionOnMappedSupertype(EntityManagerFactoryScope scope) { + var shop = scope.fromTransaction( em -> { + Supermarket supermarket = new Supermarket(); + supermarket.setName( "Tesco" ); + em.persist( supermarket ); + return supermarket; + } ); + + statementInspector.clear(); + scope.inTransaction( em -> { + Supermarket supermarket = em.find( Supermarket.class, shop.getId() ); + supermarket.setName( "Leclerc" ); + } ); + scope.inTransaction( em -> { + Supermarket supermarket = em.find( Supermarket.class, shop.getId() ); + assertThat( shop.getVersion() ).isNotEqualTo( supermarket.getVersion() ); + } ); + + statementInspector.assertHasQueryMatching( "update.*version.*" ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/NoDefaultOptimisticLockAnnotationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/NoDefaultOptimisticLockAnnotationTest.java new file mode 100644 index 000000000000..425937965357 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/NoDefaultOptimisticLockAnnotationTest.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.xml; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +@JiraKey(value = "HHH-19495") +@Jpa( + xmlMappings = {"org/hibernate/orm/test/jpa/xml/NoDefaultOptimisticLockAnnotationTest.xml"}, + annotatedClasses = {Consumer.class, ConsumerItem.class}, + useCollectingStatementInspector = true +) +class NoDefaultOptimisticLockAnnotationTest { + + private static SQLStatementInspector statementInspector; + private static int consumerId; + + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( em -> { + Consumer consumer = new Consumer(); + em.persist( consumer ); + consumerId = consumer.getId(); + + ConsumerItem item1 = new ConsumerItem(); + item1.setConsumer( consumer ); + em.persist( item1 ); + + ConsumerItem item2 = new ConsumerItem(); + item2.setConsumer( consumer ); + em.persist( item2 ); + } ); + } + + @Test + void test(EntityManagerFactoryScope scope) { + statementInspector.clear(); + + scope.inTransaction( em -> { + Consumer consumer = em.find( Consumer.class, consumerId ); + ConsumerItem inventory = new ConsumerItem(); + inventory.setConsumer( consumer ); + consumer.getConsumerItems().add( inventory ); + } ); + + statementInspector.assertIsInsert( 1 ); + statementInspector.assertNoUpdate(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/Supermarket.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/Supermarket.java new file mode 100644 index 000000000000..f520b6469db7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/xml/Supermarket.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.xml; + +public class Supermarket extends BaseShop { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/resources/org/hibernate/orm/test/jpa/xml/ExplicitOptimisticLockAnnotationOnCollectionXmlOnlyTest.xml b/hibernate-core/src/test/resources/org/hibernate/orm/test/jpa/xml/ExplicitOptimisticLockAnnotationOnCollectionXmlOnlyTest.xml new file mode 100644 index 000000000000..4ea384661d8a --- /dev/null +++ b/hibernate-core/src/test/resources/org/hibernate/orm/test/jpa/xml/ExplicitOptimisticLockAnnotationOnCollectionXmlOnlyTest.xml @@ -0,0 +1,67 @@ + + + + + org.hibernate.orm.test.jpa.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hibernate-core/src/test/resources/org/hibernate/orm/test/jpa/xml/NoDefaultOptimisticLockAnnotationTest.xml b/hibernate-core/src/test/resources/org/hibernate/orm/test/jpa/xml/NoDefaultOptimisticLockAnnotationTest.xml new file mode 100644 index 000000000000..92dbdabbb6ae --- /dev/null +++ b/hibernate-core/src/test/resources/org/hibernate/orm/test/jpa/xml/NoDefaultOptimisticLockAnnotationTest.xml @@ -0,0 +1,52 @@ + + + + + org.hibernate.orm.test.jpa.xml + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SQLStatementInspector.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SQLStatementInspector.java index d5c42adc15e9..a322069b3cab 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SQLStatementInspector.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SQLStatementInspector.java @@ -127,7 +127,19 @@ public void assertUpdate() { .anySatisfy( sql -> Assertions.assertThat( sql.toLowerCase( Locale.ROOT ) ).startsWith( "update" ) ); } + public void assertInsert() { + Assertions.assertThat( sqlQueries ) + .isNotEmpty() + .anySatisfy( sql -> Assertions.assertThat( sql.toLowerCase( Locale.ROOT ) ).startsWith( "insert" ) ); + } + public static SQLStatementInspector extractFromSession(SessionImplementor session) { return (SQLStatementInspector) session.getJdbcSessionContext().getStatementInspector(); } + + public void assertHasQueryMatching(String queryPattern) { + Assertions.assertThat( sqlQueries ) + .isNotEmpty() + .anySatisfy( sql -> Assertions.assertThat( sql.toLowerCase( Locale.ROOT ) ).matches( queryPattern ) ); + } }