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
ecf8e1ce39
commit
973434c8f1
|
@ -3,5 +3,5 @@ jakarta.persistence.RollbackException: Error while committing the transaction
|
||||||
Caused by: jakarta.persistence.PersistenceException: org.hibernate.HibernateException:
|
Caused by: jakarta.persistence.PersistenceException: org.hibernate.HibernateException:
|
||||||
|
|
||||||
Caused by: org.hibernate.HibernateException: changed an immutable collection instance: [
|
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]]
|
[[mutability]]
|
||||||
=== Immutability
|
=== Mutability
|
||||||
:root-project-dir: ../../../../../../..
|
:root-project-dir: ../../../../../../..
|
||||||
:documentation-project-dir: {root-project-dir}/documentation
|
:core-project-dir: {root-project-dir}/hibernate-core
|
||||||
:example-dir-immutability: {documentation-project-dir}/src/test/java/org/hibernate/userguide/immutability
|
:mutability-example-dir: {core-project-dir}/src/test/java/org/hibernate/orm/test/mapping/mutability
|
||||||
:extrasdir: extras/immutability
|
: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
|
==== Entity immutability
|
||||||
|
|
||||||
If a specific entity is immutable, it is good practice to mark it with the `@Immutable` annotation.
|
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]
|
[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:
|
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
|
- 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:
|
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]
|
[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]
|
[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]
|
[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]
|
[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.
|
Changes to the `theDate` attribute are ignored.
|
||||||
Once the immutable collection is created, it can never be modified.
|
|
||||||
|
.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
|
.Persisting an immutable collection
|
||||||
====
|
====
|
||||||
[source, JAVA, indent=0]
|
[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]
|
[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]
|
[source, SQL, indent=0]
|
||||||
|
@ -98,7 +164,7 @@ However, when trying to modify the `events` collection:
|
||||||
====
|
====
|
||||||
[source, JAVA, indent=0]
|
[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]
|
[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;
|
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>
|
* <h3>Mutability for basic-typed attributes</h3>
|
||||||
* <p>
|
* <p>
|
||||||
|
|
|
@ -68,6 +68,7 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
|
||||||
import org.hibernate.type.BasicType;
|
import org.hibernate.type.BasicType;
|
||||||
import org.hibernate.type.SerializableToBlobType;
|
import org.hibernate.type.SerializableToBlobType;
|
||||||
import org.hibernate.type.descriptor.java.BasicJavaType;
|
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.ImmutableMutabilityPlan;
|
||||||
import org.hibernate.type.descriptor.java.JavaType;
|
import org.hibernate.type.descriptor.java.JavaType;
|
||||||
import org.hibernate.type.descriptor.java.MutabilityPlan;
|
import org.hibernate.type.descriptor.java.MutabilityPlan;
|
||||||
|
@ -479,20 +480,21 @@ public class BasicValueBinder implements JdbcTypeIndicators {
|
||||||
explicitMutabilityAccess = (typeConfiguration) -> {
|
explicitMutabilityAccess = (typeConfiguration) -> {
|
||||||
final CollectionIdMutability mutabilityAnn = findAnnotation( modelXProperty, CollectionIdMutability.class );
|
final CollectionIdMutability mutabilityAnn = findAnnotation( modelXProperty, CollectionIdMutability.class );
|
||||||
if ( mutabilityAnn != null ) {
|
if ( mutabilityAnn != null ) {
|
||||||
final Class<? extends MutabilityPlan<?>> mutabilityClass = normalizeMutability( mutabilityAnn.value() );
|
final Class<? extends MutabilityPlan<?>> mutabilityClass = mutabilityAnn.value();
|
||||||
if ( mutabilityClass != null ) {
|
if ( mutabilityClass != null ) {
|
||||||
if ( useDeferredBeanContainerAccess ) {
|
return resolveMutability( mutabilityClass );
|
||||||
return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutabilityClass );
|
|
||||||
}
|
|
||||||
final ManagedBean<? extends MutabilityPlan<?>> jtdBean = beanRegistry.getBean( mutabilityClass );
|
|
||||||
return jtdBean.getBeanInstance();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 ) {
|
if ( implicitJavaTypeAccess != null ) {
|
||||||
final Class<?> attributeType = ReflectHelper.getClass( implicitJavaTypeAccess.apply( typeConfiguration ) );
|
final Class<?> attributeType = ReflectHelper.getClass( implicitJavaTypeAccess.apply( typeConfiguration ) );
|
||||||
if ( attributeType != null ) {
|
if ( attributeType != null ) {
|
||||||
|
final Mutability attributeTypeMutabilityAnn = attributeType.getAnnotation( Mutability.class );
|
||||||
|
if ( attributeTypeMutabilityAnn != null ) {
|
||||||
|
return resolveMutability( attributeTypeMutabilityAnn.value() );
|
||||||
|
}
|
||||||
|
|
||||||
if ( attributeType.isAnnotationPresent( Immutable.class ) ) {
|
if ( attributeType.isAnnotationPresent( Immutable.class ) ) {
|
||||||
return ImmutableMutabilityPlan.instance();
|
return ImmutableMutabilityPlan.instance();
|
||||||
}
|
}
|
||||||
|
@ -503,11 +505,7 @@ public class BasicValueBinder implements JdbcTypeIndicators {
|
||||||
if ( converterDescriptor != null ) {
|
if ( converterDescriptor != null ) {
|
||||||
final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class );
|
final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class );
|
||||||
if ( converterMutabilityAnn != null ) {
|
if ( converterMutabilityAnn != null ) {
|
||||||
if ( useDeferredBeanContainerAccess ) {
|
return resolveMutability( converterMutabilityAnn.value() );
|
||||||
return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( converterMutabilityAnn.value() );
|
|
||||||
}
|
|
||||||
final ManagedBean<? extends MutabilityPlan<?>> jtdBean = beanRegistry.getBean( converterMutabilityAnn.value() );
|
|
||||||
return jtdBean.getBeanInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( converterDescriptor.getAttributeConverterClass().isAnnotationPresent( Immutable.class ) ) {
|
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 );
|
final Class<? extends UserType<?>> customTypeImpl = Kind.ATTRIBUTE.mappingAccess.customType( modelXProperty );
|
||||||
if ( customTypeImpl.isAnnotationPresent( Immutable.class ) ) {
|
if ( customTypeImpl != null ) {
|
||||||
return ImmutableMutabilityPlan.instance();
|
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`
|
// generally, this will trigger usage of the `JavaType#getMutabilityPlan`
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo (6.0) - handle generator
|
|
||||||
// final String generator = collectionIdAnn.generator();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ManagedBeanRegistry getManagedBeanRegistry() {
|
private ManagedBeanRegistry getManagedBeanRegistry() {
|
||||||
|
@ -600,35 +603,32 @@ public class BasicValueBinder implements JdbcTypeIndicators {
|
||||||
explicitMutabilityAccess = typeConfiguration -> {
|
explicitMutabilityAccess = typeConfiguration -> {
|
||||||
final MapKeyMutability mutabilityAnn = findAnnotation( mapAttribute, MapKeyMutability.class );
|
final MapKeyMutability mutabilityAnn = findAnnotation( mapAttribute, MapKeyMutability.class );
|
||||||
if ( mutabilityAnn != null ) {
|
if ( mutabilityAnn != null ) {
|
||||||
final Class<? extends MutabilityPlan<?>> mutabilityClass = normalizeMutability( mutabilityAnn.value() );
|
final Class<? extends MutabilityPlan<?>> mutabilityClass = mutabilityAnn.value();
|
||||||
if ( mutabilityClass != null ) {
|
if ( mutabilityClass != null ) {
|
||||||
if ( useDeferredBeanContainerAccess ) {
|
return resolveMutability( mutabilityClass );
|
||||||
return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutabilityClass );
|
|
||||||
}
|
|
||||||
final ManagedBean<? extends MutabilityPlan<?>> jtdBean = getManagedBeanRegistry().getBean( mutabilityClass );
|
|
||||||
return jtdBean.getBeanInstance();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 ) {
|
if ( implicitJavaTypeAccess != null ) {
|
||||||
final Class<?> attributeType = ReflectHelper.getClass( implicitJavaTypeAccess.apply( typeConfiguration ) );
|
final Class<?> attributeType = ReflectHelper.getClass( implicitJavaTypeAccess.apply( typeConfiguration ) );
|
||||||
if ( attributeType != null ) {
|
if ( attributeType != null ) {
|
||||||
|
final Mutability attributeTypeMutabilityAnn = attributeType.getAnnotation( Mutability.class );
|
||||||
|
if ( attributeTypeMutabilityAnn != null ) {
|
||||||
|
return resolveMutability( attributeTypeMutabilityAnn.value() );
|
||||||
|
}
|
||||||
|
|
||||||
if ( attributeType.isAnnotationPresent( Immutable.class ) ) {
|
if ( attributeType.isAnnotationPresent( Immutable.class ) ) {
|
||||||
return ImmutableMutabilityPlan.instance();
|
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 ) {
|
if ( converterDescriptor != null ) {
|
||||||
final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class );
|
final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class );
|
||||||
if ( converterMutabilityAnn != null ) {
|
if ( converterMutabilityAnn != null ) {
|
||||||
if ( useDeferredBeanContainerAccess ) {
|
return resolveMutability( converterMutabilityAnn.value() );
|
||||||
return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( converterMutabilityAnn.value() );
|
|
||||||
}
|
|
||||||
final ManagedBean<? extends MutabilityPlan<?>> jtdBean = getManagedBeanRegistry().getBean( converterMutabilityAnn.value() );
|
|
||||||
return jtdBean.getBeanInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( converterDescriptor.getAttributeConverterClass().isAnnotationPresent( Immutable.class ) ) {
|
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 );
|
final Class<? extends UserType<?>> customTypeImpl = Kind.MAP_KEY.mappingAccess.customType( mapAttribute );
|
||||||
if ( customTypeImpl != null ) {
|
if ( customTypeImpl != null ) {
|
||||||
|
final Mutability customTypeMutabilityAnn = customTypeImpl.getAnnotation( Mutability.class );
|
||||||
|
if ( customTypeMutabilityAnn != null ) {
|
||||||
|
return resolveMutability( customTypeMutabilityAnn.value() );
|
||||||
|
}
|
||||||
|
|
||||||
if ( customTypeImpl.isAnnotationPresent( Immutable.class ) ) {
|
if ( customTypeImpl.isAnnotationPresent( Immutable.class ) ) {
|
||||||
return ImmutableMutabilityPlan.instance();
|
return ImmutableMutabilityPlan.instance();
|
||||||
}
|
}
|
||||||
|
@ -943,53 +949,80 @@ public class BasicValueBinder implements JdbcTypeIndicators {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void normalMutabilityDetails(XProperty attributeXProperty) {
|
private void normalMutabilityDetails(XProperty attributeXProperty) {
|
||||||
|
|
||||||
explicitMutabilityAccess = typeConfiguration -> {
|
explicitMutabilityAccess = typeConfiguration -> {
|
||||||
|
// Look for `@Mutability` on the attribute
|
||||||
final Mutability mutabilityAnn = findAnnotation( attributeXProperty, Mutability.class );
|
final Mutability mutabilityAnn = findAnnotation( attributeXProperty, Mutability.class );
|
||||||
if ( mutabilityAnn != null ) {
|
if ( mutabilityAnn != null ) {
|
||||||
final Class<? extends MutabilityPlan<?>> mutability = normalizeMutability( mutabilityAnn.value() );
|
final Class<? extends MutabilityPlan<?>> mutability = mutabilityAnn.value();
|
||||||
if ( mutability != null ) {
|
if ( mutability != null ) {
|
||||||
if ( buildingContext.getBuildingOptions().disallowExtensionsInCdi() ) {
|
return resolveMutability( mutability );
|
||||||
return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( mutability );
|
|
||||||
}
|
|
||||||
return getManagedBeanRegistry().getBean( mutability ).getBeanInstance();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look for `@Immutable` on the attribute
|
||||||
final Immutable immutableAnn = attributeXProperty.getAnnotation( Immutable.class );
|
final Immutable immutableAnn = attributeXProperty.getAnnotation( Immutable.class );
|
||||||
if ( immutableAnn != null ) {
|
if ( immutableAnn != null ) {
|
||||||
return ImmutableMutabilityPlan.instance();
|
return ImmutableMutabilityPlan.instance();
|
||||||
}
|
}
|
||||||
|
|
||||||
// see if the value's type Class is annotated `@Immutable`
|
// Look for `@Mutability` on the attribute's type
|
||||||
if ( implicitJavaTypeAccess != null ) {
|
if ( explicitJavaTypeAccess != null || implicitJavaTypeAccess != null ) {
|
||||||
final Class<?> attributeType = ReflectHelper.getClass( implicitJavaTypeAccess.apply( typeConfiguration ) );
|
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 != 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();
|
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 ) {
|
if ( converterDescriptor != null ) {
|
||||||
final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class );
|
final Mutability converterMutabilityAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Mutability.class );
|
||||||
if ( converterMutabilityAnn != null ) {
|
if ( converterMutabilityAnn != null ) {
|
||||||
if ( buildingContext.getBuildingOptions().disallowExtensionsInCdi() ) {
|
final Class<? extends MutabilityPlan<?>> mutability = converterMutabilityAnn.value();
|
||||||
return FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( converterMutabilityAnn.value() );
|
return resolveMutability( mutability );
|
||||||
}
|
|
||||||
final ManagedBean<? extends MutabilityPlan<?>> jtdBean = getManagedBeanRegistry().getBean( converterMutabilityAnn.value() );
|
|
||||||
return jtdBean.getBeanInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( converterDescriptor.getAttributeConverterClass().isAnnotationPresent( Immutable.class ) ) {
|
final Immutable converterImmutableAnn = converterDescriptor.getAttributeConverterClass().getAnnotation( Immutable.class );
|
||||||
|
if ( converterImmutableAnn != null ) {
|
||||||
return ImmutableMutabilityPlan.instance();
|
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 );
|
final Class<? extends UserType<?>> customTypeImpl = Kind.ATTRIBUTE.mappingAccess.customType( attributeXProperty );
|
||||||
if ( customTypeImpl != null ) {
|
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();
|
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) {
|
private void normalSupplementalDetails(XProperty attributeXProperty) {
|
||||||
|
|
||||||
explicitJavaTypeAccess = typeConfiguration -> {
|
explicitJavaTypeAccess = typeConfiguration -> {
|
||||||
|
@ -1067,10 +1117,6 @@ public class BasicValueBinder implements JdbcTypeIndicators {
|
||||||
return javaType;
|
return javaType;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Class<? extends MutabilityPlan<?>> normalizeMutability(Class<? extends MutabilityPlan<?>> mutability) {
|
|
||||||
return mutability;
|
|
||||||
}
|
|
||||||
|
|
||||||
private java.lang.reflect.Type resolveJavaType(XClass returnedClassOrElement) {
|
private java.lang.reflect.Type resolveJavaType(XClass returnedClassOrElement) {
|
||||||
return buildingContext.getBootstrapContext()
|
return buildingContext.getBootstrapContext()
|
||||||
.getReflectionManager()
|
.getReflectionManager()
|
||||||
|
|
|
@ -17,27 +17,6 @@ import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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.AnnotationException;
|
||||||
import org.hibernate.AssertionFailure;
|
import org.hibernate.AssertionFailure;
|
||||||
import org.hibernate.MappingException;
|
import org.hibernate.MappingException;
|
||||||
|
@ -56,6 +35,7 @@ import org.hibernate.annotations.ForeignKey;
|
||||||
import org.hibernate.annotations.HQLSelect;
|
import org.hibernate.annotations.HQLSelect;
|
||||||
import org.hibernate.annotations.Immutable;
|
import org.hibernate.annotations.Immutable;
|
||||||
import org.hibernate.annotations.Loader;
|
import org.hibernate.annotations.Loader;
|
||||||
|
import org.hibernate.annotations.Mutability;
|
||||||
import org.hibernate.annotations.NaturalIdCache;
|
import org.hibernate.annotations.NaturalIdCache;
|
||||||
import org.hibernate.annotations.OnDelete;
|
import org.hibernate.annotations.OnDelete;
|
||||||
import org.hibernate.annotations.OptimisticLockType;
|
import org.hibernate.annotations.OptimisticLockType;
|
||||||
|
@ -125,6 +105,28 @@ import org.hibernate.spi.NavigablePath;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
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.AnnotatedClassType.MAPPED_SUPERCLASS;
|
||||||
import static org.hibernate.boot.model.internal.AnnotatedDiscriminatorColumn.buildDiscriminatorColumn;
|
import static org.hibernate.boot.model.internal.AnnotatedDiscriminatorColumn.buildDiscriminatorColumn;
|
||||||
import static org.hibernate.boot.model.internal.AnnotatedJoinColumn.buildInheritanceJoinColumn;
|
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.getOverridableAnnotation;
|
||||||
import static org.hibernate.boot.model.internal.BinderHelper.hasToOneAnnotation;
|
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.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.toAliasEntityMap;
|
||||||
import static org.hibernate.boot.model.internal.BinderHelper.toAliasTableMap;
|
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.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.HCANNHelper.findContainingAnnotations;
|
||||||
import static org.hibernate.boot.model.internal.InheritanceState.getInheritanceStateOfSuperEntity;
|
import static org.hibernate.boot.model.internal.InheritanceState.getInheritanceStateOfSuperEntity;
|
||||||
import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClass;
|
import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClass;
|
||||||
|
@ -1190,6 +1192,8 @@ public class EntityBinder {
|
||||||
LOG.immutableAnnotationOnNonRoot( annotatedClass.getName() );
|
LOG.immutableAnnotationOnNonRoot( annotatedClass.getName() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureNoMutabilityPlan();
|
||||||
|
|
||||||
bindCustomPersister();
|
bindCustomPersister();
|
||||||
bindCustomSql();
|
bindCustomSql();
|
||||||
bindSynchronize();
|
bindSynchronize();
|
||||||
|
@ -1200,6 +1204,12 @@ public class EntityBinder {
|
||||||
processNamedEntityGraphs();
|
processNamedEntityGraphs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureNoMutabilityPlan() {
|
||||||
|
if ( annotatedClass.isAnnotationPresent( Mutability.class ) ) {
|
||||||
|
throw new MappingException( "@Mutability is not allowed on entity" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isMutable() {
|
private boolean isMutable() {
|
||||||
return !annotatedClass.isAnnotationPresent(Immutable.class);
|
return !annotatedClass.isAnnotationPresent(Immutable.class);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,29 +8,28 @@ package org.hibernate.boot.model.process.internal;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import jakarta.persistence.TemporalType;
|
|
||||||
|
|
||||||
import org.hibernate.MappingException;
|
import org.hibernate.MappingException;
|
||||||
import org.hibernate.dialect.Dialect;
|
import org.hibernate.dialect.Dialect;
|
||||||
import org.hibernate.mapping.BasicValue;
|
import org.hibernate.mapping.BasicValue;
|
||||||
import org.hibernate.mapping.Column;
|
import org.hibernate.mapping.Column;
|
||||||
import org.hibernate.mapping.Selectable;
|
import org.hibernate.mapping.Selectable;
|
||||||
import org.hibernate.mapping.Table;
|
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.tool.schema.extract.spi.ColumnTypeInformation;
|
||||||
import org.hibernate.type.AdjustableBasicType;
|
import org.hibernate.type.AdjustableBasicType;
|
||||||
import org.hibernate.type.BasicType;
|
import org.hibernate.type.BasicType;
|
||||||
import org.hibernate.type.CustomType;
|
import org.hibernate.type.CustomType;
|
||||||
import org.hibernate.type.SerializableType;
|
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.BasicJavaType;
|
||||||
|
import org.hibernate.type.descriptor.java.BasicPluralJavaType;
|
||||||
import org.hibernate.type.descriptor.java.EnumJavaType;
|
import org.hibernate.type.descriptor.java.EnumJavaType;
|
||||||
import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan;
|
import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan;
|
||||||
import org.hibernate.type.descriptor.java.JavaType;
|
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.SerializableJavaType;
|
||||||
import org.hibernate.type.descriptor.java.TemporalJavaType;
|
import org.hibernate.type.descriptor.java.TemporalJavaType;
|
||||||
import org.hibernate.type.descriptor.jdbc.JdbcType;
|
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.descriptor.jdbc.ObjectJdbcType;
|
||||||
import org.hibernate.type.spi.TypeConfiguration;
|
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.SMALLINT;
|
||||||
import static org.hibernate.type.SqlTypes.TINYINT;
|
import static org.hibernate.type.SqlTypes.TINYINT;
|
||||||
|
|
||||||
|
@ -52,6 +54,7 @@ public class InferredBasicValueResolver {
|
||||||
JdbcType explicitJdbcType,
|
JdbcType explicitJdbcType,
|
||||||
Type resolvedJavaType,
|
Type resolvedJavaType,
|
||||||
Supplier<JavaType<T>> reflectedJtdResolver,
|
Supplier<JavaType<T>> reflectedJtdResolver,
|
||||||
|
Function<TypeConfiguration, MutabilityPlan> explicitMutabilityPlanAccess,
|
||||||
JdbcTypeIndicators stdIndicators,
|
JdbcTypeIndicators stdIndicators,
|
||||||
Table table,
|
Table table,
|
||||||
Selectable selectable,
|
Selectable selectable,
|
||||||
|
@ -84,6 +87,7 @@ public class InferredBasicValueResolver {
|
||||||
null,
|
null,
|
||||||
explicitJdbcType,
|
explicitJdbcType,
|
||||||
resolvedJavaType,
|
resolvedJavaType,
|
||||||
|
explicitMutabilityPlanAccess,
|
||||||
stdIndicators,
|
stdIndicators,
|
||||||
typeConfiguration
|
typeConfiguration
|
||||||
);
|
);
|
||||||
|
@ -135,6 +139,7 @@ public class InferredBasicValueResolver {
|
||||||
null,
|
null,
|
||||||
explicitJdbcType,
|
explicitJdbcType,
|
||||||
resolvedJavaType,
|
resolvedJavaType,
|
||||||
|
explicitMutabilityPlanAccess,
|
||||||
stdIndicators,
|
stdIndicators,
|
||||||
typeConfiguration
|
typeConfiguration
|
||||||
);
|
);
|
||||||
|
@ -171,6 +176,7 @@ public class InferredBasicValueResolver {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
resolvedJavaType,
|
resolvedJavaType,
|
||||||
|
explicitMutabilityPlanAccess,
|
||||||
stdIndicators,
|
stdIndicators,
|
||||||
typeConfiguration
|
typeConfiguration
|
||||||
);
|
);
|
||||||
|
@ -296,7 +302,7 @@ public class InferredBasicValueResolver {
|
||||||
jdbcMapping.getJavaTypeDescriptor(),
|
jdbcMapping.getJavaTypeDescriptor(),
|
||||||
jdbcMapping.getJdbcType(),
|
jdbcMapping.getJdbcType(),
|
||||||
jdbcMapping,
|
jdbcMapping,
|
||||||
null
|
determineMutabilityPlan( explicitMutabilityPlanAccess, jdbcMapping.getJavaTypeDescriptor(), typeConfiguration )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,6 +483,7 @@ public class InferredBasicValueResolver {
|
||||||
BasicJavaType<?> explicitJavaType,
|
BasicJavaType<?> explicitJavaType,
|
||||||
JdbcType explicitJdbcType,
|
JdbcType explicitJdbcType,
|
||||||
Type resolvedJavaType,
|
Type resolvedJavaType,
|
||||||
|
Function<TypeConfiguration, MutabilityPlan> explicitMutabilityPlanAccess,
|
||||||
JdbcTypeIndicators stdIndicators,
|
JdbcTypeIndicators stdIndicators,
|
||||||
TypeConfiguration typeConfiguration) {
|
TypeConfiguration typeConfiguration) {
|
||||||
final TemporalType requestedTemporalPrecision = stdIndicators.getTemporalPrecision();
|
final TemporalType requestedTemporalPrecision = stdIndicators.getTemporalPrecision();
|
||||||
|
@ -509,13 +516,14 @@ public class InferredBasicValueResolver {
|
||||||
|
|
||||||
final BasicType<T> jdbcMapping = typeConfiguration.getBasicTypeRegistry().resolve( explicitTemporalJtd, jdbcType );
|
final BasicType<T> jdbcMapping = typeConfiguration.getBasicTypeRegistry().resolve( explicitTemporalJtd, jdbcType );
|
||||||
|
|
||||||
|
final MutabilityPlan<T> mutabilityPlan = determineMutabilityPlan( explicitMutabilityPlanAccess, explicitTemporalJtd, typeConfiguration );
|
||||||
return new InferredBasicValueResolution<>(
|
return new InferredBasicValueResolution<>(
|
||||||
jdbcMapping,
|
jdbcMapping,
|
||||||
explicitTemporalJtd,
|
explicitTemporalJtd,
|
||||||
explicitTemporalJtd,
|
explicitTemporalJtd,
|
||||||
jdbcType,
|
jdbcType,
|
||||||
jdbcMapping,
|
jdbcMapping,
|
||||||
explicitTemporalJtd.getMutabilityPlan()
|
mutabilityPlan
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,8 +583,22 @@ public class InferredBasicValueResolver {
|
||||||
basicType.getJavaTypeDescriptor(),
|
basicType.getJavaTypeDescriptor(),
|
||||||
basicType.getJdbcType(),
|
basicType.getJdbcType(),
|
||||||
basicType,
|
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,
|
jdbcType,
|
||||||
resolvedJavaType,
|
resolvedJavaType,
|
||||||
this::determineReflectedJavaType,
|
this::determineReflectedJavaType,
|
||||||
|
explicitMutabilityPlanAccess,
|
||||||
this,
|
this,
|
||||||
getTable(),
|
getTable(),
|
||||||
column,
|
column,
|
||||||
|
|
|
@ -199,7 +199,7 @@ public class MappingModelCreationHelper {
|
||||||
MappingModelCreationProcess creationProcess) {
|
MappingModelCreationProcess creationProcess) {
|
||||||
final SimpleValue value = (SimpleValue) bootProperty.getValue();
|
final SimpleValue value = (SimpleValue) bootProperty.getValue();
|
||||||
final BasicValue.Resolution<?> resolution = ( (Resolvable) value ).resolve();
|
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 FetchTiming fetchTiming;
|
||||||
final FetchStyle fetchStyle;
|
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
|
* 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
|
* @author Steve Ebersole
|
||||||
*/
|
*/
|
||||||
public class ImmutableMutabilityPlan<T> implements MutabilityPlan<T> {
|
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.
|
* 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.orm.test.mapping.converted.converter.mutabiity;
|
package org.hibernate.orm.test.mapping.mutability.attribute;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.hibernate.internal.util.StringHelper;
|
|
||||||
import org.hibernate.internal.util.collections.CollectionHelper;
|
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.jdbc.SQLStatementInspector;
|
||||||
import org.hibernate.testing.orm.junit.DomainModel;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import jakarta.persistence.AttributeConverter;
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Convert;
|
import jakarta.persistence.Convert;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
|
@ -33,41 +31,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* @author Steve Ebersole
|
* @author Steve Ebersole
|
||||||
*/
|
*/
|
||||||
@JiraKey( "HHH-16081" )
|
@JiraKey( "HHH-16081" )
|
||||||
@DomainModel( annotatedClasses = ConvertedMapMutableTests.TestEntity.class )
|
@DomainModel( annotatedClasses = MutableMapAsBasicTests.TestEntity.class )
|
||||||
@SessionFactory( useCollectingStatementInspector = true )
|
@SessionFactory( useCollectingStatementInspector = true )
|
||||||
public class ConvertedMapMutableTests {
|
public class MutableMapAsBasicTests {
|
||||||
|
|
||||||
@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 );
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@JiraKey( "HHH-16132" )
|
@JiraKey( "HHH-16132" )
|
||||||
|
@ -78,17 +44,17 @@ public class ConvertedMapMutableTests {
|
||||||
scope.inTransaction( (session) -> {
|
scope.inTransaction( (session) -> {
|
||||||
final TestEntity managed = session.get( TestEntity.class, 1 );
|
final TestEntity managed = session.get( TestEntity.class, 1 );
|
||||||
statementInspector.clear();
|
statementInspector.clear();
|
||||||
assertThat( managed.values ).hasSize( 2 );
|
assertThat( managed.data ).hasSize( 2 );
|
||||||
// make the change
|
// make the change
|
||||||
managed.values.put( "ghi", "789" );
|
managed.data.put( "ghi", "789" );
|
||||||
} );
|
} );
|
||||||
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
|
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
|
||||||
// make changes to a detached entity and merge it - should trigger update
|
// make changes to a detached entity and merge it - should trigger update
|
||||||
final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) );
|
final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) );
|
||||||
assertThat( loaded.values ).hasSize( 3 );
|
assertThat( loaded.data ).hasSize( 3 );
|
||||||
// make the change
|
// make the change
|
||||||
loaded.values.put( "jkl", "007" );
|
loaded.data.put( "jkl", "007" );
|
||||||
statementInspector.clear();
|
statementInspector.clear();
|
||||||
scope.inTransaction( (session) -> session.merge( loaded ) );
|
scope.inTransaction( (session) -> session.merge( loaded ) );
|
||||||
// the SELECT + UPDATE
|
// the SELECT + UPDATE
|
||||||
|
@ -104,13 +70,13 @@ public class ConvertedMapMutableTests {
|
||||||
scope.inTransaction( (session) -> {
|
scope.inTransaction( (session) -> {
|
||||||
final TestEntity managed = session.get( TestEntity.class, 1 );
|
final TestEntity managed = session.get( TestEntity.class, 1 );
|
||||||
statementInspector.clear();
|
statementInspector.clear();
|
||||||
assertThat( managed.values ).hasSize( 2 );
|
assertThat( managed.data ).hasSize( 2 );
|
||||||
} );
|
} );
|
||||||
assertThat( statementInspector.getSqlQueries() ).isEmpty();
|
assertThat( statementInspector.getSqlQueries() ).isEmpty();
|
||||||
|
|
||||||
// make no changes to a detached entity and merge it - should not trigger update
|
// 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 ) );
|
final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) );
|
||||||
assertThat( loaded.values ).hasSize( 2 );
|
assertThat( loaded.data ).hasSize( 2 );
|
||||||
statementInspector.clear();
|
statementInspector.clear();
|
||||||
scope.inTransaction( (session) -> session.merge( loaded ) );
|
scope.inTransaction( (session) -> session.merge( loaded ) );
|
||||||
// the SELECT
|
// 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" )
|
@Entity( name = "TestEntity" )
|
||||||
@Table( name = "entity_mutable_map" )
|
@Table( name = "entity_mutable_map" )
|
||||||
public static class TestEntity {
|
public static class TestEntity {
|
||||||
@Id
|
@Id
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
||||||
@Convert( converter = MapConverter.class )
|
@Convert( converter = MapConverter.class )
|
||||||
@Column( name = "vals" )
|
private Map<String,String> data;
|
||||||
private Map<String,String> values;
|
|
||||||
|
|
||||||
private TestEntity() {
|
private TestEntity() {
|
||||||
// for use by Hibernate
|
// for use by Hibernate
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestEntity(
|
public TestEntity(Integer id, Map<String,String> data) {
|
||||||
Integer id,
|
|
||||||
Map<String,String> values) {
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.values = values;
|
this.data = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,42 +2,43 @@
|
||||||
* Hibernate, Relational Persistence for Idiomatic Java
|
* Hibernate, Relational Persistence for Idiomatic Java
|
||||||
*
|
*
|
||||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
* 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.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
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.CascadeType;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.OneToMany;
|
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
|
* @author Vlad Mihalcea
|
||||||
*/
|
*/
|
||||||
public class CollectionImmutabilityTest extends BaseEntityManagerFunctionalTestCase {
|
@DomainModel( annotatedClasses = {
|
||||||
|
PluralAttributeMutabilityTest.Batch.class,
|
||||||
@Override
|
PluralAttributeMutabilityTest.Event.class
|
||||||
protected Class<?>[] getAnnotatedClasses() {
|
} )
|
||||||
return new Class<?>[] {
|
@SessionFactory
|
||||||
Batch.class,
|
public class PluralAttributeMutabilityTest {
|
||||||
Event.class
|
private static final Logger log = Logger.getLogger( PluralAttributeMutabilityTest.class );
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test() {
|
public void test(SessionFactoryScope scope) {
|
||||||
//tag::collection-immutability-persist-example[]
|
scope.inTransaction( (entityManager) -> {
|
||||||
doInJPA(this::entityManagerFactory, entityManager -> {
|
//tag::collection-immutability-persist-example[]
|
||||||
Batch batch = new Batch();
|
Batch batch = new Batch();
|
||||||
batch.setId(1L);
|
batch.setId(1L);
|
||||||
batch.setName("Change request");
|
batch.setName("Change request");
|
||||||
|
@ -56,21 +57,27 @@ public class CollectionImmutabilityTest extends BaseEntityManagerFunctionalTestC
|
||||||
batch.getEvents().add(event2);
|
batch.getEvents().add(event2);
|
||||||
|
|
||||||
entityManager.persist(batch);
|
entityManager.persist(batch);
|
||||||
});
|
//end::collection-immutability-persist-example[]
|
||||||
//end::collection-immutability-persist-example[]
|
} );
|
||||||
//tag::collection-entity-update-example[]
|
|
||||||
doInJPA(this::entityManagerFactory, entityManager -> {
|
scope.inTransaction( (entityManager) -> {
|
||||||
|
//tag::collection-entity-update-example[]
|
||||||
Batch batch = entityManager.find(Batch.class, 1L);
|
Batch batch = entityManager.find(Batch.class, 1L);
|
||||||
log.info("Change batch name");
|
log.info("Change batch name");
|
||||||
batch.setName("Proposed change request");
|
batch.setName("Proposed change request");
|
||||||
});
|
//end::collection-entity-update-example[]
|
||||||
//end::collection-entity-update-example[]
|
} );
|
||||||
|
|
||||||
//tag::collection-immutability-update-example[]
|
//tag::collection-immutability-update-example[]
|
||||||
try {
|
try {
|
||||||
doInJPA(this::entityManagerFactory, entityManager -> {
|
//end::collection-immutability-update-example[]
|
||||||
Batch batch = entityManager.find(Batch.class, 1L);
|
scope.inTransaction( (entityManager) -> {
|
||||||
|
//tag::collection-immutability-update-example[]
|
||||||
|
Batch batch = entityManager.find( Batch.class, 1L );
|
||||||
batch.getEvents().clear();
|
batch.getEvents().clear();
|
||||||
});
|
//end::collection-immutability-update-example[]
|
||||||
|
} );
|
||||||
|
//tag::collection-immutability-update-example[]
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
log.error("Immutable collections cannot be modified");
|
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() );
|
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);
|
<T extends StatementInspector> T getStatementInspector(Class<T> type);
|
||||||
SQLStatementInspector getCollectingStatementInspector();
|
SQLStatementInspector getCollectingStatementInspector();
|
||||||
|
|
||||||
|
default void withSessionFactory(Consumer<SessionFactoryImplementor> action) {
|
||||||
|
action.accept( getSessionFactory() );
|
||||||
|
}
|
||||||
|
|
||||||
void inSession(Consumer<SessionImplementor> action);
|
void inSession(Consumer<SessionImplementor> action);
|
||||||
void inTransaction(Consumer<SessionImplementor> action);
|
void inTransaction(Consumer<SessionImplementor> action);
|
||||||
void inTransaction(SessionImplementor session, Consumer<SessionImplementor> action);
|
void inTransaction(SessionImplementor session, Consumer<SessionImplementor> action);
|
||||||
|
|
Loading…
Reference in New Issue