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;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.hibernate.annotations.Immutable;
|
||||
import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor;
|
||||
import org.hibernate.boot.model.convert.spi.ConverterDescriptor;
|
||||
import org.hibernate.boot.model.convert.spi.JpaAttributeConverterCreationContext;
|
||||
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
|
||||
import org.hibernate.boot.spi.MetadataBuildingContext;
|
||||
import org.hibernate.mapping.BasicValue;
|
||||
import org.hibernate.mapping.Collection;
|
||||
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||
import org.hibernate.type.descriptor.converter.spi.JpaAttributeConverter;
|
||||
import org.hibernate.type.BasicType;
|
||||
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.ImmutableMutabilityPlan;
|
||||
import org.hibernate.type.descriptor.java.JavaType;
|
||||
|
@ -112,23 +115,14 @@ public class NamedConverterResolution<J> implements BasicValue.Resolution<J> {
|
|||
? explicitJdbcType
|
||||
: relationalJtd.getRecommendedJdbcType( sqlTypeIndicators );
|
||||
|
||||
final MutabilityPlan<T> explicitMutabilityPlan = explicitMutabilityPlanAccess != null
|
||||
? explicitMutabilityPlanAccess.apply( typeConfiguration )
|
||||
: null;
|
||||
final MutabilityPlan<T> mutabilityPlan = determineMutabilityPlan(
|
||||
explicitMutabilityPlanAccess,
|
||||
typeConfiguration,
|
||||
converter,
|
||||
domainJtd
|
||||
);
|
||||
|
||||
|
||||
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>(
|
||||
return new NamedConverterResolution<>(
|
||||
domainJtd,
|
||||
relationalJtd,
|
||||
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<?> 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) {
|
||||
final Properties properties = new Properties();
|
||||
if ( pairs.length > 0 ) {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
package org.hibernate.type.descriptor.java.spi;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.hibernate.collection.spi.CollectionSemantics;
|
||||
import org.hibernate.collection.spi.PersistentCollection;
|
||||
|
@ -89,17 +90,34 @@ public class CollectionJavaType<C> extends AbstractClassJavaType<C> {
|
|||
|
||||
@Override
|
||||
public boolean areEqual(C one, C another) {
|
||||
return one == another ||
|
||||
(
|
||||
one instanceof PersistentCollection &&
|
||||
( (PersistentCollection<?>) one ).wasInitialized() &&
|
||||
( (PersistentCollection<?>) one ).isWrapper( another )
|
||||
) ||
|
||||
(
|
||||
another instanceof PersistentCollection &&
|
||||
( (PersistentCollection<?>) another ).wasInitialized() &&
|
||||
( (PersistentCollection<?>) another ).isWrapper( one )
|
||||
);
|
||||
// return one == another ||
|
||||
// (
|
||||
// one instanceof PersistentCollection &&
|
||||
// ( (PersistentCollection<?>) one ).wasInitialized() &&
|
||||
// ( (PersistentCollection<?>) one ).isWrapper( another )
|
||||
// ) ||
|
||||
// (
|
||||
// another instanceof PersistentCollection &&
|
||||
// ( (PersistentCollection<?>) another ).wasInitialized() &&
|
||||
// ( (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
|
||||
|
|
|
@ -136,7 +136,7 @@ public class JavaTypeRegistry implements JavaTypeBaseline.BaselineTarget, Serial
|
|||
() -> {
|
||||
if ( javaType instanceof ParameterizedType ) {
|
||||
final ParameterizedType parameterizedType = (ParameterizedType) javaType;
|
||||
final JavaType<J> rawType = findDescriptor( ( parameterizedType ).getRawType() );
|
||||
final JavaType<J> rawType = findDescriptor( parameterizedType.getRawType() );
|
||||
if ( rawType != null ) {
|
||||
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