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:
Steve Ebersole 2023-02-02 12:23:40 -06:00
parent f4e95d96c6
commit ae238d3032
7 changed files with 613 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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