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:
parent
de59b44779
commit
15b24d6c14
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
@ -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[]
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -479,6 +479,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
|
|||
jdbcType,
|
||||
resolvedJavaType,
|
||||
this::determineReflectedJavaType,
|
||||
explicitMutabilityPlanAccess,
|
||||
this,
|
||||
getTable(),
|
||||
column,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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[]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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[]
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
|
@ -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 ) ) );
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 ) );
|
||||
}
|
||||
}
|
|
@ -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[]
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -50,6 +50,11 @@ public interface DomainModelScope {
|
|||
action.accept( entityBinding.getRootClass() );
|
||||
}
|
||||
|
||||
default PersistentClass getEntityBinding(Class<?> theEntityClass) {
|
||||
assert theEntityClass != null;
|
||||
return getDomainModel().getEntityBinding( theEntityClass.getName() );
|
||||
}
|
||||
|
||||
|
||||
// ...
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue