HHH-16148 - Introduce Immutability (MutabilityPlan) for use with @Mutability

HHH-16141 - Support @Mutability and @Immutable on UserType
HHH-16147 - Support @Mutability and @Immutable on AttributeConverter
HHH-16146 - Improve User Guide documentation for (im)mutability
This commit is contained in:
Steve Ebersole 2023-02-09 13:21:11 -06:00
parent de59b44779
commit 15b24d6c14
34 changed files with 2144 additions and 661 deletions

View File

@ -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
org.hibernate.orm.test.mapping.mutability.attribute.PluralAttributeMutabilityTest$Batch.events#1
]

View File

@ -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 <<mutability-entity,entity>>, <<mutability-attribute,attribute>>,
<<mutability-converter,AttributeConverter>> and <<mutability-usertype,UserType>>. 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 <<mutability-converter,AttributeConverter>> 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.

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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[]
}

View File

@ -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.
*
*
*
* <h3>Mutability for basic-typed attributes</h3>
* <p>

View File

@ -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<? extends MutabilityPlan<?>> mutabilityClass = normalizeMutability( mutabilityAnn.value() );
final Class<? extends MutabilityPlan<?>> mutabilityClass = mutabilityAnn.value();
if ( mutabilityClass != null ) {
if ( useDeferredBeanContainerAccess ) {
return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutabilityClass );
}
final ManagedBean<? extends MutabilityPlan<?>> 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<? extends MutabilityPlan<?>> 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<? extends UserType<?>> 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<? extends MutabilityPlan<?>> mutabilityClass = normalizeMutability( mutabilityAnn.value() );
final Class<? extends MutabilityPlan<?>> mutabilityClass = mutabilityAnn.value();
if ( mutabilityClass != null ) {
if ( useDeferredBeanContainerAccess ) {
return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutabilityClass );
}
final ManagedBean<? extends MutabilityPlan<?>> 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<? extends MutabilityPlan<?>> 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<? extends UserType<?>> 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<? extends MutabilityPlan<?>> mutability = normalizeMutability( mutabilityAnn.value() );
final Class<? extends MutabilityPlan<?>> 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<? extends MutabilityPlan<?>> 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<? extends MutabilityPlan<?>> jtdBean = getManagedBeanRegistry().getBean( converterMutabilityAnn.value() );
return jtdBean.getBeanInstance();
final Class<? extends MutabilityPlan<?>> 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<? extends UserType<?>> 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<? extends MutabilityPlan<?>> 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 <T> MutabilityPlan<T> resolveMutability(Class<? extends MutabilityPlan> 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<? extends MutabilityPlan<?>> normalizeMutability(Class<? extends MutabilityPlan<?>> mutability) {
return mutability;
}
private java.lang.reflect.Type resolveJavaType(XClass returnedClassOrElement) {
return buildingContext.getBootstrapContext()
.getReflectionManager()

View File

@ -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);
}

View File

@ -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<JavaType<T>> reflectedJtdResolver,
Function<TypeConfiguration, MutabilityPlan> 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<TypeConfiguration, MutabilityPlan> explicitMutabilityPlanAccess,
JdbcTypeIndicators stdIndicators,
TypeConfiguration typeConfiguration) {
final TemporalType requestedTemporalPrecision = stdIndicators.getTemporalPrecision();
@ -509,13 +516,14 @@ public class InferredBasicValueResolver {
final BasicType<T> jdbcMapping = typeConfiguration.getBasicTypeRegistry().resolve( explicitTemporalJtd, jdbcType );
final MutabilityPlan<T> 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 <T> MutabilityPlan<T> determineMutabilityPlan(
Function<TypeConfiguration, MutabilityPlan> explicitMutabilityPlanAccess,
JavaType<T> jtd,
TypeConfiguration typeConfiguration) {
if ( explicitMutabilityPlanAccess != null ) {
final MutabilityPlan<T> mutabilityPlan = explicitMutabilityPlanAccess.apply( typeConfiguration );
if ( mutabilityPlan != null ) {
return mutabilityPlan;
}
}
return jtd.getMutabilityPlan();
}
}

View File

@ -479,6 +479,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
jdbcType,
resolvedJavaType,
this::determineReflectedJavaType,
explicitMutabilityPlanAccess,
this,
getTable(),
column,

View File

@ -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;

View File

@ -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<Object> {
/**
* Singleton access
*/
public static final Immutability INSTANCE = new Immutability();
public static <X> MutabilityPlan<X> instance() {
//noinspection unchecked
return (MutabilityPlan<X>) 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;
}
}

View File

@ -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<T> implements MutabilityPlan<T> {

View File

@ -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<String,String> values;
private TestEntity() {
// for use by Hibernate
}
public TestEntity(
Integer id,
Map<String,String> values) {
this.id = id;
this.values = values;
}
}
}

View File

@ -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<Date,String> {
@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<Date,String> {
@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;
}
}
}

View File

@ -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[]

View File

@ -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;
}
}
}

View File

@ -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<Event> 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<Event> 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[]
}

View File

@ -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<String,String> data;
private TestEntity() {
// for use by Hibernate
}
public TestEntity(Integer id, Map<String,String> data) {
this.id = id;
this.data = data;
}
}
}

View File

@ -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<String,String> data;
private TestEntity() {
// for use by Hibernate
}
public TestEntity(Integer id, Map<String,String> data) {
this.id = id;
this.data = data;
}
}
}

View File

@ -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<Map<String,String>,String> {
@Override
public String convertToDatabaseColumn(Map<String,String> map) {
if ( CollectionHelper.isEmpty( map ) ) {
return null;
}
return StringHelper.join( ", ", CollectionHelper.asPairs( map ) );
}
@Override
public Map<String,String> 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<String,String> values;
private Map<String,String> data;
private TestEntity() {
// for use by Hibernate
}
public TestEntity(
Integer id,
Map<String,String> values) {
public TestEntity(Integer id, Map<String,String> data) {
this.id = id;
this.values = values;
this.data = data;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
* 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");

View File

@ -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<Date, String> {
@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 ) ) );
}
}

View File

@ -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;
}
}
}

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -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<Map<String, String>, String> {
@Override
public String convertToDatabaseColumn(Map<String, String> map) {
if ( CollectionHelper.isEmpty( map ) ) {
return null;
}
return StringHelper.join( ", ", CollectionHelper.asPairs( map ) );
}
@Override
public Map<String, String> convertToEntityAttribute(String pairs) {
if ( StringHelper.isEmpty( pairs ) ) {
return null;
}
return CollectionHelper.toMap( StringHelper.split( ", ", pairs ) );
}
}

View File

@ -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[]
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -50,6 +50,11 @@ public interface DomainModelScope {
action.accept( entityBinding.getRootClass() );
}
default PersistentClass getEntityBinding(Class<?> theEntityClass) {
assert theEntityClass != null;
return getDomainModel().getEntityBinding( theEntityClass.getName() );
}
// ...
}

View File

@ -27,6 +27,10 @@ public interface SessionFactoryScope {
<T extends StatementInspector> T getStatementInspector(Class<T> type);
SQLStatementInspector getCollectingStatementInspector();
default void withSessionFactory(Consumer<SessionFactoryImplementor> action) {
action.accept( getSessionFactory() );
}
void inSession(Consumer<SessionImplementor> action);
void inTransaction(Consumer<SessionImplementor> action);
void inTransaction(SessionImplementor session, Consumer<SessionImplementor> action);