HHH-16081 - Converted collection-as-basic values are considered immutable
HHH-16132 - Dirty checking broken for collection-as-basic mappings (test)
This commit is contained in:
parent
f4e95d96c6
commit
ae238d3032
|
@ -6,18 +6,21 @@
|
||||||
*/
|
*/
|
||||||
package org.hibernate.boot.model.process.internal;
|
package org.hibernate.boot.model.process.internal;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.Immutable;
|
||||||
import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor;
|
import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor;
|
||||||
import org.hibernate.boot.model.convert.spi.ConverterDescriptor;
|
import org.hibernate.boot.model.convert.spi.ConverterDescriptor;
|
||||||
import org.hibernate.boot.model.convert.spi.JpaAttributeConverterCreationContext;
|
import org.hibernate.boot.model.convert.spi.JpaAttributeConverterCreationContext;
|
||||||
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
|
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
|
||||||
import org.hibernate.boot.spi.MetadataBuildingContext;
|
import org.hibernate.boot.spi.MetadataBuildingContext;
|
||||||
import org.hibernate.mapping.BasicValue;
|
import org.hibernate.mapping.BasicValue;
|
||||||
|
import org.hibernate.mapping.Collection;
|
||||||
import org.hibernate.metamodel.mapping.JdbcMapping;
|
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||||
import org.hibernate.type.descriptor.converter.spi.JpaAttributeConverter;
|
|
||||||
import org.hibernate.type.BasicType;
|
import org.hibernate.type.BasicType;
|
||||||
import org.hibernate.type.descriptor.converter.internal.AttributeConverterMutabilityPlanImpl;
|
import org.hibernate.type.descriptor.converter.internal.AttributeConverterMutabilityPlanImpl;
|
||||||
|
import org.hibernate.type.descriptor.converter.spi.JpaAttributeConverter;
|
||||||
import org.hibernate.type.descriptor.java.BasicJavaType;
|
import org.hibernate.type.descriptor.java.BasicJavaType;
|
||||||
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;
|
||||||
|
@ -112,23 +115,14 @@ public class NamedConverterResolution<J> implements BasicValue.Resolution<J> {
|
||||||
? explicitJdbcType
|
? explicitJdbcType
|
||||||
: relationalJtd.getRecommendedJdbcType( sqlTypeIndicators );
|
: relationalJtd.getRecommendedJdbcType( sqlTypeIndicators );
|
||||||
|
|
||||||
final MutabilityPlan<T> explicitMutabilityPlan = explicitMutabilityPlanAccess != null
|
final MutabilityPlan<T> mutabilityPlan = determineMutabilityPlan(
|
||||||
? explicitMutabilityPlanAccess.apply( typeConfiguration )
|
explicitMutabilityPlanAccess,
|
||||||
: null;
|
typeConfiguration,
|
||||||
|
converter,
|
||||||
|
domainJtd
|
||||||
|
);
|
||||||
|
|
||||||
|
return new NamedConverterResolution<>(
|
||||||
final MutabilityPlan<T> mutabilityPlan;
|
|
||||||
if ( explicitMutabilityPlan != null ) {
|
|
||||||
mutabilityPlan = explicitMutabilityPlan;
|
|
||||||
}
|
|
||||||
else if ( ! domainJtd.getMutabilityPlan().isMutable() ) {
|
|
||||||
mutabilityPlan = ImmutableMutabilityPlan.instance();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mutabilityPlan = new AttributeConverterMutabilityPlanImpl<>( converter, true );
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NamedConverterResolution<T>(
|
|
||||||
domainJtd,
|
domainJtd,
|
||||||
relationalJtd,
|
relationalJtd,
|
||||||
jdbcType,
|
jdbcType,
|
||||||
|
@ -138,6 +132,36 @@ public class NamedConverterResolution<J> implements BasicValue.Resolution<J> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static <T> MutabilityPlan<T> determineMutabilityPlan(
|
||||||
|
Function<TypeConfiguration, MutabilityPlan> explicitMutabilityPlanAccess,
|
||||||
|
TypeConfiguration typeConfiguration,
|
||||||
|
JpaAttributeConverter<T, ?> converter,
|
||||||
|
JavaType<T> domainJtd) {
|
||||||
|
//noinspection unchecked
|
||||||
|
final MutabilityPlan<T> explicitMutabilityPlan = explicitMutabilityPlanAccess != null
|
||||||
|
? explicitMutabilityPlanAccess.apply( typeConfiguration )
|
||||||
|
: null;
|
||||||
|
if ( explicitMutabilityPlan != null ) {
|
||||||
|
return explicitMutabilityPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( converter.getConverterJavaType().getJavaTypeClass().isAnnotationPresent( Immutable.class ) ) {
|
||||||
|
return ImmutableMutabilityPlan.instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !domainJtd.getMutabilityPlan().isMutable()
|
||||||
|
&& !isCollection( domainJtd.getJavaTypeClass() ) ) {
|
||||||
|
return ImmutableMutabilityPlan.instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AttributeConverterMutabilityPlanImpl<>( converter, true );
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCollection(Class<?> javaType) {
|
||||||
|
return Collection.class.isAssignableFrom( javaType )
|
||||||
|
|| Map.class.isAssignableFrom( javaType );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private final JavaType<J> domainJtd;
|
private final JavaType<J> domainJtd;
|
||||||
private final JavaType<?> relationalJtd;
|
private final JavaType<?> relationalJtd;
|
||||||
|
|
|
@ -426,6 +426,16 @@ public final class CollectionHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String[] asPairs(Map<String,String> map) {
|
||||||
|
final String[] pairs = new String[ map.size() * 2 ];
|
||||||
|
int i = 0;
|
||||||
|
for ( Map.Entry<String,String> entry : map.entrySet() ) {
|
||||||
|
pairs[i++] = entry.getKey();
|
||||||
|
pairs[i++] = entry.getValue();
|
||||||
|
}
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
public static Properties toProperties(Object... pairs) {
|
public static Properties toProperties(Object... pairs) {
|
||||||
final Properties properties = new Properties();
|
final Properties properties = new Properties();
|
||||||
if ( pairs.length > 0 ) {
|
if ( pairs.length > 0 ) {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
package org.hibernate.type.descriptor.java.spi;
|
package org.hibernate.type.descriptor.java.spi;
|
||||||
|
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.hibernate.collection.spi.CollectionSemantics;
|
import org.hibernate.collection.spi.CollectionSemantics;
|
||||||
import org.hibernate.collection.spi.PersistentCollection;
|
import org.hibernate.collection.spi.PersistentCollection;
|
||||||
|
@ -89,17 +90,34 @@ public class CollectionJavaType<C> extends AbstractClassJavaType<C> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean areEqual(C one, C another) {
|
public boolean areEqual(C one, C another) {
|
||||||
return one == another ||
|
// return one == another ||
|
||||||
(
|
// (
|
||||||
one instanceof PersistentCollection &&
|
// one instanceof PersistentCollection &&
|
||||||
( (PersistentCollection<?>) one ).wasInitialized() &&
|
// ( (PersistentCollection<?>) one ).wasInitialized() &&
|
||||||
( (PersistentCollection<?>) one ).isWrapper( another )
|
// ( (PersistentCollection<?>) one ).isWrapper( another )
|
||||||
) ||
|
// ) ||
|
||||||
(
|
// (
|
||||||
another instanceof PersistentCollection &&
|
// another instanceof PersistentCollection &&
|
||||||
( (PersistentCollection<?>) another ).wasInitialized() &&
|
// ( (PersistentCollection<?>) another ).wasInitialized() &&
|
||||||
( (PersistentCollection<?>) another ).isWrapper( one )
|
// ( (PersistentCollection<?>) another ).isWrapper( one )
|
||||||
);
|
// );
|
||||||
|
|
||||||
|
|
||||||
|
if ( one == another ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( one instanceof PersistentCollection ) {
|
||||||
|
final PersistentCollection pc = (PersistentCollection) one;
|
||||||
|
return pc.wasInitialized() && ( pc.isWrapper( another ) || pc.isDirectlyProvidedCollection( another ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( another instanceof PersistentCollection ) {
|
||||||
|
final PersistentCollection pc = (PersistentCollection) another;
|
||||||
|
return pc.wasInitialized() && ( pc.isWrapper( one ) || pc.isDirectlyProvidedCollection( one ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Objects.equals( one, another );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -136,7 +136,7 @@ public class JavaTypeRegistry implements JavaTypeBaseline.BaselineTarget, Serial
|
||||||
() -> {
|
() -> {
|
||||||
if ( javaType instanceof ParameterizedType ) {
|
if ( javaType instanceof ParameterizedType ) {
|
||||||
final ParameterizedType parameterizedType = (ParameterizedType) javaType;
|
final ParameterizedType parameterizedType = (ParameterizedType) javaType;
|
||||||
final JavaType<J> rawType = findDescriptor( ( parameterizedType ).getRawType() );
|
final JavaType<J> rawType = findDescriptor( parameterizedType.getRawType() );
|
||||||
if ( rawType != null ) {
|
if ( rawType != null ) {
|
||||||
return rawType.createJavaType( parameterizedType, typeConfiguration );
|
return rawType.createJavaType( parameterizedType, typeConfiguration );
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
/*
|
||||||
|
* 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.internal.util.StringHelper;
|
||||||
|
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.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.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
|
||||||
|
*/
|
||||||
|
@JiraKey( "HHH-16081" )
|
||||||
|
@DomainModel( annotatedClasses = ConvertedMapMutableTests.TestEntity.class )
|
||||||
|
@SessionFactory( useCollectingStatementInspector = true )
|
||||||
|
public class ConvertedMapMutableTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMutableMap(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
|
||||||
|
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
final TestEntity loaded = session.get( TestEntity.class, 1 );
|
||||||
|
assertThat( loaded.values ).hasSize( 2 );
|
||||||
|
loaded.values.put( "ghi", "789" );
|
||||||
|
statementInspector.clear();
|
||||||
|
} );
|
||||||
|
assertThat( statementInspector.getSqlQueries() ).isNotEmpty();
|
||||||
|
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
final TestEntity loaded = session.get( TestEntity.class, 1 );
|
||||||
|
assertThat( loaded.values ).hasSize( 3 );
|
||||||
|
statementInspector.clear();
|
||||||
|
} );
|
||||||
|
assertThat( statementInspector.getSqlQueries() ).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMutableMapWithMerge(SessionFactoryScope scope) {
|
||||||
|
final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) );
|
||||||
|
assertThat( loaded.values ).hasSize( 2 );
|
||||||
|
|
||||||
|
loaded.values.put( "ghi", "789" );
|
||||||
|
scope.inTransaction( (session) -> session.merge( loaded ) );
|
||||||
|
|
||||||
|
final TestEntity changed = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) );
|
||||||
|
assertThat( changed.values ).hasSize( 3 );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@JiraKey( "HHH-16132" )
|
||||||
|
void testDirtyChecking(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
|
||||||
|
|
||||||
|
// make changes to a managed entity - should trigger update
|
||||||
|
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() ).hasSize( 1 );
|
||||||
|
|
||||||
|
// make changes to a detached entity and merge it - should trigger update
|
||||||
|
final TestEntity loaded = scope.fromTransaction( (session) -> session.get( TestEntity.class, 1 ) );
|
||||||
|
assertThat( loaded.values ).hasSize( 3 );
|
||||||
|
// make the change
|
||||||
|
loaded.values.put( "jkl", "007" );
|
||||||
|
statementInspector.clear();
|
||||||
|
scope.inTransaction( (session) -> session.merge( loaded ) );
|
||||||
|
// the SELECT + UPDATE
|
||||||
|
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@JiraKey( "HHH-16132" )
|
||||||
|
void testNotDirtyChecking(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
|
||||||
|
|
||||||
|
// make no 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();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MapConverter implements AttributeConverter<Map<String,String>,String> {
|
||||||
|
@Override
|
||||||
|
public String convertToDatabaseColumn(Map<String,String> map) {
|
||||||
|
if ( CollectionHelper.isEmpty( map ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return StringHelper.join( ", ", CollectionHelper.asPairs( map ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String,String> convertToEntityAttribute(String pairs) {
|
||||||
|
if ( StringHelper.isEmpty( pairs ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return CollectionHelper.toMap( StringHelper.split( ", ", pairs ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity( name = "TestEntity" )
|
||||||
|
@Table( name = "entity_mutable_map" )
|
||||||
|
public static class TestEntity {
|
||||||
|
@Id
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Convert( converter = MapConverter.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue