From 7bcf161d36d5b1e59fba1ae32e8e2d692c2fd33d Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Mon, 19 Aug 2013 14:53:56 -0500 Subject: [PATCH] HHH-8111 - AttributeConverter doesn't override built-in type mappings --- .../org/hibernate/mapping/SimpleValue.java | 160 +++++++------ ...buteConverterSqlTypeDescriptorAdapter.java | 121 ++++++++++ .../descriptor/converter/package-info.java | 5 + .../java/JavaTypeDescriptorRegistry.java | 20 +- .../java/SerializableTypeDescriptor.java | 31 ++- .../sql/JdbcTypeJavaClassMappings.java | 9 +- .../test/type/AttributeConverterTest.java | 211 +++++++++++++++++- 7 files changed, 449 insertions(+), 108 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/converter/AttributeConverterSqlTypeDescriptorAdapter.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/converter/package-info.java diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java index 57220006eb..6f660262bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java @@ -26,10 +26,6 @@ package org.hibernate.mapping; import javax.persistence.AttributeConverter; import java.lang.annotation.Annotation; import java.lang.reflect.TypeVariable; -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -52,13 +48,9 @@ import org.hibernate.id.factory.IdentifierGeneratorFactory; import org.hibernate.internal.util.ReflectHelper; import org.hibernate.type.AbstractSingleColumnStandardBasicType; import org.hibernate.type.Type; -import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.ValueExtractor; -import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.converter.AttributeConverterSqlTypeDescriptorAdapter; import org.hibernate.type.descriptor.java.JavaTypeDescriptor; import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry; -import org.hibernate.type.descriptor.sql.BasicBinder; -import org.hibernate.type.descriptor.sql.BasicExtractor; import org.hibernate.type.descriptor.sql.JdbcTypeJavaClassMappings; import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; import org.hibernate.type.descriptor.sql.SqlTypeDescriptorRegistry; @@ -348,7 +340,6 @@ public class SimpleValue implements KeyValue { return result; } - @SuppressWarnings("unchecked") public void setTypeUsingReflection(String className, String propertyName) throws MappingException { // NOTE : this is called as the last piece in setting SimpleValue type information, and implementations // rely on that fact, using it as a signal that all information it is going to get is defined at this point... @@ -374,34 +365,89 @@ public class SimpleValue implements KeyValue { } // we had an AttributeConverter... + type = buildAttributeConverterTypeAdapter(); + } - // todo : we should validate the number of columns present - // todo : ultimately I want to see attributeConverterJavaType and attributeConverterJdbcTypeCode specify-able separately - // then we can "play them against each other" in terms of determining proper typing - // todo : see if we already have previously built a custom on-the-fly BasicType for this AttributeConverter; see note below about caching + /** + * Build a Hibernate Type that incorporates the JPA AttributeConverter. AttributeConverter works totally in + * memory, meaning it converts between one Java representation (the entity attribute representation) and another + * (the value bound into JDBC statements or extracted from results). However, the Hibernate Type system operates + * at the lower level of actually dealing directly with those JDBC objects. So even though we have an + * AttributeConverter, we still need to "fill out" the rest of the BasicType data and bridge calls + * to bind/extract through the converter. + *

+ * Essentially the idea here is that an intermediate Java type needs to be used. Let's use an example as a means + * to illustrate... Consider an {@code AttributeConverter}. This tells Hibernate that the domain + * model defines this attribute as an Integer value (the 'entityAttributeJavaType'), but that we need to treat the + * value as a String (the 'databaseColumnJavaType') when dealing with JDBC (aka, the database type is a + * VARCHAR/CHAR):

+ * + * For the JavaTypeDescriptor portion we simply resolve the "entity attribute representation" part of + // the AttributeConverter to resolve the corresponding descriptor. For the SqlTypeDescriptor portion we use the + // "database column representation" part of the AttributeConverter to resolve the "recommended" JDBC type-code + // and use that type-code to resolve the SqlTypeDescriptor to use. + * + * + *

+ * + *

+ *

+ * + * @todo : ultimately I want to see attributeConverterJavaType and attributeConverterJdbcTypeCode specify-able separately + * then we can "play them against each other" in terms of determining proper typing + * + * @todo : see if we already have previously built a custom on-the-fly BasicType for this AttributeConverter; see note below about caching + * + * @return The built AttributeConverter -> Type adapter + */ + @SuppressWarnings("unchecked") + private Type buildAttributeConverterTypeAdapter() { + // todo : validate the number of columns present here? - // AttributeConverter works totally in memory, meaning it converts between one Java representation (the entity - // attribute representation) and another (the value bound into JDBC statements or extracted from results). - // However, the Hibernate Type system operates at the lower level of actually dealing with those JDBC objects. - // So even though we have an AttributeConverter, we still need to "fill out" the rest of the BasicType - // data. For the JavaTypeDescriptor portion we simply resolve the "entity attribute representation" part of - // the AttributeConverter to resolve the corresponding descriptor. For the SqlTypeDescriptor portion we use the - // "database column representation" part of the AttributeConverter to resolve the "recommended" JDBC type-code - // and use that type-code to resolve the SqlTypeDescriptor to use. final Class entityAttributeJavaType = jpaAttributeConverterDefinition.getEntityAttributeType(); final Class databaseColumnJavaType = jpaAttributeConverterDefinition.getDatabaseColumnType(); - final int jdbcTypeCode = JdbcTypeJavaClassMappings.INSTANCE.determineJdbcTypeCodeForJavaClass( databaseColumnJavaType ); - final JavaTypeDescriptor javaTypeDescriptor = JavaTypeDescriptorRegistry.INSTANCE.getDescriptor( entityAttributeJavaType ); + + // resolve the JavaTypeDescriptor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // For the JavaTypeDescriptor portion we simply resolve the "entity attribute representation" part of + // the AttributeConverter to resolve the corresponding descriptor. + final JavaTypeDescriptor entityAttributeJavaTypeDescriptor = JavaTypeDescriptorRegistry.INSTANCE.getDescriptor( entityAttributeJavaType ); + + + // build the SqlTypeDescriptor adapter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Going back to the illustration, this should be a SqlTypeDescriptor that handles the Integer <-> String + // conversions. This is the more complicated piece. First we need to determine the JDBC type code + // corresponding to the AttributeConverter's declared "databaseColumnJavaType" (how we read that value out + // of ResultSets). See JdbcTypeJavaClassMappings for details. Again, given example, this should return + // VARCHAR/CHAR + final int jdbcTypeCode = JdbcTypeJavaClassMappings.INSTANCE.determineJdbcTypeCodeForJavaClass( databaseColumnJavaType ); + // find the standard SqlTypeDescriptor for that JDBC type code. final SqlTypeDescriptor sqlTypeDescriptor = SqlTypeDescriptorRegistry.INSTANCE.getDescriptor( jdbcTypeCode ); - // the adapter here injects the AttributeConverter calls into the binding/extraction process... + // find the JavaTypeDescriptor representing the "intermediate database type representation". Back to the + // illustration, this should be the type descriptor for Strings + final JavaTypeDescriptor intermediateJavaTypeDescriptor = JavaTypeDescriptorRegistry.INSTANCE.getDescriptor( databaseColumnJavaType ); + // and finally construct the adapter, which injects the AttributeConverter calls into the binding/extraction + // process... final SqlTypeDescriptor sqlTypeDescriptorAdapter = new AttributeConverterSqlTypeDescriptorAdapter( jpaAttributeConverterDefinition.getAttributeConverter(), - sqlTypeDescriptor + sqlTypeDescriptor, + intermediateJavaTypeDescriptor ); + final String name = "BasicType adapter for AttributeConverter<" + entityAttributeJavaType + "," + databaseColumnJavaType + ">"; - type = new AbstractSingleColumnStandardBasicType( sqlTypeDescriptorAdapter, javaTypeDescriptor ) { + final Type type = new AbstractSingleColumnStandardBasicType( sqlTypeDescriptorAdapter, entityAttributeJavaTypeDescriptor ) { @Override public String getName() { return name; @@ -410,6 +456,8 @@ public class SimpleValue implements KeyValue { log.debug( "Created : " + name ); // todo : cache the BasicType we just created in case that AttributeConverter is applied multiple times. + + return type; } private Class extractType(TypeVariable typeVariable) { @@ -461,64 +509,6 @@ public class SimpleValue implements KeyValue { this.jpaAttributeConverterDefinition = jpaAttributeConverterDefinition; } - public static class AttributeConverterSqlTypeDescriptorAdapter implements SqlTypeDescriptor { - private final AttributeConverter converter; - private final SqlTypeDescriptor delegate; - - public AttributeConverterSqlTypeDescriptorAdapter(AttributeConverter converter, SqlTypeDescriptor delegate) { - this.converter = converter; - this.delegate = delegate; - } - - @Override - public int getSqlType() { - return delegate.getSqlType(); - } - - @Override - public boolean canBeRemapped() { - return delegate.canBeRemapped(); - } - - @Override - public ValueBinder getBinder(JavaTypeDescriptor javaTypeDescriptor) { - final ValueBinder realBinder = delegate.getBinder( javaTypeDescriptor ); - return new BasicBinder( javaTypeDescriptor, this ) { - @Override - @SuppressWarnings("unchecked") - protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) - throws SQLException { - realBinder.bind( st, converter.convertToDatabaseColumn( value ), index, options ); - } - }; - } - - @Override - public ValueExtractor getExtractor(JavaTypeDescriptor javaTypeDescriptor) { - final ValueExtractor realExtractor = delegate.getExtractor( javaTypeDescriptor ); - return new BasicExtractor( javaTypeDescriptor, this ) { - @Override - @SuppressWarnings("unchecked") - protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { - return (X) converter.convertToEntityAttribute( realExtractor.extract( rs, name, options ) ); - } - - @Override - @SuppressWarnings("unchecked") - protected X doExtract(CallableStatement statement, int index, WrapperOptions options) - throws SQLException { - return (X) converter.convertToEntityAttribute( realExtractor.extract( statement, index, options ) ); - } - - @Override - @SuppressWarnings("unchecked") - protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { - return (X) converter.convertToEntityAttribute( realExtractor.extract( statement, new String[] {name}, options ) ); - } - }; - } - } - private void createParameterImpl() { try { String[] columnsNames = new String[columns.size()]; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/converter/AttributeConverterSqlTypeDescriptorAdapter.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/converter/AttributeConverterSqlTypeDescriptorAdapter.java new file mode 100644 index 0000000000..80c8511ee2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/converter/AttributeConverterSqlTypeDescriptorAdapter.java @@ -0,0 +1,121 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2013, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.type.descriptor.converter; + +import javax.persistence.AttributeConverter; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.sql.BasicBinder; +import org.hibernate.type.descriptor.sql.BasicExtractor; +import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; + +/** + * Adapter for incorporating JPA {@link AttributeConverter} handling into the SqlTypeDescriptor contract. + *

+ * Essentially this is responsible for mapping to/from the intermediate database type representation. Continuing the + * {@code AttributeConverter} example from + * {@link org.hibernate.mapping.SimpleValue#buildAttributeConverterTypeAdapter()}, the "intermediate database type + * representation" would be the String representation. So on binding, we convert the incoming Integer to String; + * on extraction we extract the value as String and convert to Integer. + * + * @author Steve Ebersole + */ +public class AttributeConverterSqlTypeDescriptorAdapter implements SqlTypeDescriptor { + private final AttributeConverter converter; + private final SqlTypeDescriptor delegate; + private final JavaTypeDescriptor intermediateJavaTypeDescriptor; + + public AttributeConverterSqlTypeDescriptorAdapter( + AttributeConverter converter, + SqlTypeDescriptor delegate, + JavaTypeDescriptor intermediateJavaTypeDescriptor) { + this.converter = converter; + this.delegate = delegate; + this.intermediateJavaTypeDescriptor = intermediateJavaTypeDescriptor; + } + + @Override + public int getSqlType() { + return delegate.getSqlType(); + } + + @Override + public boolean canBeRemapped() { + // todo : consider the ramifications of this. + // certainly we need to account for the remapping of the delegate sql-type, but is it really valid to + // allow remapping of the converter sql-type? + return delegate.canBeRemapped(); + } + + + // Binding ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + @Override + @SuppressWarnings("unchecked") + public ValueBinder getBinder(JavaTypeDescriptor javaTypeDescriptor) { + // Get the binder for the intermediate type representation + final ValueBinder realBinder = delegate.getBinder( intermediateJavaTypeDescriptor ); + return new BasicBinder( javaTypeDescriptor, this ) { + @Override + @SuppressWarnings("unchecked") + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + realBinder.bind( st, converter.convertToDatabaseColumn( value ), index, options ); + } + }; + } + + + // Extraction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + @Override + public ValueExtractor getExtractor(JavaTypeDescriptor javaTypeDescriptor) { + final ValueExtractor realExtractor = delegate.getExtractor( intermediateJavaTypeDescriptor ); + return new BasicExtractor( javaTypeDescriptor, this ) { + @Override + @SuppressWarnings("unchecked") + protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { + return (X) converter.convertToEntityAttribute( realExtractor.extract( rs, name, options ) ); + } + + @Override + @SuppressWarnings("unchecked") + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return (X) converter.convertToEntityAttribute( realExtractor.extract( statement, index, options ) ); + } + + @Override + @SuppressWarnings("unchecked") + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return (X) converter.convertToEntityAttribute( realExtractor.extract( statement, new String[] {name}, options ) ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/converter/package-info.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/converter/package-info.java new file mode 100644 index 0000000000..2f67826f26 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/converter/package-info.java @@ -0,0 +1,5 @@ +/** + * Support for handling JPA {@link javax.persistence.AttributeConverter} instances as part of the + * Hibernate {@link org.hibernate.type.Type} system. + */ +package org.hibernate.type.descriptor.converter; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JavaTypeDescriptorRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JavaTypeDescriptorRegistry.java index 48e94a1dca..d5b1da2fac 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JavaTypeDescriptorRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JavaTypeDescriptorRegistry.java @@ -28,6 +28,8 @@ import java.util.Comparator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jboss.logging.Logger; + import org.hibernate.HibernateException; import org.hibernate.type.descriptor.WrapperOptions; @@ -37,6 +39,8 @@ import org.hibernate.type.descriptor.WrapperOptions; * @author Steve Ebersole */ public class JavaTypeDescriptorRegistry { + private static final Logger log = Logger.getLogger( JavaTypeDescriptorRegistry.class ); + public static final JavaTypeDescriptorRegistry INSTANCE = new JavaTypeDescriptorRegistry(); private ConcurrentHashMap descriptorsByClass = new ConcurrentHashMap(); @@ -61,20 +65,20 @@ public class JavaTypeDescriptorRegistry { return descriptor; } + if ( Serializable.class.isAssignableFrom( cls ) ) { + return new SerializableTypeDescriptor( cls ); + } + // find the first "assignable" match for ( Map.Entry entry : descriptorsByClass.entrySet() ) { - if ( cls.isAssignableFrom( entry.getKey() ) ) { + if ( entry.getKey().isAssignableFrom( cls ) ) { + log.debugf( "Using cached JavaTypeDescriptor instance for Java class [%s]", cls.getName() ); return entry.getValue(); } } - // we could not find one; warn the user (as stuff is likely to break later) and create a fallback instance... - if ( Serializable.class.isAssignableFrom( cls ) ) { - return new SerializableTypeDescriptor( cls ); - } - else { - return new FallbackJavaTypeDescriptor( cls ); - } + log.warnf( "Could not find matching type descriptor for requested Java class [%s]; using fallback", cls.getName() ); + return new FallbackJavaTypeDescriptor( cls ); } public static class FallbackJavaTypeDescriptor extends AbstractTypeDescriptor { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/SerializableTypeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/SerializableTypeDescriptor.java index 493a5247e5..fc87fb9773 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/SerializableTypeDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/SerializableTypeDescriptor.java @@ -101,33 +101,48 @@ public class SerializableTypeDescriptor extends Abstract public X unwrap(T value, Class type, WrapperOptions options) { if ( value == null ) { return null; - } else if ( byte[].class.isAssignableFrom( type ) ) { + } + else if ( type.isInstance( value ) ) { + return (X) value; + } + else if ( byte[].class.isAssignableFrom( type ) ) { return (X) toBytes( value ); - } else if ( InputStream.class.isAssignableFrom( type ) ) { + } + else if ( InputStream.class.isAssignableFrom( type ) ) { return (X) new ByteArrayInputStream( toBytes( value ) ); - } else if ( BinaryStream.class.isAssignableFrom( type ) ) { + } + else if ( BinaryStream.class.isAssignableFrom( type ) ) { return (X) new BinaryStreamImpl( toBytes( value ) ); - } else if ( Blob.class.isAssignableFrom( type )) { + } + else if ( Blob.class.isAssignableFrom( type )) { return (X) options.getLobCreator().createBlob( toBytes(value) ); } throw unknownUnwrap( type ); } + @SuppressWarnings("unchecked") public T wrap(X value, WrapperOptions options) { if ( value == null ) { return null; - } else if ( byte[].class.isInstance( value ) ) { + } + else if ( byte[].class.isInstance( value ) ) { return fromBytes( (byte[]) value ); - } else if ( InputStream.class.isInstance( value ) ) { + } + else if ( InputStream.class.isInstance( value ) ) { return fromBytes( DataHelper.extractBytes( (InputStream) value ) ); - } else if ( Blob.class.isInstance( value )) { + } + else if ( Blob.class.isInstance( value )) { try { return fromBytes( DataHelper.extractBytes( ( (Blob) value ).getBinaryStream() ) ); - } catch ( SQLException e ) { + } + catch ( SQLException e ) { throw new HibernateException(e); } } + else if ( getJavaTypeClass().isInstance( value ) ) { + return (T) value; + } throw unknownWrap( value.getClass() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/JdbcTypeJavaClassMappings.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/JdbcTypeJavaClassMappings.java index 9d894b5343..e837f1b521 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/JdbcTypeJavaClassMappings.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/JdbcTypeJavaClassMappings.java @@ -41,9 +41,12 @@ import org.jboss.logging.Logger; import org.hibernate.mapping.Array; /** - * Presents recommended {@literal JDCB typecode <-> Java Class} mappings. Currently the recommendations - * contained here come from the JDBC spec itself, as outlined at - * Eventually, the plan is to have {@link org.hibernate.dialect.Dialect} contribute this information. + * Presents recommended {@literal JDCB typecode <-> Java Class} mappings. Currently the mappings contained here come + * from the recommendations defined by the JDBC spec itself, as outlined at + * . + *

+ * Eventually, the plan is to have {@link org.hibernate.dialect.Dialect} and + * {@link java.sql.DatabaseMetaData#getTypeInfo()} contribute this information. * * @author Steve Ebersole */ diff --git a/hibernate-core/src/test/java/org/hibernate/test/type/AttributeConverterTest.java b/hibernate-core/src/test/java/org/hibernate/test/type/AttributeConverterTest.java index a1ab1ced22..895367f738 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/type/AttributeConverterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/type/AttributeConverterTest.java @@ -28,11 +28,16 @@ import javax.persistence.Convert; import javax.persistence.Converter; import javax.persistence.Entity; import javax.persistence.Id; +import java.io.Serializable; import java.sql.Clob; +import java.sql.Timestamp; import java.sql.Types; import org.hibernate.IrrelevantEntity; +import org.hibernate.Session; +import org.hibernate.SessionFactory; import org.hibernate.cfg.AttributeConverterDefinition; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; @@ -40,6 +45,7 @@ import org.hibernate.mapping.SimpleValue; import org.hibernate.type.AbstractStandardBasicType; import org.hibernate.type.BasicType; import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.JdbcTimestampTypeDescriptor; import org.hibernate.type.descriptor.java.StringTypeDescriptor; import org.junit.Test; @@ -75,7 +81,7 @@ public class AttributeConverterTest extends BaseUnitTestCase { } @Test - public void testNormalOperation() { + public void testBasicConverterApplication() { Configuration cfg = new Configuration(); cfg.addAttributeConverter( StringClobConverter.class, true ); cfg.addAnnotatedClass( Tester.class ); @@ -107,15 +113,105 @@ public class AttributeConverterTest extends BaseUnitTestCase { } } + @Test + public void testBasicUsage() { + Configuration cfg = new Configuration(); + cfg.addAttributeConverter( IntegerToVarcharConverter.class, false ); + cfg.addAnnotatedClass( Tester4.class ); + cfg.setProperty( AvailableSettings.HBM2DDL_AUTO, "create-drop" ); + cfg.setProperty( AvailableSettings.GENERATE_STATISTICS, "true" ); - @Entity + SessionFactory sf = cfg.buildSessionFactory(); + + try { + Session session = sf.openSession(); + session.beginTransaction(); + session.save( new Tester4( 1L, "steve", 200 ) ); + session.getTransaction().commit(); + session.close(); + + sf.getStatistics().clear(); + session = sf.openSession(); + session.beginTransaction(); + session.get( Tester4.class, 1L ); + session.getTransaction().commit(); + session.close(); + assertEquals( 0, sf.getStatistics().getEntityUpdateCount() ); + + session = sf.openSession(); + session.beginTransaction(); + Tester4 t4 = (Tester4) session.get( Tester4.class, 1L ); + t4.code = 300; + session.getTransaction().commit(); + session.close(); + + session = sf.openSession(); + session.beginTransaction(); + t4 = (Tester4) session.get( Tester4.class, 1L ); + assertEquals( 300, t4.code.longValue() ); + session.delete( t4 ); + session.getTransaction().commit(); + session.close(); + } + finally { + sf.close(); + } + } + + @Test + public void testBasicTimestampUsage() { + Configuration cfg = new Configuration(); + cfg.addAttributeConverter( InstantConverter.class, false ); + cfg.addAnnotatedClass( IrrelevantInstantEntity.class ); + cfg.setProperty( AvailableSettings.HBM2DDL_AUTO, "create-drop" ); + cfg.setProperty( AvailableSettings.GENERATE_STATISTICS, "true" ); + + SessionFactory sf = cfg.buildSessionFactory(); + + try { + Session session = sf.openSession(); + session.beginTransaction(); + session.save( new IrrelevantInstantEntity( 1L ) ); + session.getTransaction().commit(); + session.close(); + + sf.getStatistics().clear(); + session = sf.openSession(); + session.beginTransaction(); + IrrelevantInstantEntity e = (IrrelevantInstantEntity) session.get( IrrelevantInstantEntity.class, 1L ); + session.getTransaction().commit(); + session.close(); + assertEquals( 0, sf.getStatistics().getEntityUpdateCount() ); + + session = sf.openSession(); + session.beginTransaction(); + session.delete( e ); + session.getTransaction().commit(); + session.close(); + } + finally { + sf.close(); + } + } + + // Entity declarations used in the test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + @Entity(name = "T1") public static class Tester { @Id private Long id; private String name; + + public Tester() { + } + + public Tester(Long id, String name) { + this.id = id; + this.name = name; + } } - @Entity + @Entity(name = "T2") public static class Tester2 { @Id private Long id; @@ -123,14 +219,94 @@ public class AttributeConverterTest extends BaseUnitTestCase { private String name; } - @Entity + @Entity(name = "T3") public static class Tester3 { @Id private Long id; @org.hibernate.annotations.Type( type = "string" ) + @Convert(disableConversion = true) private String name; } + @Entity(name = "T4") + public static class Tester4 { + @Id + private Long id; + private String name; + @Convert( converter = IntegerToVarcharConverter.class ) + private Integer code; + + public Tester4() { + } + + public Tester4(Long id, String name, Integer code) { + this.id = id; + this.name = name; + this.code = code; + } + } + + // This class is for mimicking an Instant from Java 8, which a converter might convert to a java.sql.Timestamp + public static class Instant implements Serializable { + private static final long serialVersionUID = 1L; + + private long javaMillis; + + public Instant(long javaMillis) { + this.javaMillis = javaMillis; + } + + public long toJavaMillis() { + return javaMillis; + } + + public static Instant fromJavaMillis(long javaMillis) { + return new Instant( javaMillis ); + } + + public static Instant now() { + return new Instant( System.currentTimeMillis() ); + } + } + + @Entity + public static class IrrelevantInstantEntity { + @Id + private Long id; + private Instant dateCreated; + + public IrrelevantInstantEntity() { + } + + public IrrelevantInstantEntity(Long id) { + this( id, Instant.now() ); + } + + public IrrelevantInstantEntity(Long id, Instant dateCreated) { + this.id = id; + this.dateCreated = dateCreated; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Instant getDateCreated() { + return dateCreated; + } + + public void setDateCreated(Instant dateCreated) { + this.dateCreated = dateCreated; + } + } + + + // Converter declarations used in the test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @Converter( autoApply = true ) public static class StringClobConverter implements AttributeConverter { @Override @@ -143,4 +319,31 @@ public class AttributeConverterTest extends BaseUnitTestCase { return null; } } + + @Converter( autoApply = true ) + public static class IntegerToVarcharConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(Integer attribute) { + return attribute == null ? null : attribute.toString(); + } + + @Override + public Integer convertToEntityAttribute(String dbData) { + return dbData == null ? null : Integer.valueOf( dbData ); + } + } + + + @Converter( autoApply = true ) + public static class InstantConverter implements AttributeConverter { + @Override + public Timestamp convertToDatabaseColumn(Instant attribute) { + return new Timestamp( attribute.toJavaMillis() ); + } + + @Override + public Instant convertToEntityAttribute(Timestamp dbData) { + return Instant.fromJavaMillis( dbData.getTime() ); + } + } }