diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/extras/immutability/collection-immutability-update-example.log.txt b/documentation/src/main/asciidoc/userguide/chapters/domain/extras/immutability/collection-immutability-update-example.log.txt index 8fcb37705d..2d7e3b3ad4 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/extras/immutability/collection-immutability-update-example.log.txt +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/extras/immutability/collection-immutability-update-example.log.txt @@ -3,5 +3,5 @@ jakarta.persistence.RollbackException: Error while committing the transaction Caused by: jakarta.persistence.PersistenceException: org.hibernate.HibernateException: Caused by: org.hibernate.HibernateException: changed an immutable collection instance: [ - org.hibernate.userguide.immutability.CollectionImmutabilityTest$Batch.events#1 -] \ No newline at end of file + org.hibernate.orm.test.mapping.mutability.attribute.PluralAttributeMutabilityTest$Batch.events#1 +] diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/immutability.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/immutability.adoc index ba2ec99e16..91d3e2b4f9 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/immutability.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/immutability.adoc @@ -1,12 +1,36 @@ -[[entity-immutability]] -=== Immutability +[[mutability]] +=== Mutability :root-project-dir: ../../../../../../.. -:documentation-project-dir: {root-project-dir}/documentation -:example-dir-immutability: {documentation-project-dir}/src/test/java/org/hibernate/userguide/immutability +:core-project-dir: {root-project-dir}/hibernate-core +:mutability-example-dir: {core-project-dir}/src/test/java/org/hibernate/orm/test/mapping/mutability :extrasdir: extras/immutability -Immutability can be specified for both entities and collections. +Immutability can be specified for both entities and attributes. +Unfortunately mutability is an overloaded term. It can refer to either: + +- Whether the internal state of a value can be changed. In this sense, a `java.lang.Date` is considered +mutable because its internal state can be changed by calling `Date#setTime`, whereas `java.lang.String` +is considered immutable because its internal state cannot be changed. Hibernate uses this distinction +for numerous internal optimizations related to dirty checking and making copies. +- Whether the value is updateable in regard to the database. Hibernate can perform other optimizations +based on this distinction. + + +[[mutability-immutable]] +==== @Immutable + +The `@Immutable` annotation declares something immutable in the updateability sense. Mutable (updateable) +is the implicit condition. + +`@Immutable` is allowed on an <>, <>, +<> and <>. Unfortunately, it +has slightly different impacts depending on where it is placed; see the linked sections for details. + + + + +[[mutability-entity]] ==== Entity immutability If a specific entity is immutable, it is good practice to mark it with the `@Immutable` annotation. @@ -15,13 +39,13 @@ If a specific entity is immutable, it is good practice to mark it with the `@Imm ==== [source, JAVA, indent=0] ---- -include::{example-dir-immutability}/EntityImmutabilityTest.java[tags=entity-immutability-example] +include::{mutability-example-dir}/entity/EntityImmutabilityTest.java[tags=entity-immutability-example] ---- ==== Internally, Hibernate is going to perform several optimizations, such as: -- reducing memory footprint since there is no need to retain the dehydrated state for the dirty checking mechanism +- reducing memory footprint since there is no need to retain the loaded state for the dirty checking mechanism - speeding-up the Persistence Context flushing phase since immutable entities can skip the dirty checking process Considering the following entity is persisted in the database: @@ -30,7 +54,7 @@ Considering the following entity is persisted in the database: ==== [source, JAVA, indent=0] ---- -include::{example-dir-immutability}/EntityImmutabilityTest.java[tags=entity-immutability-persist-example] +include::{mutability-example-dir}/entity/EntityImmutabilityTest.java[tags=entity-immutability-persist-example] ---- ==== @@ -41,7 +65,7 @@ Hibernate will skip any modification, therefore no SQL `UPDATE` statement is exe ==== [source, JAVA, indent=0] ---- -include::{example-dir-immutability}/EntityImmutabilityTest.java[tags=entity-immutability-update-example] +include::{mutability-example-dir}/entity/EntityImmutabilityTest.java[tags=entity-immutability-update-example] ---- [source, SQL, indent=0] @@ -50,28 +74,70 @@ include::{extrasdir}/entity-immutability-update-example.sql[] ---- ==== -==== Collection immutability -Just like entities, collections can also be marked with the `@Immutable` annotation. +`@Mutability` is not allowed on an entity. -Considering the following entity mappings: -.Immutable collection + + + +[[mutability-attribute]] +==== Attribute mutability + +The `@Immutable` annotation may also be used on attributes. The impact varies +slightly depending on the exact kind of attribute. + +`@Mutability` on an attribute applies the specified `MutabilityPlan` to the attribute for handling +internal state changes in the values for the attribute. + + +[[mutability-attribute-basic]] +===== Attribute immutability - basic + +When applied to a basic attribute, `@Immutable` implies immutability in both the updateable +and internal-state sense. E.g. + +.Immutable basic attribute ==== [source, JAVA, indent=0] ---- -include::{example-dir-immutability}/CollectionImmutabilityTest.java[tags=collection-immutability-example] +include::{mutability-example-dir}/attribute/BasicAttributeMutabilityTests.java[tags=attribute-immutable-example] ---- ==== -This time, not only the `Event` entity is immutable, but the `Event` collection stored by the `Batch` parent entity. -Once the immutable collection is created, it can never be modified. +Changes to the `theDate` attribute are ignored. + +.Immutable basic attribute change +==== +[source, JAVA, indent=0] +---- +include::{mutability-example-dir}/attribute/BasicAttributeMutabilityTests.java[tags=attribute-immutable-managed-example] +---- +==== + + +[[mutability-attribute-embeddable]] +===== Attribute immutability - embeddable + +To be continued.. + +// todo : document the effect of `@Immutable` on `@Embeddable`, `@Embedded` and `@EmbeddedId` mappings + + +[[mutability-attribute-plural]] +===== Attribute immutability - plural + +Plural attributes (`@ElementCollection`, @OneToMany`, `@ManyToMany` and `@ManyToAny`) may also +be annotated with `@Immutable`. + +TIP:: While most immutable changes are simply discarded, modifying an immutable collection will cause an exception. + .Persisting an immutable collection ==== [source, JAVA, indent=0] ---- -include::{example-dir-immutability}/CollectionImmutabilityTest.java[tags=collection-immutability-persist-example] +include::{mutability-example-dir}/attribute/PluralAttributeMutabilityTest.java[tags=collection-immutability-persist-example] ---- ==== @@ -83,7 +149,7 @@ For instance, we can still modify the entity name: ==== [source, JAVA, indent=0] ---- -include::{example-dir-immutability}/CollectionImmutabilityTest.java[tags=collection-entity-update-example] +include::{mutability-example-dir}/attribute/PluralAttributeMutabilityTest.java[tags=collection-entity-update-example] ---- [source, SQL, indent=0] @@ -98,7 +164,7 @@ However, when trying to modify the `events` collection: ==== [source, JAVA, indent=0] ---- -include::{example-dir-immutability}/CollectionImmutabilityTest.java[tags=collection-immutability-update-example] +include::{mutability-example-dir}/attribute/PluralAttributeMutabilityTest.java[tags=collection-immutability-update-example] ---- [source, bash, indent=0] @@ -107,7 +173,96 @@ include::{extrasdir}/collection-immutability-update-example.log.txt[] ---- ==== -[TIP] + +[[mutability-attribute-entity]] +===== Attribute immutability - entity + +To be continued.. + +// todo : document the effect of `@Immutable` on `@OneToOne`, `@ManyToOne` and `@Any` mappings + + +[[mutability-converter]] +==== AttributeConverter mutability + +Declaring `@Mutability` on an `AttributeConverter` applies the specified `MutabilityPlan` to +all value mappings (attribute, collection element, etc.) to which the converter is applied. + +Declaring `@Immutable` on an `AttributeConverter` is shorthand for declaring `@Mutability` with an +immutable `MutabilityPlan`. + + +[[mutability-usertype]] +==== UserType mutability + +Similar to <> both `@Mutability` and `@Immutable` may +be declared on a `UserType`. + +`@Mutability` applies the specified `MutabilityPlan` to all value mappings (attribute, collection element, etc.) +to which the `UserType` is applied. + + +`@Immutable` applies an immutable `MutabilityPlan` to all value mappings (attribute, collection element, etc.) +to which the `UserType` is applied. + + +[[mutability-mutability]] +==== @Mutability + +`MutabilityPlan` is the contract used by Hibernate to abstract mutability concerns, in the sense of internal state changes. + +A Java type has an inherent `MutabilityPlan` based on its `JavaType#getMutabilityPlan`. + +The `@Mutability` annotation allows a specific `MutabilityPlan` to be used and is allowed on an +attribute, `AttributeConverter` and `UserType`. When used on a `AttributeConverter` or `UserType`, +the specified `MutabilityPlan` is effective for all basic values to which the `AttributeConverter` or +`UserType` is applied. + +To understand the impact of internal-state mutability, consider the following entity: + +.Basic mutability model ==== -While immutable entity changes are simply discarded, modifying an immutable collection will end up in a `HibernateException` being thrown. +[source, JAVA, indent=0] +---- +include::{mutability-example-dir}/MutabilityBaselineEntity.java[tags=mutability-base-entity-example] +---- ==== + +When dealing with an inherently immutable value, such as a `String`, there is only one way to +update the value: + +.Changing immutable value +==== +[source, JAVA, indent=0] +---- +include::{mutability-example-dir}/MutabilityBaselineEntity.java[tags=mutability-base-string-example] +---- +==== + +During flush, this change will make the entity "dirty" and the changes will be written (UPDATE) to +the database. + +When dealing with mutable values, however, Hibernate must be aware of both ways to change the value. First, like +with the immutable value, we can set the new value: + +.Changing mutable value - setting +==== +[source, JAVA, indent=0] +---- +include::{mutability-example-dir}/MutabilityBaselineEntity.java[tags=mutability-base-date-set-example] +---- +==== + +We can also mutate the existing value: + +.Changing mutable value - mutating +==== +[source, JAVA, indent=0] +---- +include::{mutability-example-dir}/MutabilityBaselineEntity.java[tags=mutability-base-date-mutate-example] +---- +==== + +This mutating example has the same effect as the setting example - they each will make the entity dirty. + + diff --git a/documentation/src/test/java/org/hibernate/userguide/immutability/EntityImmutabilityTest.java b/documentation/src/test/java/org/hibernate/userguide/immutability/EntityImmutabilityTest.java deleted file mode 100644 index 345ea9b5fa..0000000000 --- a/documentation/src/test/java/org/hibernate/userguide/immutability/EntityImmutabilityTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ -package org.hibernate.userguide.immutability; - -import java.util.Date; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; - -import org.hibernate.annotations.Immutable; -import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; - -import org.junit.Test; - -import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class EntityImmutabilityTest extends BaseEntityManagerFunctionalTestCase { - - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { - Event.class - }; - } - - @Test - public void test() { - //tag::entity-immutability-persist-example[] - doInJPA(this::entityManagerFactory, entityManager -> { - Event event = new Event(); - event.setId(1L); - event.setCreatedOn(new Date()); - event.setMessage("Hibernate User Guide rocks!"); - - entityManager.persist(event); - }); - //end::entity-immutability-persist-example[] - //tag::entity-immutability-update-example[] - doInJPA(this::entityManagerFactory, entityManager -> { - Event event = entityManager.find(Event.class, 1L); - log.info("Change event message"); - event.setMessage("Hibernate User Guide"); - }); - doInJPA(this::entityManagerFactory, entityManager -> { - Event event = entityManager.find(Event.class, 1L); - assertEquals("Hibernate User Guide rocks!", event.getMessage()); - }); - //end::entity-immutability-update-example[] - } - - //tag::entity-immutability-example[] - @Entity(name = "Event") - @Immutable - public static class Event { - - @Id - private Long id; - - private Date createdOn; - - private String message; - - //Getters and setters are omitted for brevity - - //end::entity-immutability-example[] - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - //tag::entity-immutability-example[] - } - //end::entity-immutability-example[] -} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Mutability.java b/hibernate-core/src/main/java/org/hibernate/annotations/Mutability.java index c18ed2812a..a763ef4faa 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Mutability.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Mutability.java @@ -20,7 +20,20 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Specifies a {@link MutabilityPlan} for a some sort of basic value mapping. + * Specifies a {@link MutabilityPlan} for a basic value mapping. + * + * Mutability refers to whether the internal state of a value can change. + * For example, {@linkplain java.util.Date Date} is considered mutable because its + * internal state can be changed using {@link java.util.Date#setTime} whereas + * {@linkplain java.lang.String String} is considered immutable because its internal + * state cannot be changed. Hibernate uses this distinction when it can for internal + * optimizations. + * + * Hibernate understands the inherent mutability of a large number of Java types - + * {@linkplain java.util.Date Date}, {@linkplain java.lang.String String}, etc. + * {@linkplain Mutability} and friends allow plugging in specific strategies. + * + * * *

Mutability for basic-typed attributes

*

diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java index 1d2328f820..4afafb3cd6 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java @@ -68,6 +68,7 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.type.BasicType; import org.hibernate.type.SerializableToBlobType; import org.hibernate.type.descriptor.java.BasicJavaType; +import org.hibernate.type.descriptor.java.Immutability; import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -479,20 +480,21 @@ public class BasicValueBinder implements JdbcTypeIndicators { explicitMutabilityAccess = (typeConfiguration) -> { final CollectionIdMutability mutabilityAnn = findAnnotation( modelXProperty, CollectionIdMutability.class ); if ( mutabilityAnn != null ) { - final Class> mutabilityClass = normalizeMutability( mutabilityAnn.value() ); + final Class> mutabilityClass = mutabilityAnn.value(); if ( mutabilityClass != null ) { - if ( useDeferredBeanContainerAccess ) { - return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutabilityClass ); - } - final ManagedBean> jtdBean = beanRegistry.getBean( mutabilityClass ); - return jtdBean.getBeanInstance(); + return resolveMutability( mutabilityClass ); } } - // see if the value's type Class is annotated `@Immutable` + // see if the value's type Class is annotated with mutability-related annotations if ( implicitJavaTypeAccess != null ) { final Class attributeType = ReflectHelper.getClass( implicitJavaTypeAccess.apply( typeConfiguration ) ); if ( attributeType != null ) { + final Mutability attributeTypeMutabilityAnn = attributeType.getAnnotation( Mutability.class ); + if ( attributeTypeMutabilityAnn != null ) { + return resolveMutability( attributeTypeMutabilityAnn.value() ); + } + if ( attributeType.isAnnotationPresent( Immutable.class ) ) { return ImmutableMutabilityPlan.instance(); } @@ -503,11 +505,7 @@ public class BasicValueBinder implements JdbcTypeIndicators { if ( converterDescriptor != null ) { final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class ); if ( converterMutabilityAnn != null ) { - if ( useDeferredBeanContainerAccess ) { - return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( converterMutabilityAnn.value() ); - } - final ManagedBean> jtdBean = beanRegistry.getBean( converterMutabilityAnn.value() ); - return jtdBean.getBeanInstance(); + return resolveMutability( converterMutabilityAnn.value() ); } if ( converterDescriptor.getAttributeConverterClass().isAnnotationPresent( Immutable.class ) ) { @@ -515,17 +513,22 @@ public class BasicValueBinder implements JdbcTypeIndicators { } } + // if there is a UserType, see if its Class is annotated with mutability-related annotations final Class> customTypeImpl = Kind.ATTRIBUTE.mappingAccess.customType( modelXProperty ); - if ( customTypeImpl.isAnnotationPresent( Immutable.class ) ) { - return ImmutableMutabilityPlan.instance(); + if ( customTypeImpl != null ) { + final Mutability customTypeMutabilityAnn = customTypeImpl.getAnnotation( Mutability.class ); + if ( customTypeMutabilityAnn != null ) { + return resolveMutability( customTypeMutabilityAnn.value() ); + } + + if ( customTypeImpl.isAnnotationPresent( Immutable.class ) ) { + return ImmutableMutabilityPlan.instance(); + } } // generally, this will trigger usage of the `JavaType#getMutabilityPlan` return null; }; - - // todo (6.0) - handle generator -// final String generator = collectionIdAnn.generator(); } private ManagedBeanRegistry getManagedBeanRegistry() { @@ -600,35 +603,32 @@ public class BasicValueBinder implements JdbcTypeIndicators { explicitMutabilityAccess = typeConfiguration -> { final MapKeyMutability mutabilityAnn = findAnnotation( mapAttribute, MapKeyMutability.class ); if ( mutabilityAnn != null ) { - final Class> mutabilityClass = normalizeMutability( mutabilityAnn.value() ); + final Class> mutabilityClass = mutabilityAnn.value(); if ( mutabilityClass != null ) { - if ( useDeferredBeanContainerAccess ) { - return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutabilityClass ); - } - final ManagedBean> jtdBean = getManagedBeanRegistry().getBean( mutabilityClass ); - return jtdBean.getBeanInstance(); + return resolveMutability( mutabilityClass ); } } - // see if the value's type Class is annotated `@Immutable` + // see if the value's Java Class is annotated with mutability-related annotations if ( implicitJavaTypeAccess != null ) { final Class attributeType = ReflectHelper.getClass( implicitJavaTypeAccess.apply( typeConfiguration ) ); if ( attributeType != null ) { + final Mutability attributeTypeMutabilityAnn = attributeType.getAnnotation( Mutability.class ); + if ( attributeTypeMutabilityAnn != null ) { + return resolveMutability( attributeTypeMutabilityAnn.value() ); + } + if ( attributeType.isAnnotationPresent( Immutable.class ) ) { return ImmutableMutabilityPlan.instance(); } } } - // if the value is converted, see if the converter Class is annotated `@Immutable` + // if the value is converted, see if converter Class is annotated with mutability-related annotations if ( converterDescriptor != null ) { final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class ); if ( converterMutabilityAnn != null ) { - if ( useDeferredBeanContainerAccess ) { - return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( converterMutabilityAnn.value() ); - } - final ManagedBean> jtdBean = getManagedBeanRegistry().getBean( converterMutabilityAnn.value() ); - return jtdBean.getBeanInstance(); + return resolveMutability( converterMutabilityAnn.value() ); } if ( converterDescriptor.getAttributeConverterClass().isAnnotationPresent( Immutable.class ) ) { @@ -636,8 +636,14 @@ public class BasicValueBinder implements JdbcTypeIndicators { } } + // if there is a UserType, see if its Class is annotated with mutability-related annotations final Class> customTypeImpl = Kind.MAP_KEY.mappingAccess.customType( mapAttribute ); if ( customTypeImpl != null ) { + final Mutability customTypeMutabilityAnn = customTypeImpl.getAnnotation( Mutability.class ); + if ( customTypeMutabilityAnn != null ) { + return resolveMutability( customTypeMutabilityAnn.value() ); + } + if ( customTypeImpl.isAnnotationPresent( Immutable.class ) ) { return ImmutableMutabilityPlan.instance(); } @@ -943,53 +949,80 @@ public class BasicValueBinder implements JdbcTypeIndicators { } private void normalMutabilityDetails(XProperty attributeXProperty) { - explicitMutabilityAccess = typeConfiguration -> { + // Look for `@Mutability` on the attribute final Mutability mutabilityAnn = findAnnotation( attributeXProperty, Mutability.class ); if ( mutabilityAnn != null ) { - final Class> mutability = normalizeMutability( mutabilityAnn.value() ); + final Class> mutability = mutabilityAnn.value(); if ( mutability != null ) { - if ( buildingContext.getBuildingOptions().disallowExtensionsInCdi() ) { - return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutability ); - } - return getManagedBeanRegistry().getBean( mutability ).getBeanInstance(); + return resolveMutability( mutability ); } } + // Look for `@Immutable` on the attribute final Immutable immutableAnn = attributeXProperty.getAnnotation( Immutable.class ); if ( immutableAnn != null ) { return ImmutableMutabilityPlan.instance(); } - // see if the value's type Class is annotated `@Immutable` - if ( implicitJavaTypeAccess != null ) { - final Class attributeType = ReflectHelper.getClass( implicitJavaTypeAccess.apply( typeConfiguration ) ); + // Look for `@Mutability` on the attribute's type + if ( explicitJavaTypeAccess != null || implicitJavaTypeAccess != null ) { + Class attributeType = null; + if ( explicitJavaTypeAccess != null ) { + final BasicJavaType jtd = explicitJavaTypeAccess.apply( typeConfiguration ); + if ( jtd != null ) { + attributeType = jtd.getJavaTypeClass(); + } + } + if ( attributeType == null ) { + final java.lang.reflect.Type javaType = implicitJavaTypeAccess.apply( typeConfiguration ); + if ( javaType != null ) { + attributeType = ReflectHelper.getClass( javaType ); + } + } + if ( attributeType != null ) { - if ( attributeType.isAnnotationPresent( Immutable.class ) ) { + final Mutability classMutability = attributeType.getAnnotation( Mutability.class ); + + if ( classMutability != null ) { + final Class> mutability = classMutability.value(); + if ( mutability != null ) { + return resolveMutability( mutability ); + } + } + + final Immutable classImmutable = attributeType.getAnnotation( Immutable.class ); + if ( classImmutable != null ) { return ImmutableMutabilityPlan.instance(); } } } - // if the value is converted, see if the converter Class is annotated `@Immutable` + // if the value is converted, see if the converter Class is annotated `@Mutability` if ( converterDescriptor != null ) { final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class ); if ( converterMutabilityAnn != null ) { - if ( buildingContext.getBuildingOptions().disallowExtensionsInCdi() ) { - return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( converterMutabilityAnn.value() ); - } - final ManagedBean> jtdBean = getManagedBeanRegistry().getBean( converterMutabilityAnn.value() ); - return jtdBean.getBeanInstance(); + final Class> mutability = converterMutabilityAnn.value(); + return resolveMutability( mutability ); } - if ( converterDescriptor.getAttributeConverterClass().isAnnotationPresent( Immutable.class ) ) { + final Immutable converterImmutableAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Immutable.class ); + if ( converterImmutableAnn != null ) { return ImmutableMutabilityPlan.instance(); } } + // if a custom UserType is specified, see if the UserType Class is annotated `@Mutability` final Class> customTypeImpl = Kind.ATTRIBUTE.mappingAccess.customType( attributeXProperty ); if ( customTypeImpl != null ) { - if ( customTypeImpl.isAnnotationPresent( Immutable.class ) ) { + final Mutability customTypeMutabilityAnn = customTypeImpl.getAnnotation( Mutability.class ); + if ( customTypeMutabilityAnn != null ) { + final Class> mutability = customTypeMutabilityAnn.value(); + return resolveMutability( mutability ); + } + + final Immutable customTypeImmutableAnn = customTypeImpl.getAnnotation( Immutable.class ); + if ( customTypeImmutableAnn != null ) { return ImmutableMutabilityPlan.instance(); } } @@ -999,6 +1032,23 @@ public class BasicValueBinder implements JdbcTypeIndicators { }; } + @SuppressWarnings({ "rawtypes", "unchecked" }) + private MutabilityPlan resolveMutability(Class mutability) { + if ( mutability.equals( Immutability.class ) ) { + return Immutability.instance(); + } + + if ( mutability.equals( ImmutableMutabilityPlan.class ) ) { + return ImmutableMutabilityPlan.instance(); + } + + if ( buildingContext.getBuildingOptions().disallowExtensionsInCdi() ) { + return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutability ); + } + + return getManagedBeanRegistry().getBean( mutability ).getBeanInstance(); + } + private void normalSupplementalDetails(XProperty attributeXProperty) { explicitJavaTypeAccess = typeConfiguration -> { @@ -1067,10 +1117,6 @@ public class BasicValueBinder implements JdbcTypeIndicators { return javaType; } - private Class> normalizeMutability(Class> mutability) { - return mutability; - } - private java.lang.reflect.Type resolveJavaType(XClass returnedClassOrElement) { return buildingContext.getBootstrapContext() .getReflectionManager() 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 6c24c4c388..486dcfed22 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 @@ -17,27 +17,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import jakarta.persistence.Access; -import jakarta.persistence.AttributeOverride; -import jakarta.persistence.AttributeOverrides; -import jakarta.persistence.Cacheable; -import jakarta.persistence.ConstraintMode; -import jakarta.persistence.DiscriminatorColumn; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.IdClass; -import jakarta.persistence.Inheritance; -import jakarta.persistence.InheritanceType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.NamedEntityGraph; -import jakarta.persistence.NamedEntityGraphs; -import jakarta.persistence.PrimaryKeyJoinColumn; -import jakarta.persistence.PrimaryKeyJoinColumns; -import jakarta.persistence.SecondaryTable; -import jakarta.persistence.SecondaryTables; -import jakarta.persistence.SharedCacheMode; -import jakarta.persistence.UniqueConstraint; import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; import org.hibernate.MappingException; @@ -56,6 +35,7 @@ import org.hibernate.annotations.ForeignKey; import org.hibernate.annotations.HQLSelect; import org.hibernate.annotations.Immutable; import org.hibernate.annotations.Loader; +import org.hibernate.annotations.Mutability; import org.hibernate.annotations.NaturalIdCache; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OptimisticLockType; @@ -125,6 +105,28 @@ import org.hibernate.spi.NavigablePath; import org.jboss.logging.Logger; +import jakarta.persistence.Access; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.AttributeOverrides; +import jakarta.persistence.Cacheable; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.IdClass; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.NamedEntityGraphs; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.PrimaryKeyJoinColumns; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.SecondaryTables; +import jakarta.persistence.SharedCacheMode; +import jakarta.persistence.UniqueConstraint; + import static org.hibernate.boot.model.internal.AnnotatedClassType.MAPPED_SUPERCLASS; import static org.hibernate.boot.model.internal.AnnotatedDiscriminatorColumn.buildDiscriminatorColumn; import static org.hibernate.boot.model.internal.AnnotatedJoinColumn.buildInheritanceJoinColumn; @@ -132,10 +134,10 @@ import static org.hibernate.boot.model.internal.BinderHelper.getMappedSuperclass import static org.hibernate.boot.model.internal.BinderHelper.getOverridableAnnotation; import static org.hibernate.boot.model.internal.BinderHelper.hasToOneAnnotation; import static org.hibernate.boot.model.internal.BinderHelper.isDefault; -import static org.hibernate.boot.model.internal.GeneratorBinder.makeIdGenerator; 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.EmbeddableBinder.fillEmbeddable; +import static org.hibernate.boot.model.internal.GeneratorBinder.makeIdGenerator; import static org.hibernate.boot.model.internal.HCANNHelper.findContainingAnnotations; import static org.hibernate.boot.model.internal.InheritanceState.getInheritanceStateOfSuperEntity; import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClass; @@ -1190,6 +1192,8 @@ public class EntityBinder { LOG.immutableAnnotationOnNonRoot( annotatedClass.getName() ); } + ensureNoMutabilityPlan(); + bindCustomPersister(); bindCustomSql(); bindSynchronize(); @@ -1200,6 +1204,12 @@ public class EntityBinder { processNamedEntityGraphs(); } + private void ensureNoMutabilityPlan() { + if ( annotatedClass.isAnnotationPresent( Mutability.class ) ) { + throw new MappingException( "@Mutability is not allowed on entity" ); + } + } + private boolean isMutable() { return !annotatedClass.isAnnotationPresent(Immutable.class); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/process/internal/InferredBasicValueResolver.java b/hibernate-core/src/main/java/org/hibernate/boot/model/process/internal/InferredBasicValueResolver.java index 6a0b2a3081..ed65e87351 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/process/internal/InferredBasicValueResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/process/internal/InferredBasicValueResolver.java @@ -8,29 +8,28 @@ package org.hibernate.boot.model.process.internal; import java.io.Serializable; import java.lang.reflect.Type; +import java.util.function.Function; import java.util.function.Supplier; -import jakarta.persistence.EnumType; -import jakarta.persistence.TemporalType; - import org.hibernate.MappingException; import org.hibernate.dialect.Dialect; import org.hibernate.mapping.BasicValue; import org.hibernate.mapping.Column; import org.hibernate.mapping.Selectable; import org.hibernate.mapping.Table; -import org.hibernate.type.descriptor.converter.internal.NamedEnumValueConverter; -import org.hibernate.type.descriptor.converter.internal.OrdinalEnumValueConverter; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.type.AdjustableBasicType; import org.hibernate.type.BasicType; import org.hibernate.type.CustomType; import org.hibernate.type.SerializableType; -import org.hibernate.type.descriptor.java.BasicPluralJavaType; +import org.hibernate.type.descriptor.converter.internal.NamedEnumValueConverter; +import org.hibernate.type.descriptor.converter.internal.OrdinalEnumValueConverter; import org.hibernate.type.descriptor.java.BasicJavaType; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.EnumJavaType; import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.MutabilityPlan; import org.hibernate.type.descriptor.java.SerializableJavaType; import org.hibernate.type.descriptor.java.TemporalJavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; @@ -38,6 +37,9 @@ import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.descriptor.jdbc.ObjectJdbcType; import org.hibernate.type.spi.TypeConfiguration; +import jakarta.persistence.EnumType; +import jakarta.persistence.TemporalType; + import static org.hibernate.type.SqlTypes.SMALLINT; import static org.hibernate.type.SqlTypes.TINYINT; @@ -52,6 +54,7 @@ public class InferredBasicValueResolver { JdbcType explicitJdbcType, Type resolvedJavaType, Supplier> reflectedJtdResolver, + Function explicitMutabilityPlanAccess, JdbcTypeIndicators stdIndicators, Table table, Selectable selectable, @@ -84,6 +87,7 @@ public class InferredBasicValueResolver { null, explicitJdbcType, resolvedJavaType, + explicitMutabilityPlanAccess, stdIndicators, typeConfiguration ); @@ -135,6 +139,7 @@ public class InferredBasicValueResolver { null, explicitJdbcType, resolvedJavaType, + explicitMutabilityPlanAccess, stdIndicators, typeConfiguration ); @@ -171,6 +176,7 @@ public class InferredBasicValueResolver { null, null, resolvedJavaType, + explicitMutabilityPlanAccess, stdIndicators, typeConfiguration ); @@ -296,7 +302,7 @@ public class InferredBasicValueResolver { jdbcMapping.getJavaTypeDescriptor(), jdbcMapping.getJdbcType(), jdbcMapping, - null + determineMutabilityPlan( explicitMutabilityPlanAccess, jdbcMapping.getJavaTypeDescriptor(), typeConfiguration ) ); } @@ -477,6 +483,7 @@ public class InferredBasicValueResolver { BasicJavaType explicitJavaType, JdbcType explicitJdbcType, Type resolvedJavaType, + Function explicitMutabilityPlanAccess, JdbcTypeIndicators stdIndicators, TypeConfiguration typeConfiguration) { final TemporalType requestedTemporalPrecision = stdIndicators.getTemporalPrecision(); @@ -509,13 +516,14 @@ public class InferredBasicValueResolver { final BasicType jdbcMapping = typeConfiguration.getBasicTypeRegistry().resolve( explicitTemporalJtd, jdbcType ); + final MutabilityPlan mutabilityPlan = determineMutabilityPlan( explicitMutabilityPlanAccess, explicitTemporalJtd, typeConfiguration ); return new InferredBasicValueResolution<>( jdbcMapping, explicitTemporalJtd, explicitTemporalJtd, jdbcType, jdbcMapping, - explicitTemporalJtd.getMutabilityPlan() + mutabilityPlan ); } @@ -575,8 +583,22 @@ public class InferredBasicValueResolver { basicType.getJavaTypeDescriptor(), basicType.getJdbcType(), basicType, - reflectedJtd.getMutabilityPlan() + determineMutabilityPlan( explicitMutabilityPlanAccess, reflectedJtd, typeConfiguration ) ); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static MutabilityPlan determineMutabilityPlan( + Function explicitMutabilityPlanAccess, + JavaType jtd, + TypeConfiguration typeConfiguration) { + if ( explicitMutabilityPlanAccess != null ) { + final MutabilityPlan mutabilityPlan = explicitMutabilityPlanAccess.apply( typeConfiguration ); + if ( mutabilityPlan != null ) { + return mutabilityPlan; + } + } + return jtd.getMutabilityPlan(); + } + } 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 ca29347a6b..66f15972bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -479,6 +479,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol jdbcType, resolvedJavaType, this::determineReflectedJavaType, + explicitMutabilityPlanAccess, this, getTable(), column, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index d0dc0f8acf..6dd44bb46a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -199,7 +199,7 @@ public class MappingModelCreationHelper { MappingModelCreationProcess creationProcess) { final SimpleValue value = (SimpleValue) bootProperty.getValue(); final BasicValue.Resolution resolution = ( (Resolvable) value ).resolve(); - SimpleAttributeMetadata attributeMetadata = new SimpleAttributeMetadata( propertyAccess, resolution.getMutabilityPlan(), bootProperty, value ); + final SimpleAttributeMetadata attributeMetadata = new SimpleAttributeMetadata( propertyAccess, resolution.getMutabilityPlan(), bootProperty, value ); final FetchTiming fetchTiming; final FetchStyle fetchStyle; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/Immutability.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/Immutability.java new file mode 100644 index 0000000000..45f3a1f4d4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/Immutability.java @@ -0,0 +1,52 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.type.descriptor.java; + +import java.io.Serializable; + +import org.hibernate.SharedSessionContract; +import org.hibernate.annotations.Mutability; + +/** + * Object-typed form of {@link ImmutableMutabilityPlan} for easier use + * with {@link Mutability} for users + * + * @see org.hibernate.annotations.Immutable + * + * @author Steve Ebersole + */ +public class Immutability implements MutabilityPlan { + /** + * Singleton access + */ + public static final Immutability INSTANCE = new Immutability(); + + public static MutabilityPlan instance() { + //noinspection unchecked + return (MutabilityPlan) INSTANCE; + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Object deepCopy(Object value) { + return value; + } + + @Override + public Serializable disassemble(Object value, SharedSessionContract session) { + return (Serializable) value; + } + + @Override + public Object assemble(Serializable cached, SharedSessionContract session) { + return cached; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ImmutableMutabilityPlan.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ImmutableMutabilityPlan.java index 365117f1ed..b475340471 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ImmutableMutabilityPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ImmutableMutabilityPlan.java @@ -13,6 +13,11 @@ import org.hibernate.SharedSessionContract; /** * Mutability plan for immutable objects * + * @apiNote For use with {@link org.hibernate.annotations.Mutability}, + * users should instead use {@link Immutability} as the type parameterization + * here does not work with the parameterization defined on + * {@link org.hibernate.annotations.Mutability#value} + * * @author Steve Ebersole */ public class ImmutableMutabilityPlan implements MutabilityPlan { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMapImmutableTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMapImmutableTests.java deleted file mode 100644 index 152487fe3a..0000000000 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMapImmutableTests.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. - */ -package org.hibernate.orm.test.mapping.converted.converter.mutabiity; - -import java.util.Map; - -import org.hibernate.annotations.Immutable; -import org.hibernate.internal.util.collections.CollectionHelper; - -import org.hibernate.testing.jdbc.SQLStatementInspector; -import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.FailureExpected; -import org.hibernate.testing.orm.junit.JiraKey; -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.BeforeEach; -import org.junit.jupiter.api.Test; - -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Ebersole - */ -@DomainModel( annotatedClasses = ConvertedMapImmutableTests.TestEntity.class ) -@SessionFactory( useCollectingStatementInspector = true ) -public class ConvertedMapImmutableTests { - - @Test - @JiraKey( "HHH-16081" ) - void testManagedUpdate(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - - scope.inTransaction( (session) -> { - final TestEntity loaded = session.get( TestEntity.class, 1 ); - loaded.values.put( "ghi", "789" ); - statementInspector.clear(); - } ); - - final TestEntity after = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( after.values ).hasSize( 2 ); - } - - @Test - @JiraKey( "HHH-16081" ) - @FailureExpected( reason = "Fails due to HHH-16132 - Hibernate believes the attribute is dirty, even though it is immutable." ) - void testMerge(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - - final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( loaded.values ).hasSize( 2 ); - - loaded.values.put( "ghi", "789" ); - statementInspector.clear(); - scope.inTransaction( (session) -> session.merge( loaded ) ); - - final TestEntity merged = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( merged.values ).hasSize( 2 ); - } - - @Test - @JiraKey( "HHH-16132" ) - @FailureExpected( reason = "Fails due to HHH-16132 - Hibernate believes the attribute is dirty, even though it is immutable." ) - void testDirtyChecking(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - - // make changes to a managed entity - should not trigger update since it is immutable - scope.inTransaction( (session) -> { - final TestEntity managed = session.get( TestEntity.class, 1 ); - statementInspector.clear(); - assertThat( managed.values ).hasSize( 2 ); - // make the change - managed.values.put( "ghi", "789" ); - } ); - assertThat( statementInspector.getSqlQueries() ).isEmpty(); - - // make no changes to a detached entity and merge it - should not trigger update - final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( loaded.values ).hasSize( 2 ); - // make the change - loaded.values.put( "ghi", "789" ); - statementInspector.clear(); - scope.inTransaction( (session) -> session.merge( loaded ) ); - // the SELECT - assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); - } - - @Test - @JiraKey( "HHH-16132" ) - void testNotDirtyChecking(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - - // make changes to a managed entity - should not trigger update - scope.inTransaction( (session) -> { - final TestEntity managed = session.get( TestEntity.class, 1 ); - statementInspector.clear(); - assertThat( managed.values ).hasSize( 2 ); - } ); - assertThat( statementInspector.getSqlQueries() ).isEmpty(); - - // make no changes to a detached entity and merge it - should not trigger update - final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( loaded.values ).hasSize( 2 ); - statementInspector.clear(); - scope.inTransaction( (session) -> session.merge( loaded ) ); - // the SELECT - assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); - } - - @BeforeEach - void createTestData(SessionFactoryScope scope) { - scope.inTransaction( (session) -> { - session.persist( new TestEntity( - 1, - CollectionHelper.toMap( - "abc", "123", - "def", "456" - ) - ) ); - } ); - } - - @AfterEach - void dropTestData(SessionFactoryScope scope) { - scope.inTransaction( (session) -> { - session.createMutationQuery( "delete TestEntity" ).executeUpdate(); - } ); - } - - @Immutable - public static class ImmutableMapConverter extends ConvertedMapMutableTests.MapConverter { - } - - @Entity( name = "TestEntity" ) - @Table( name = "entity_immutable_map" ) - public static class TestEntity { - @Id - private Integer id; - - @Convert( converter = ImmutableMapConverter.class ) - @Column( name="vals" ) - private Map values; - - private TestEntity() { - // for use by Hibernate - } - - public TestEntity( - Integer id, - Map values) { - this.id = id; - this.values = values; - } - } -} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMutabilityTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMutabilityTests.java deleted file mode 100644 index 70b70706c3..0000000000 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMutabilityTests.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. - */ -package org.hibernate.orm.test.mapping.converted.converter.mutabiity; - -import java.time.Instant; -import java.time.format.DateTimeFormatter; -import java.util.Date; - -import org.hibernate.annotations.Immutable; -import org.hibernate.internal.util.StringHelper; - -import org.hibernate.testing.jdbc.SQLStatementInspector; -import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.JiraKey; -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.BeforeEach; -import org.junit.jupiter.api.Test; - -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Convert; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Ebersole - */ -@JiraKey( "HHH-16081" ) -@DomainModel( annotatedClasses = ConvertedMutabilityTests.TestEntityWithDates.class ) -@SessionFactory( useCollectingStatementInspector = true ) -public class ConvertedMutabilityTests { - private static final Instant START = Instant.now(); - - @Test - void testImmutableDate(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - - scope.inTransaction( (session) -> { - final TestEntityWithDates loaded = session.get( TestEntityWithDates.class, 1 ); - - statementInspector.clear(); - - // change `d2` - because it is immutable, this should not trigger an update - loaded.d2.setTime( Instant.EPOCH.toEpochMilli() ); - } ); - - assertThat( statementInspector.getSqlQueries() ).isEmpty(); - - scope.inTransaction( (session) -> { - final TestEntityWithDates loaded = session.get( TestEntityWithDates.class, 1 ); - assertThat( loaded.d1.getTime() ).isEqualTo( START.toEpochMilli() ); - } ); - } - - @Test - void testMutableDate(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - - scope.inTransaction( (session) -> { - final TestEntityWithDates loaded = session.get( TestEntityWithDates.class, 1 ); - - statementInspector.clear(); - - // change `d1` - because it is mutable, this should trigger an update - loaded.d1.setTime( Instant.EPOCH.toEpochMilli() ); - } ); - - assertThat( statementInspector.getSqlQueries() ).isNotEmpty(); - - scope.inTransaction( (session) -> { - final TestEntityWithDates loaded = session.get( TestEntityWithDates.class, 1 ); - assertThat( loaded.d1.getTime() ).isEqualTo( Instant.EPOCH.toEpochMilli() ); - } ); - } - - @Test - void testDatesWithMerge(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - final TestEntityWithDates loaded = scope.fromTransaction( (session) -> session.get( TestEntityWithDates.class, 1 ) ); - - loaded.d1.setTime( Instant.EPOCH.toEpochMilli() ); - - statementInspector.clear(); - scope.inTransaction( (session) -> session.merge( loaded ) ); - assertThat( statementInspector.getSqlQueries() ).isNotEmpty(); - - final TestEntityWithDates loaded2 = scope.fromTransaction( (session) -> session.get( TestEntityWithDates.class, 1 ) ); - assertThat( loaded2.d1.getTime() ).isEqualTo( Instant.EPOCH.toEpochMilli() ); - - loaded2.d2.setTime( Instant.EPOCH.toEpochMilli() ); - statementInspector.clear(); - scope.inTransaction( (session) -> session.merge( loaded ) ); - assertThat( statementInspector.getSqlQueries() ).isNotEmpty(); - - final TestEntityWithDates loaded3 = scope.fromTransaction( (session) -> session.get( TestEntityWithDates.class, 1 ) ); - assertThat( loaded3.d2.getTime() ).isEqualTo( START.toEpochMilli() ); - } - - @BeforeEach - void createTestData(SessionFactoryScope scope) { - scope.inTransaction( (session) -> { - session.persist( new TestEntityWithDates( - 1, - Date.from( START ), - Date.from( START ) - ) ); - } ); - } - - @AfterEach - void dropTestData(SessionFactoryScope scope) { - scope.inTransaction( (session) -> { - session.createMutationQuery( "delete TestEntityWithDates" ).executeUpdate(); - } ); - } - - public static class DateConverter implements AttributeConverter { - @Override - public String convertToDatabaseColumn(Date date) { - if ( date == null ) { - return null; - } - return DateTimeFormatter.ISO_INSTANT.format( date.toInstant() ); - } - - @Override - public Date convertToEntityAttribute(String date) { - if ( StringHelper.isEmpty( date ) ) { - return null; - } - return Date.from( Instant.from( DateTimeFormatter.ISO_INSTANT.parse( date ) ) ); - } - } - - @Immutable - public static class ImmutableDateConverter implements AttributeConverter { - @Override - public String convertToDatabaseColumn(Date date) { - if ( date == null ) { - return null; - } - return DateTimeFormatter.ISO_INSTANT.format( date.toInstant() ); - } - - @Override - public Date convertToEntityAttribute(String date) { - if ( StringHelper.isEmpty( date ) ) { - return null; - } - return Date.from( Instant.from( DateTimeFormatter.ISO_INSTANT.parse( date ) ) ); - } - } - - - @Entity( name = "TestEntityWithDates" ) - @Table( name = "entity_dates" ) - public static class TestEntityWithDates { - @Id - private Integer id; - - @Convert( converter = DateConverter.class ) - private Date d1; - @Convert( converter = ImmutableDateConverter.class ) - private Date d2; - - private TestEntityWithDates() { - // for use by Hibernate - } - - public TestEntityWithDates( - Integer id, - Date d1, - Date d2) { - this.id = id; - this.d1 = d1; - this.d2 = d2; - } - } - -} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/MutabilityBaselineEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/MutabilityBaselineEntity.java new file mode 100644 index 0000000000..b12e947413 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/MutabilityBaselineEntity.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability; + +import java.time.Instant; +import java.util.Date; + +import org.hibernate.Session; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * Used as example entity in UG + * + * @author Steve Ebersole + */ +//tag::mutability-base-entity-example[] +@Entity +public class MutabilityBaselineEntity { + @Id + private Integer id; + @Basic + private String name; + @Basic + private Date activeTimestamp; +//end::mutability-base-entity-example[] + + private MutabilityBaselineEntity() { + // for Hibernate use + } + + public MutabilityBaselineEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getActiveTimestamp() { + return activeTimestamp; + } + + public void setActiveTimestamp(Date activeTimestamp) { + this.activeTimestamp = activeTimestamp; + } + + void stringExample() { + //tag::mutability-base-string-example[] + Session session = getSession(); + MutabilityBaselineEntity entity = session.find( MutabilityBaselineEntity.class, 1 ); + entity.setName( "new name" ); + //end::mutability-base-string-example[] + } + + private void dateExampleSet() { + //tag::mutability-base-date-set-example[] + Session session = getSession(); + MutabilityBaselineEntity entity = session.find( MutabilityBaselineEntity.class, 1 ); + entity.setActiveTimestamp( now() ); + //end::mutability-base-date-set-example[] + } + + private void dateExampleMutate() { + //tag::mutability-base-date-mutate-example[] + Session session = getSession(); + MutabilityBaselineEntity entity = session.find( MutabilityBaselineEntity.class, 1 ); + entity.getActiveTimestamp().setTime( now().getTime() ); + //end::mutability-base-date-mutate-example[] + } + + private Session getSession() { + return null; + } + + private Date now() { + return Date.from( Instant.now() ); + } + +//tag::mutability-base-entity-example[] +} +//end::mutability-base-entity-example[] + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/BasicAttributeMutabilityTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/BasicAttributeMutabilityTests.java new file mode 100644 index 0000000000..c803d58d68 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/BasicAttributeMutabilityTests.java @@ -0,0 +1,260 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.attribute; + +import java.time.Instant; +import java.util.Date; + +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Mutability; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.descriptor.java.Immutability; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +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 jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tested premises: + * + * 1. `@Immutable` on a basic attribute - + * * not-updateable + * * immutable + * 2. `@Mutability(Immutability.class)` on basic attribute + * * updateable + * * immutable + * + * @author Steve Ebersole + */ +@DomainModel( annotatedClasses = BasicAttributeMutabilityTests.TheEntity.class ) +@SessionFactory +public class BasicAttributeMutabilityTests { + private static final Instant START = Instant.now(); + + @Test + public void verifyDomainModel(DomainModelScope domainModelScope, SessionFactoryScope sfSessionFactoryScope) { + final PersistentClass persistentClass = domainModelScope + .getDomainModel() + .getEntityBinding( TheEntity.class.getName() ); + final EntityPersister entityDescriptor = sfSessionFactoryScope.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( TheEntity.class ); + + // `@Immutable` + final Property theDateProperty = persistentClass.getProperty( "theDate" ); + assertThat( theDateProperty.isUpdateable() ).isFalse(); + final AttributeMapping theDateAttribute = entityDescriptor.findAttributeMapping( "theDate" ); + assertThat( theDateAttribute.getExposedMutabilityPlan().isMutable() ).isFalse(); + + // `@Mutability(Immutability.class)` + final Property anotherDateProperty = persistentClass.getProperty( "anotherDate" ); + assertThat( anotherDateProperty.isUpdateable() ).isTrue(); + final AttributeMapping anotherDateAttribute = entityDescriptor.findAttributeMapping( "anotherDate" ); + assertThat( anotherDateAttribute.getExposedMutabilityPlan().isMutable() ).isFalse(); + } + + /** + * `@Immutable` attribute while managed - no update + */ + @Test + public void testImmutableManaged(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "@Immutable test", Date.from( START ) ) ); + } ); + + // try to update the managed form + scope.inTransaction( (session) -> { + //tag::attribute-immutable-managed-example[] + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + // this change will be ignored + theEntity.theDate.setTime( Instant.EPOCH.toEpochMilli() ); + //end::attribute-immutable-managed-example[] + } ); + + // verify the value did not change + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.theDate.getTime() ).isEqualTo( START.toEpochMilli() ); + } ); + } + + /** + * `@Immutable` attribute on merged detached value - no update (its non-updateable) + */ + @Test + public void testImmutableDetached(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "@Immutable test", Date.from( START ) ) ); + } ); + + // load a detached reference + final TheEntity detached = scope.fromTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.theDate.getTime() ).isEqualTo( START.toEpochMilli() ); + return theEntity; + } ); + + // make the change again, this time to a detached instance and merge it. + //tag::attribute-immutable-merge-example[] + detached.theDate.setTime( Instant.EPOCH.toEpochMilli() ); + scope.inTransaction( (session) -> session.merge( detached ) ); + //end::attribute-immutable-merge-example[] + + // verify the value did not change via the merge + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.theDate.getTime() ).isEqualTo( START.toEpochMilli() ); + } ); + } + + /** + * `@Mutability(Immutability.class)` attribute while managed - no update + */ + @Test + public void testImmutabilityManaged(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "@Mutability test", Date.from( START ) ) ); + } ); + + // try to update the managed form + scope.inTransaction( (session) -> { + //tag::attribute-immutability-managed-example[] + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + theEntity.anotherDate.setTime( Instant.EPOCH.toEpochMilli() ); + //end::attribute-immutability-managed-example[] + } ); + + // reload it and verify the value did not change + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.anotherDate.getTime() ).isEqualTo( START.toEpochMilli() ); + } ); + } + + /** + * `@Mutability(Immutability.class)` attribute while managed - update because + * we cannot distinguish one type of change from another while detached + */ + @Test + public void testImmutabilityDetached(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "@Mutability test", Date.from( START ) ) ); + } ); + + // load a detached reference + final TheEntity detached = scope.fromTransaction( (session) -> session.find( TheEntity.class, 1 ) ); + + //tag::attribute-immutability-merge-example[] + detached.anotherDate.setTime( Instant.EPOCH.toEpochMilli() ); + scope.inTransaction( (session) -> session.merge( detached ) ); + //end::attribute-immutability-merge-example[] + + // verify the change was persisted + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.anotherDate.getTime() ).isEqualTo( Instant.EPOCH.toEpochMilli() ); + } ); + } + + /** + * Normal mutable value while managed + */ + @Test + public void testMutableManaged(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "Baseline mutable test", Date.from( START ) ) ); + } ); + + // try to mutate the managed form + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + theEntity.mutableDate.setTime( Instant.EPOCH.toEpochMilli() ); + } ); + + // verify the value did change + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.mutableDate.getTime() ).isEqualTo( Instant.EPOCH.toEpochMilli() ); + } ); + } + + /** + * Normal mutable value while detached + */ + @Test + public void testMutableMerge(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "Baseline mutable test", Date.from( START ) ) ); + } ); + + // load a detached reference + final TheEntity detached = scope.fromTransaction( (session) -> session.find( TheEntity.class, 1 ) ); + + // now mutate the detached state and merge + detached.mutableDate.setTime( START.toEpochMilli() ); + scope.inTransaction( (session) -> session.merge( detached ) ); + + // verify the value did change + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.mutableDate.getTime() ).isEqualTo( START.toEpochMilli() ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.createMutationQuery( "delete TheEntity" ).executeUpdate() ); + } + + @Entity( name = "TheEntity" ) + @Table( name = "TheEntity" ) + public static class TheEntity { + @Id + private Integer id; + + @Basic + private String name; + + //tag::attribute-immutable-example[] + @Immutable + private Date theDate; + //end::attribute-immutable-example[] + + //tag::attribute-immutability-example[] + @Mutability(Immutability.class) + private Date anotherDate; + //end::attribute-immutability-example[] + + private Date mutableDate; + + private TheEntity() { + // for use by Hibernate + } + + public TheEntity(Integer id, String name, Date aDate) { + this.id = id; + this.name = name; + this.theDate = aDate; + this.anotherDate = aDate; + this.mutableDate = aDate; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/EntityAttributeMutabilityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/EntityAttributeMutabilityTest.java new file mode 100644 index 0000000000..634c17d306 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/EntityAttributeMutabilityTest.java @@ -0,0 +1,184 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.attribute; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Mutability; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.descriptor.java.Immutability; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Basic; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@DomainModel( annotatedClasses = EntityAttributeMutabilityTest.Employee.class ) +@SessionFactory +public class EntityAttributeMutabilityTest { + + @Test + public void verifyMetamodel(DomainModelScope domainModelScope, SessionFactoryScope sessionFactoryScope) { + final PersistentClass persistentClass = domainModelScope.getEntityBinding( Employee.class ); + final EntityPersister entityDescriptor = sessionFactoryScope.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( Employee.class ); + + // `@Immutable` + final Property managerProperty = persistentClass.getProperty( "manager" ); + assertThat( managerProperty.isUpdateable() ).isFalse(); + final AttributeMapping managerAttribute = entityDescriptor.findAttributeMapping( "manager" ); + assertThat( managerAttribute.getExposedMutabilityPlan().isMutable() ).isFalse(); + + // `@Mutability(Immutability.class)` - no effect + final Property manager2Property = persistentClass.getProperty( "manager2" ); + assertThat( manager2Property.isUpdateable() ).isTrue(); + final AttributeMapping manager2Attribute = entityDescriptor.findAttributeMapping( "manager2" ); + assertThat( manager2Attribute.getExposedMutabilityPlan().isMutable() ).isTrue(); + } + + @Entity( name = "Employee" ) + @Table( name = "Employee" ) + public static class Employee { + @Id + private Integer id; + @Basic + private String name; + @ManyToOne + @JoinColumn( name = "manager_fk" ) + @Immutable + private Employee manager; + @ManyToOne + @JoinColumn( name = "manager2_fk" ) + @Mutability(Immutability.class) + private Employee manager2; + + private Employee() { + // for use by Hibernate + } + + public Employee(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + //tag::collection-immutability-example[] + @Entity(name = "Batch") + public static class Batch { + + @Id + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.ALL) + @Immutable + private List events = new ArrayList<>(); + + //Getters and setters are omitted for brevity + + //end::collection-immutability-example[] + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getEvents() { + return events; + } + //tag::collection-immutability-example[] + } + + @Entity(name = "Event") + @Immutable + public static class Event { + + @Id + private Long id; + + private Date createdOn; + + private String message; + + //Getters and setters are omitted for brevity + + //end::collection-immutability-example[] + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + //tag::collection-immutability-example[] + } + //end::collection-immutability-example[] +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/ImmutabilityMapAsBasicTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/ImmutabilityMapAsBasicTests.java new file mode 100644 index 0000000000..be2417ef8d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/ImmutabilityMapAsBasicTests.java @@ -0,0 +1,193 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.attribute; + +import java.util.Map; + +import org.hibernate.annotations.Mutability; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.orm.test.mapping.mutability.converted.MapConverter; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.descriptor.java.Immutability; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.JiraKey; +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.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @implNote Uses a converter just for helping with the Map part. It is the + * {@link Mutability} usage that is important + * + * @author Steve Ebersole + */ +@JiraKey( "HHH-16081" ) +@DomainModel( annotatedClasses = ImmutabilityMapAsBasicTests.TestEntity.class ) +@SessionFactory( useCollectingStatementInspector = true ) +public class ImmutabilityMapAsBasicTests { + @Test + void verifyMetamodel(DomainModelScope domainModelScope, SessionFactoryScope sessionFactoryScope) { + domainModelScope.withHierarchy( TestEntity.class, (entity) -> { + final Property property = entity.getProperty( "data" ); + assertThat( property.isUpdateable() ).isTrue(); + + final BasicValue value = (BasicValue) property.getValue(); + final BasicValue.Resolution resolution = value.resolve(); + assertThat( resolution.getMutabilityPlan().isMutable() ).isFalse(); + } ); + + final MappingMetamodelImplementor mappingMetamodel = sessionFactoryScope.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + final EntityPersister entityDescriptor = mappingMetamodel.getEntityDescriptor( TestEntity.class ); + final AttributeMapping attribute = entityDescriptor.findAttributeMapping( "data" ); + assertThat( attribute.getAttributeMetadata().isUpdatable() ).isTrue(); + assertThat( attribute.getExposedMutabilityPlan().isMutable() ).isFalse(); + } + + @Test + @JiraKey( "HHH-16132" ) + void testDirtyCheckingManaged(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + // mutate the managed entity state - should not trigger update since it is immutable + scope.inTransaction( (session) -> { + // load a managed reference + final TestEntity managed = session.get( TestEntity.class, 1 ); + assertThat( managed.data ).hasSize( 2 ); + + // make the change + managed.data.put( "ghi", "789" ); + + // clear statements prior to flush + statementInspector.clear(); + } ); + + assertThat( statementInspector.getSqlQueries() ).isEmpty(); + } + + /** + * Illustrates how we can't really detect that an "internal state" mutation + * happened while detached. When merged, we just see a different value. + */ + @Test + @JiraKey( "HHH-16132" ) + void testDirtyCheckingMerge(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + // load a detached reference + final TestEntity detached = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); + assertThat( detached.data ).hasSize( 2 ); + + // make the change + detached.data.put( "jkl", "007" ); + + // clear statements prior to merge + statementInspector.clear(); + + // do the merge + scope.inTransaction( (session) -> session.merge( detached ) ); + + // the SELECT + UPDATE + assertThat( statementInspector.getSqlQueries() ).hasSize( 2 ); + } + + @Test + @JiraKey( "HHH-16132" ) + void testNotDirtyCheckingManaged(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + // make no changes to a managed entity + scope.inTransaction( (session) -> { + // Load a managed reference + final TestEntity managed = session.get( TestEntity.class, 1 ); + assertThat( managed.data ).hasSize( 2 ); + + // make no changes + + // clear statements in prep for next check + statementInspector.clear(); + } ); + + // because we made no changes, there should be no update + assertThat( statementInspector.getSqlQueries() ).isEmpty(); + } + + @Test + @JiraKey( "HHH-16132" ) + void testNotDirtyCheckingMerge(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + // load a detached instance + final TestEntity detached = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); + assertThat( detached.data ).hasSize( 2 ); + + // clear statements in prep for next check + statementInspector.clear(); + + // merge the detached reference without making any changes + scope.inTransaction( (session) -> session.merge( detached ) ); + // (the SELECT) - should not trigger the UPDATE + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + } + + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TestEntity( + 1, + CollectionHelper.toMap( + "abc", "123", + "def", "456" + ) + ) ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createMutationQuery( "delete TestEntity" ).executeUpdate(); + } ); + } + + @Entity( name = "TestEntity" ) + @Table( name = "entity_mutable_map" ) + public static class TestEntity { + @Id + private Integer id; + + @Mutability(Immutability.class) + @Convert( converter = MapConverter.class ) + private Map data; + + private TestEntity() { + // for use by Hibernate + } + + public TestEntity(Integer id, Map data) { + this.id = id; + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/ImmutableMapAsBasicTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/ImmutableMapAsBasicTests.java new file mode 100644 index 0000000000..61ec7bb3ee --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/ImmutableMapAsBasicTests.java @@ -0,0 +1,210 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.attribute; + +import java.util.Map; + +import org.hibernate.annotations.Immutable; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.orm.test.mapping.mutability.converted.MapConverter; +import org.hibernate.persister.entity.EntityPersister; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.JiraKey; +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.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for `@Immutable` on a map-as-basic attribute + * + * Essentially the same as the `@Immutable` checks in {@link BasicAttributeMutabilityTests} in + * {@link BasicAttributeMutabilityTests#testImmutableManaged}, + * {@link BasicAttributeMutabilityTests#testImmutableDetached} and + * {@link BasicAttributeMutabilityTests#verifyDomainModel}. + * + * Again, `@Immutable` means: + * * not-updateable + * * but is oddly mutable + * + * @implNote Uses a converter just for helping with the Map part. It is the + * {@link Immutable} usage that is important + * + * @author Steve Ebersole + */ +@JiraKey( "HHH-16081" ) +@DomainModel( annotatedClasses = ImmutableMapAsBasicTests.TestEntity.class ) +@SessionFactory( useCollectingStatementInspector = true ) +public class ImmutableMapAsBasicTests { + @Test + void verifyMetamodel(DomainModelScope domainModelScope, SessionFactoryScope sessionFactoryScope) { + final PersistentClass persistentClass = domainModelScope + .getDomainModel() + .getEntityBinding( TestEntity.class.getName() ); + final EntityPersister entityDescriptor = sessionFactoryScope + .getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( TestEntity.class ); + + final Property property = persistentClass.getProperty( "data" ); + assertThat( property.isUpdateable() ).isFalse(); + + final BasicValue value = (BasicValue) property.getValue(); + final BasicValue.Resolution resolution = value.resolve(); + assertThat( resolution.getMutabilityPlan().isMutable() ).isFalse(); + + final AttributeMapping attribute = entityDescriptor.findAttributeMapping( "data" ); + assertThat( attribute.getAttributeMetadata().isUpdatable() ).isFalse(); + assertThat( attribute.getExposedMutabilityPlan().isMutable() ).isFalse(); + } + + /** + * Because `@Immutable` implies non-updateable, changes are ignored (no UPDATE) + */ + @Test + @JiraKey( "HHH-16132" ) + void testDirtyCheckingManaged(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + // mutate the managed entity state + scope.inTransaction( (session) -> { + // load a managed reference + final TestEntity managed = session.get( TestEntity.class, 1 ); + assertThat( managed.data ).hasSize( 2 ); + + // make the change + managed.data.put( "ghi", "789" ); + + // clear statements prior to flush + statementInspector.clear(); + } ); + + // there should be no UPDATE + assertThat( statementInspector.getSqlQueries() ).isEmpty(); + } + + /** + * Because `@Immutable` implies non-updateable, changes are ignored (no UPDATE) + */ + @Test + @JiraKey( "HHH-16132" ) + void testDirtyCheckingMerge(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + // load a detached reference + final TestEntity detached = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); + assertThat( detached.data ).hasSize( 2 ); + + // make the change + detached.data.put( "jkl", "007" ); + + // clear statements prior to merge + statementInspector.clear(); + + // do the merge + scope.inTransaction( (session) -> session.merge( detached ) ); + + // the SELECT - no UPDATE + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( "update" ); + } + + @Test + @JiraKey( "HHH-16132" ) + void testNotDirtyCheckingManaged(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + // make no changes to a managed entity + scope.inTransaction( (session) -> { + // Load a managed reference + final TestEntity managed = session.get( TestEntity.class, 1 ); + assertThat( managed.data ).hasSize( 2 ); + + // make no changes + + // clear statements in prep for next check + statementInspector.clear(); + } ); + + // because we made no changes, there should be no update + assertThat( statementInspector.getSqlQueries() ).isEmpty(); + } + + @Test + @JiraKey( "HHH-16132" ) + void testNotDirtyCheckingMerge(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + // load a detached instance + final TestEntity detached = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); + assertThat( detached.data ).hasSize( 2 ); + + // clear statements in prep for next check + statementInspector.clear(); + + // merge the detached reference without making any changes + scope.inTransaction( (session) -> session.merge( detached ) ); + // (the SELECT) - should not trigger the UPDATE + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + } + + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TestEntity( + 1, + CollectionHelper.toMap( + "abc", "123", + "def", "456" + ) + ) ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createMutationQuery( "delete TestEntity" ).executeUpdate(); + } ); + } + + @Entity( name = "TestEntity" ) + @Table( name = "entity_mutable_map" ) + public static class TestEntity { + @Id + private Integer id; + + @Immutable + @Convert( converter = MapConverter.class ) + private Map data; + + private TestEntity() { + // for use by Hibernate + } + + public TestEntity(Integer id, Map data) { + this.id = id; + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMapMutableTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/MutableMapAsBasicTests.java similarity index 57% rename from hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMapMutableTests.java rename to hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/MutableMapAsBasicTests.java index 22cc1f6e93..c03187ee8e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/mutabiity/ConvertedMapMutableTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/MutableMapAsBasicTests.java @@ -4,12 +4,12 @@ * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. */ -package org.hibernate.orm.test.mapping.converted.converter.mutabiity; +package org.hibernate.orm.test.mapping.mutability.attribute; import java.util.Map; -import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.orm.test.mapping.mutability.converted.MapConverter; import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; @@ -20,8 +20,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -33,41 +31,9 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Steve Ebersole */ @JiraKey( "HHH-16081" ) -@DomainModel( annotatedClasses = ConvertedMapMutableTests.TestEntity.class ) +@DomainModel( annotatedClasses = MutableMapAsBasicTests.TestEntity.class ) @SessionFactory( useCollectingStatementInspector = true ) -public class ConvertedMapMutableTests { - - @Test - void testMutableMap(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - - scope.inTransaction( (session) -> { - final TestEntity loaded = session.get( TestEntity.class, 1 ); - assertThat( loaded.values ).hasSize( 2 ); - loaded.values.put( "ghi", "789" ); - statementInspector.clear(); - } ); - assertThat( statementInspector.getSqlQueries() ).isNotEmpty(); - - scope.inTransaction( (session) -> { - final TestEntity loaded = session.get( TestEntity.class, 1 ); - assertThat( loaded.values ).hasSize( 3 ); - statementInspector.clear(); - } ); - assertThat( statementInspector.getSqlQueries() ).isEmpty(); - } - - @Test - void testMutableMapWithMerge(SessionFactoryScope scope) { - final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( loaded.values ).hasSize( 2 ); - - loaded.values.put( "ghi", "789" ); - scope.inTransaction( (session) -> session.merge( loaded ) ); - - final TestEntity changed = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( changed.values ).hasSize( 3 ); - } +public class MutableMapAsBasicTests { @Test @JiraKey( "HHH-16132" ) @@ -78,17 +44,17 @@ public class ConvertedMapMutableTests { scope.inTransaction( (session) -> { final TestEntity managed = session.get( TestEntity.class, 1 ); statementInspector.clear(); - assertThat( managed.values ).hasSize( 2 ); + assertThat( managed.data ).hasSize( 2 ); // make the change - managed.values.put( "ghi", "789" ); + managed.data.put( "ghi", "789" ); } ); assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); // make changes to a detached entity and merge it - should trigger update final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( loaded.values ).hasSize( 3 ); + assertThat( loaded.data ).hasSize( 3 ); // make the change - loaded.values.put( "jkl", "007" ); + loaded.data.put( "jkl", "007" ); statementInspector.clear(); scope.inTransaction( (session) -> session.merge( loaded ) ); // the SELECT + UPDATE @@ -104,13 +70,13 @@ public class ConvertedMapMutableTests { scope.inTransaction( (session) -> { final TestEntity managed = session.get( TestEntity.class, 1 ); statementInspector.clear(); - assertThat( managed.values ).hasSize( 2 ); + assertThat( managed.data ).hasSize( 2 ); } ); assertThat( statementInspector.getSqlQueries() ).isEmpty(); // make no changes to a detached entity and merge it - should not trigger update final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) ); - assertThat( loaded.values ).hasSize( 2 ); + assertThat( loaded.data ).hasSize( 2 ); statementInspector.clear(); scope.inTransaction( (session) -> session.merge( loaded ) ); // the SELECT @@ -137,43 +103,22 @@ public class ConvertedMapMutableTests { } ); } - public static class MapConverter implements AttributeConverter,String> { - @Override - public String convertToDatabaseColumn(Map map) { - if ( CollectionHelper.isEmpty( map ) ) { - return null; - } - return StringHelper.join( ", ", CollectionHelper.asPairs( map ) ); - } - - @Override - public Map convertToEntityAttribute(String pairs) { - if ( StringHelper.isEmpty( pairs ) ) { - return null; - } - return CollectionHelper.toMap( StringHelper.split( ", ", pairs ) ); - } - } - @Entity( name = "TestEntity" ) @Table( name = "entity_mutable_map" ) public static class TestEntity { - @Id - private Integer id; + @Id + private Integer id; @Convert( converter = MapConverter.class ) - @Column( name = "vals" ) - private Map values; + private Map data; private TestEntity() { // for use by Hibernate } - public TestEntity( - Integer id, - Map values) { + public TestEntity(Integer id, Map data) { this.id = id; - this.values = values; + this.data = data; } } } diff --git a/documentation/src/test/java/org/hibernate/userguide/immutability/CollectionImmutabilityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/PluralAttributeMutabilityTest.java similarity index 66% rename from documentation/src/test/java/org/hibernate/userguide/immutability/CollectionImmutabilityTest.java rename to hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/PluralAttributeMutabilityTest.java index 9cda8af61f..84b260fa12 100644 --- a/documentation/src/test/java/org/hibernate/userguide/immutability/CollectionImmutabilityTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/attribute/PluralAttributeMutabilityTest.java @@ -2,42 +2,43 @@ * Hibernate, Relational Persistence for Idiomatic Java * * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. */ -package org.hibernate.userguide.immutability; +package org.hibernate.orm.test.mapping.mutability.attribute; import java.util.ArrayList; import java.util.Date; import java.util.List; + +import org.hibernate.annotations.Immutable; + +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.Test; + +import org.jboss.logging.Logger; + import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; -import org.hibernate.annotations.Immutable; -import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; - -import org.junit.Test; - -import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; - /** * @author Vlad Mihalcea */ -public class CollectionImmutabilityTest extends BaseEntityManagerFunctionalTestCase { - - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { - Batch.class, - Event.class - }; - } +@DomainModel( annotatedClasses = { + PluralAttributeMutabilityTest.Batch.class, + PluralAttributeMutabilityTest.Event.class +} ) +@SessionFactory +public class PluralAttributeMutabilityTest { + private static final Logger log = Logger.getLogger( PluralAttributeMutabilityTest.class ); @Test - public void test() { - //tag::collection-immutability-persist-example[] - doInJPA(this::entityManagerFactory, entityManager -> { + public void test(SessionFactoryScope scope) { + scope.inTransaction( (entityManager) -> { + //tag::collection-immutability-persist-example[] Batch batch = new Batch(); batch.setId(1L); batch.setName("Change request"); @@ -56,21 +57,27 @@ public class CollectionImmutabilityTest extends BaseEntityManagerFunctionalTestC batch.getEvents().add(event2); entityManager.persist(batch); - }); - //end::collection-immutability-persist-example[] - //tag::collection-entity-update-example[] - doInJPA(this::entityManagerFactory, entityManager -> { + //end::collection-immutability-persist-example[] + } ); + + scope.inTransaction( (entityManager) -> { + //tag::collection-entity-update-example[] Batch batch = entityManager.find(Batch.class, 1L); log.info("Change batch name"); batch.setName("Proposed change request"); - }); - //end::collection-entity-update-example[] + //end::collection-entity-update-example[] + } ); + //tag::collection-immutability-update-example[] try { - doInJPA(this::entityManagerFactory, entityManager -> { - Batch batch = entityManager.find(Batch.class, 1L); + //end::collection-immutability-update-example[] + scope.inTransaction( (entityManager) -> { + //tag::collection-immutability-update-example[] + Batch batch = entityManager.find( Batch.class, 1L ); batch.getEvents().clear(); - }); + //end::collection-immutability-update-example[] + } ); + //tag::collection-immutability-update-example[] } catch (Exception e) { log.error("Immutable collections cannot be modified"); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/DateConverter.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/DateConverter.java new file mode 100644 index 0000000000..806a9c8366 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/DateConverter.java @@ -0,0 +1,38 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +import org.hibernate.internal.util.StringHelper; + +import jakarta.persistence.AttributeConverter; + +/** + * Handles Date as a character data on the database + * + * @author Steve Ebersole + */ +public class DateConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(Date date) { + if ( date == null ) { + return null; + } + return DateTimeFormatter.ISO_INSTANT.format( date.toInstant() ); + } + + @Override + public Date convertToEntityAttribute(String date) { + if ( StringHelper.isEmpty( date ) ) { + return null; + } + return Date.from( Instant.from( DateTimeFormatter.ISO_INSTANT.parse( date ) ) ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityConverterTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityConverterTests.java new file mode 100644 index 0000000000..754b7a9879 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityConverterTests.java @@ -0,0 +1,85 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import java.util.Date; + +import org.hibernate.annotations.Mutability; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Property; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Basic; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests applying {@link Mutability} to an {@link jakarta.persistence.AttributeConverter}. + * + * Here we just verify that has the same effect as {@link Mutability} directly on the attribute + * in terms of configuring the boot model references. + * + * @see ImmutableConvertedBaselineTests + * + * @author Steve Ebersole + */ +@ServiceRegistry +@DomainModel( annotatedClasses = ImmutabilityConverterTests.TestEntity.class ) +public class ImmutabilityConverterTests { + @Test + void verifyMetamodel(DomainModelScope scope) { + scope.withHierarchy( TestEntity.class, (entity) -> { + final Property theDateProperty = entity.getProperty( "theDate" ); + assertThat( theDateProperty ).isNotNull(); + assertThat( theDateProperty.isUpdateable() ).isTrue(); + + final BasicValue basicValue = (BasicValue) theDateProperty.getValue(); + final BasicValue.Resolution resolution = basicValue.resolve(); + assertThat( resolution.getMutabilityPlan().isMutable() ).isFalse(); + } ); + } + + @Entity( name = "TestEntity" ) + @Table( name = "TestEntity" ) + public static class TestEntity { + @Id + private Integer id; + @Basic + private String name; + @Convert( converter = ImmutabilityDateConverter.class ) + private Date theDate; + + private TestEntity() { + // for use by Hibernate + } + + public TestEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityDateConverter.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityDateConverter.java new file mode 100644 index 0000000000..4b67f4da43 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityDateConverter.java @@ -0,0 +1,17 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import org.hibernate.annotations.Mutability; +import org.hibernate.type.descriptor.java.Immutability; + +/** + * @author Steve Ebersole + */ +@Mutability(Immutability.class) +public class ImmutabilityDateConverter extends DateConverter { +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityMapConverter.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityMapConverter.java new file mode 100644 index 0000000000..a44fa232df --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutabilityMapConverter.java @@ -0,0 +1,17 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import org.hibernate.annotations.Mutability; +import org.hibernate.type.descriptor.java.Immutability; + +/** + * @author Steve Ebersole + */ +@Mutability(Immutability.class) +public class ImmutabilityMapConverter extends MapConverter { +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableConvertedBaselineTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableConvertedBaselineTests.java new file mode 100644 index 0000000000..5c92c3ec89 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableConvertedBaselineTests.java @@ -0,0 +1,226 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import java.time.Instant; +import java.util.Date; + +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Mutability; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.orm.test.mapping.mutability.attribute.BasicAttributeMutabilityTests; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.descriptor.java.Immutability; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +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 jakarta.persistence.Basic; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for combining {@link Mutability} and {@link Immutable} with conversions + * directly on attributes as a baseline for applying {@link Mutability} and {@link Immutable} + * to the converter class + * + * @see BasicAttributeMutabilityTests + * @see ImmutableConverterTests + * @see ImmutabilityConverterTests + * + * @author Steve Ebersole + */ +@DomainModel( annotatedClasses = ImmutableConvertedBaselineTests.TheEntity.class ) +@SessionFactory +public class ImmutableConvertedBaselineTests { + private static final Instant START = Instant.now(); + + /** + * Essentially the same as {@link BasicAttributeMutabilityTests#verifyDomainModel} + */ + @Test + void verifyDomainModel(DomainModelScope domainModelScope, SessionFactoryScope sfSessionFactoryScope) { + final PersistentClass persistentClass = domainModelScope.getEntityBinding( TheEntity.class ); + final EntityPersister entityDescriptor = sfSessionFactoryScope + .getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( TheEntity.class ); + + // `@Immutable` + final Property theDateProperty = persistentClass.getProperty( "theDate" ); + assertThat( theDateProperty.isUpdateable() ).isFalse(); + final AttributeMapping theDateAttribute = entityDescriptor.findAttributeMapping( "theDate" ); + assertThat( theDateAttribute.getExposedMutabilityPlan().isMutable() ).isFalse(); + + // `@Mutability(Immutability.class)` + final Property anotherDateProperty = persistentClass.getProperty( "anotherDate" ); + assertThat( anotherDateProperty.isUpdateable() ).isTrue(); + final AttributeMapping anotherDateAttribute = entityDescriptor.findAttributeMapping( "anotherDate" ); + assertThat( anotherDateAttribute.getExposedMutabilityPlan().isMutable() ).isFalse(); + } + + /** + * Effectively the same as {@linkplain BasicAttributeMutabilityTests#testImmutableManaged} + * + * Because it is non-updateable, no UPDATE + */ + @Test + void testImmutableManaged(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "@Immutable test", Date.from( START ) ) ); + } ); + + // load a managed reference and mutate the date + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + theEntity.theDate.setTime( Instant.EPOCH.toEpochMilli() ); + } ); + + // reload the entity and verify that the mutation was ignored + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.theDate.getTime() ).isEqualTo( START.toEpochMilli() ); + } ); + } + + /** + * Effectively the same as {@linkplain BasicAttributeMutabilityTests#testImmutableDetached} + * + * Because it is non-updateable, no UPDATE + */ + @Test + void testImmutableMerge(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "@Immutable test", Date.from( START ) ) ); + } ); + + // load a detached reference + final TheEntity detached = scope.fromTransaction( (session) -> session.find( TheEntity.class, 1 ) ); + + // make the change to the detached instance and merge it + detached.theDate.setTime( Instant.EPOCH.toEpochMilli() ); + scope.inTransaction( (session) -> session.merge( detached ) ); + + // verify the value did not change + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.theDate.getTime() ).isEqualTo( START.toEpochMilli() ); + } ); + } + + /** + * Effectively the same as {@linkplain BasicAttributeMutabilityTests#testImmutabilityManaged}. + * + * Because the state mutation is done on a managed instance, Hibernate detects that; and + * because it is internal-state-immutable, we will ignore the mutation and there will + * be no UPDATE + */ + @Test + void testImmutabilityManaged(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "@Mutability test", Date.from( START ) ) ); + } ); + + // try to update the managed form + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.anotherDate ).isEqualTo( Date.from( START ) ); + theEntity.anotherDate.setTime( Instant.EPOCH.toEpochMilli() ); + } ); + + // reload it and verify the value did not change + final TheEntity detached = scope.fromTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.anotherDate ).isEqualTo( Date.from( START ) ); + return theEntity; + } ); + + // Unfortunately, dues to how merge works (find + set) this change to the + // detached instance looks like a set when applied to the managed instance. + // Therefore, from the perspective of the merge operation, the Date itself was + // set rather than its internal state being changed. AKA, this will "correctly" + // result in an update + detached.anotherDate.setTime( Instant.EPOCH.toEpochMilli() ); + scope.inTransaction( (session) -> session.merge( detached ) ); + + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.anotherDate ).isEqualTo( Date.from( Instant.EPOCH ) ); + } ); + } + + /** + * Effectively the same as {@linkplain BasicAttributeMutabilityTests#testImmutabilityDetached} + * + * There will be an UPDATE because we cannot distinguish one type of change from another while detached + */ + @Test + void testImmutabilityDetached(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "@Mutability test", Date.from( START ) ) ); + } ); + + // load a detached reference + final TheEntity detached = scope.fromTransaction( (session) -> session.find( TheEntity.class, 1 ) ); + + // mutate the date and merge the detached ref + detached.anotherDate.setTime( Instant.EPOCH.toEpochMilli() ); + scope.inTransaction( (session) -> session.merge( detached ) ); + + // verify the value change was persisted + scope.inTransaction( (session) -> { + final TheEntity theEntity = session.find( TheEntity.class, 1 ); + assertThat( theEntity.anotherDate.getTime() ).isEqualTo( Instant.EPOCH.toEpochMilli() ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.createMutationQuery( "delete TheEntity" ).executeUpdate() ); + } + + @Entity( name = "TheEntity" ) + @Table( name = "TheEntity" ) + public static class TheEntity { + @Id + private Integer id; + + @Basic + private String name; + + @Immutable + @Convert(converter = DateConverter.class) + private Date theDate; + + @Mutability(Immutability.class) + @Convert(converter = DateConverter.class) + private Date anotherDate; + + + private TheEntity() { + // for use by Hibernate + } + + public TheEntity(Integer id, String name, Date aDate) { + this.id = id; + this.name = name; + this.theDate = aDate; + this.anotherDate = aDate; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableConverterTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableConverterTests.java new file mode 100644 index 0000000000..9f1014524a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableConverterTests.java @@ -0,0 +1,100 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import java.util.Date; + +import org.hibernate.annotations.Immutable; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Property; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Basic; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests applying {@link Immutable} to an {@link jakarta.persistence.AttributeConverter}. + * + * Here we just verify that has the same effect as {@link Immutable} directly on the attribute + * in terms of configuring the boot model references. + * + * @see ImmutableConvertedBaselineTests + * + * @author Steve Ebersole + */ +@ServiceRegistry +@DomainModel( annotatedClasses = ImmutableConverterTests.TestEntity.class ) +@SessionFactory +public class ImmutableConverterTests { + @Test + void verifyMetamodel(DomainModelScope scope) { + scope.withHierarchy( TestEntity.class, (entity) -> { + { + final Property property = entity.getProperty( "mutableDate" ); + assertThat( property ).isNotNull(); + assertThat( property.isUpdateable() ).isTrue(); + + final BasicValue basicValue = (BasicValue) property.getValue(); + final BasicValue.Resolution resolution = basicValue.resolve(); + assertThat( resolution.getMutabilityPlan().isMutable() ).isTrue(); + } + + { + final Property property = entity.getProperty( "immutableDate" ); + assertThat( property ).isNotNull(); + assertThat( property.isUpdateable() ).isTrue(); + + final BasicValue basicValue = (BasicValue) property.getValue(); + final BasicValue.Resolution resolution = basicValue.resolve(); + assertThat( resolution.getMutabilityPlan().isMutable() ).isFalse(); + } + } ); + } + + @Entity( name = "TestEntity" ) + @Table( name = "TestEntity" ) + public static class TestEntity { + @Id + private Integer id; + @Basic + private String name; + @Convert( converter = ImmutableDateConverter.class ) + private Date immutableDate; + private Date mutableDate; + + private TestEntity() { + // for use by Hibernate + } + + public TestEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableDateConverter.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableDateConverter.java new file mode 100644 index 0000000000..fdb364215d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableDateConverter.java @@ -0,0 +1,16 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import org.hibernate.annotations.Immutable; + +/** + * @author Steve Ebersole + */ +@Immutable +public class ImmutableDateConverter extends DateConverter { +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableMapConverter.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableMapConverter.java new file mode 100644 index 0000000000..b6a4401a6a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/ImmutableMapConverter.java @@ -0,0 +1,16 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import org.hibernate.annotations.Immutable; + +/** + * @author Steve Ebersole + */ +@Immutable +public class ImmutableMapConverter extends MapConverter { +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/MapConverter.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/MapConverter.java new file mode 100644 index 0000000000..a01dffb89d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/converted/MapConverter.java @@ -0,0 +1,35 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.converted; + +import java.util.Map; + +import org.hibernate.internal.util.StringHelper; +import org.hibernate.internal.util.collections.CollectionHelper; + +import jakarta.persistence.AttributeConverter; + +/** + * @author Steve Ebersole + */ +public class MapConverter implements AttributeConverter, String> { + @Override + public String convertToDatabaseColumn(Map map) { + if ( CollectionHelper.isEmpty( map ) ) { + return null; + } + return StringHelper.join( ", ", CollectionHelper.asPairs( map ) ); + } + + @Override + public Map convertToEntityAttribute(String pairs) { + if ( StringHelper.isEmpty( pairs ) ) { + return null; + } + return CollectionHelper.toMap( StringHelper.split( ", ", pairs ) ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/EntityImmutabilityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/EntityImmutabilityTest.java new file mode 100644 index 0000000000..a7cd47409c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/EntityImmutabilityTest.java @@ -0,0 +1,113 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.entity; + +import java.util.Date; + +import org.hibernate.annotations.Immutable; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import org.jboss.logging.Logger; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vlad Mihalcea + */ +@DomainModel( annotatedClasses = EntityImmutabilityTest.Event.class ) +@SessionFactory +public class EntityImmutabilityTest { + private static final Logger log = Logger.getLogger( EntityImmutabilityTest.class ); + + @Test + void verifyMetamodel(DomainModelScope scope) { + scope.withHierarchy( Event.class, (entity) -> { + assertThat( entity.isMutable() ).isFalse(); + + // this implies that all attributes and mapped columns are non-updateable, + // but the code does not explicitly set that. The functional test + // verifies that they function as non-updateable + } ); + } + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( (entityManager) -> { + //tag::entity-immutability-persist-example[] + Event event = new Event(); + event.setId(1L); + event.setCreatedOn(new Date()); + event.setMessage("Hibernate User Guide rocks!"); + + entityManager.persist(event); + //end::entity-immutability-persist-example[] + } ); + + scope.inTransaction( (entityManager) -> { + //tag::entity-immutability-update-example[] + Event event = entityManager.find(Event.class, 1L); + log.info("Change event message"); + event.setMessage("Hibernate User Guide"); + //end::entity-immutability-update-example[] + } ); + scope.inTransaction( (entityManager) -> { + Event event = entityManager.find(Event.class, 1L); + assertThat( event.getMessage() ).isEqualTo( "Hibernate User Guide rocks!" ); + } ); + } + + //tag::entity-immutability-example[] + @Entity(name = "Event") + @Immutable + public static class Event { + + @Id + private Long id; + + private Date createdOn; + + private String message; + + //Getters and setters are omitted for brevity + + //end::entity-immutability-example[] + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + //tag::entity-immutability-example[] + } + //end::entity-immutability-example[] +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/EntityMutabilityPlanTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/EntityMutabilityPlanTest.java new file mode 100644 index 0000000000..837645ab15 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/EntityMutabilityPlanTest.java @@ -0,0 +1,50 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.mutability.entity; + +import java.util.Date; + +import org.hibernate.MappingException; +import org.hibernate.annotations.Mutability; +import org.hibernate.boot.MetadataSources; +import org.hibernate.type.descriptor.java.Immutability; + +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@ServiceRegistry +public class EntityMutabilityPlanTest { + @Test + void verifyMetamodel(ServiceRegistryScope scope) { + final MetadataSources metadataSources = new MetadataSources( scope.getRegistry() ); + metadataSources.addAnnotatedClass( Event.class ); + try { + metadataSources.buildMetadata(); + fail( "Expecting exception about @Mutability on the entity" ); + } + catch (MappingException expected) { + } + } + + @Entity(name = "Event") + @Mutability(Immutability.class) + public static class Event { + @Id + private Long id; + private Date createdOn; + private String message; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/package-info.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/package-info.java new file mode 100644 index 0000000000..ca67149b52 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/mutability/entity/package-info.java @@ -0,0 +1,13 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ + +/** + * Tests for the behavior of {@link org.hibernate.annotations.Immutable} attached to an entity + * + * @author Steve Ebersole + */ +package org.hibernate.orm.test.mapping.mutability.entity; diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DomainModelScope.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DomainModelScope.java index 34211eddf1..dafcf2ac30 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DomainModelScope.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DomainModelScope.java @@ -50,6 +50,11 @@ public interface DomainModelScope { action.accept( entityBinding.getRootClass() ); } + default PersistentClass getEntityBinding(Class theEntityClass) { + assert theEntityClass != null; + return getDomainModel().getEntityBinding( theEntityClass.getName() ); + } + // ... } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryScope.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryScope.java index a3ef58448b..272d799bf5 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryScope.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryScope.java @@ -27,6 +27,10 @@ public interface SessionFactoryScope { T getStatementInspector(Class type); SQLStatementInspector getCollectingStatementInspector(); + default void withSessionFactory(Consumer action) { + action.accept( getSessionFactory() ); + } + void inSession(Consumer action); void inTransaction(Consumer action); void inTransaction(SessionImplementor session, Consumer action);