From 4b19bdc619d7d6e57d010504eadaf486d770453a Mon Sep 17 00:00:00 2001 From: Jonathan Bregler Date: Thu, 27 Sep 2018 13:17:09 +0200 Subject: [PATCH] HHH-12995: Querying DECIMAL columns via Double fields can lead to precision loss on SAP HANA - add new configuration parameter hibernate.dialect.hana.treat_double_typed_fields_as_decimal --- .../dialect/AbstractHANADialect.java | 21 ++ .../dialect/functional/HANADecimalTest.java | 257 ++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/dialect/functional/HANADecimalTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java index fc007e70cc..85db531f07 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java @@ -94,6 +94,8 @@ import org.hibernate.type.descriptor.sql.BlobTypeDescriptor; import org.hibernate.type.descriptor.sql.BooleanTypeDescriptor; import org.hibernate.type.descriptor.sql.CharTypeDescriptor; import org.hibernate.type.descriptor.sql.ClobTypeDescriptor; +import org.hibernate.type.descriptor.sql.DecimalTypeDescriptor; +import org.hibernate.type.descriptor.sql.DoubleTypeDescriptor; import org.hibernate.type.descriptor.sql.NCharTypeDescriptor; import org.hibernate.type.descriptor.sql.NClobTypeDescriptor; import org.hibernate.type.descriptor.sql.NVarcharTypeDescriptor; @@ -691,13 +693,22 @@ public abstract class AbstractHANADialect extends Dialect { } } + // Set the LOB prefetch size. LOBs larger than this value will be read into memory as the HANA JDBC driver closes + // the LOB when the result set is closed. private static final String MAX_LOB_PREFETCH_SIZE_PARAMETER_NAME = new String( "hibernate.dialect.hana.max_lob_prefetch_size" ); + // Use TINYINT instead of the native BOOLEAN type private static final String USE_LEGACY_BOOLEAN_TYPE_PARAMETER_NAME = new String( "hibernate.dialect.hana.use_legacy_boolean_type" ); + // Use unicode (NVARCHAR, NCLOB, etc.) instead of non-unicode (VARCHAR, CLOB) string types private static final String USE_UNICODE_STRING_TYPES_PARAMETER_NAME = new String( "hibernate.dialect.hana.use_unicode_string_types" ); + // Read and write double-typed fields as BigDecimal instead of Double to get around precision issues of the HANA + // JDBC driver (https://service.sap.com/sap/support/notes/2590160) + private static final String TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_PARAMETER_NAME = new String( + "hibernate.dialect.hana.treat_double_typed_fields_as_decimal" ); private static final int MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE = 1024; private static final Boolean USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE = Boolean.FALSE; private static final Boolean USE_UNICODE_STRING_TYPES_DEFAULT_VALUE = Boolean.FALSE; + private static final Boolean TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_DEFAULT_VALUE = Boolean.FALSE; private HANANClobTypeDescriptor nClobTypeDescriptor = new HANANClobTypeDescriptor( MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); @@ -708,6 +719,7 @@ public abstract class AbstractHANADialect extends Dialect { private boolean useLegacyBooleanType = USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE.booleanValue(); private boolean useUnicodeStringTypes = USE_UNICODE_STRING_TYPES_DEFAULT_VALUE.booleanValue(); + private boolean treatDoubleTypedFieldsAsDecimal = TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_DEFAULT_VALUE.booleanValue(); /* * Tables named "TYPE" need to be quoted @@ -1127,6 +1139,8 @@ public abstract class AbstractHANADialect extends Dialect { return this.useUnicodeStringTypes ? NVarcharTypeDescriptor.INSTANCE : VarcharTypeDescriptor.INSTANCE; case Types.CHAR: return this.useUnicodeStringTypes ? NCharTypeDescriptor.INSTANCE : CharTypeDescriptor.INSTANCE; + case Types.DOUBLE: + return this.treatDoubleTypedFieldsAsDecimal ? DecimalTypeDescriptor.INSTANCE : DoubleTypeDescriptor.INSTANCE; default: return super.getSqlTypeDescriptorOverride( sqlCode ); } @@ -1565,6 +1579,13 @@ public abstract class AbstractHANADialect extends Dialect { if ( this.useLegacyBooleanType ) { registerColumnType( Types.BOOLEAN, "tinyint" ); } + + this.treatDoubleTypedFieldsAsDecimal = configurationService.getSetting( TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_PARAMETER_NAME, StandardConverters.BOOLEAN, + TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_DEFAULT_VALUE ).booleanValue(); + + if ( this.treatDoubleTypedFieldsAsDecimal ) { + registerHibernateType( Types.DOUBLE, StandardBasicTypes.BIG_DECIMAL.getName() ); + } } public SqlTypeDescriptor getBlobTypeDescriptor() { diff --git a/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/HANADecimalTest.java b/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/HANADecimalTest.java new file mode 100644 index 0000000000..163b965af7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/HANADecimalTest.java @@ -0,0 +1,257 @@ +/* + * 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 . + */ +package org.hibernate.test.dialect.functional; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.hibernate.Session; +import org.hibernate.dialect.AbstractHANADialect; +import org.hibernate.query.Query; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; + +/** + * Tests the correctness of the parameter hibernate.dialect.hana.treat_double_typed_fields_as_decimal which controls the + * handling of double types as either {@link BigDecimal} (parameter is set to true) or {@link Double} (default behavior + * or parameter is set to false) + * + * @author Jonathan Bregler + */ +@RequiresDialect(value = { AbstractHANADialect.class }) +public class HANADecimalTest extends BaseCoreFunctionalTestCase { + + private static final String ENTITY_NAME = "DecimalEntity"; + + @Override + protected void prepareTest() throws Exception { + doInHibernate( this::sessionFactory, localSession -> { + localSession.doWork( connection -> { + try ( PreparedStatement ps = connection + .prepareStatement( "CREATE COLUMN TABLE " + ENTITY_NAME + + " (key INTEGER, doubledouble DOUBLE, decimaldecimal DECIMAL(38,15), doubledecimal DECIMAL(38,15), decimaldouble DOUBLE, PRIMARY KEY (key))" ) ) { + ps.execute(); + } + } ); + } ); + } + + @Override + protected void cleanupTest() throws Exception { + doInHibernate( this::sessionFactory, localSession -> { + localSession.doWork( connection -> { + try ( PreparedStatement ps = connection.prepareStatement( "DROP TABLE " + ENTITY_NAME ) ) { + ps.execute(); + } + catch (Exception e) { + // Ignore + } + } ); + } ); + } + + @Test + @TestForIssue(jiraKey = "HHH-12995") + public void testDecimalTypeFalse() throws Exception { + rebuildSessionFactory( configuration -> { + configuration.setProperty( "hibernate.dialect.hana.treat_double_typed_fields_as_decimal", Boolean.FALSE.toString() ); + } ); + + Session s = openSession(); + s.beginTransaction(); + + DecimalEntity entity = new DecimalEntity(); + entity.key = Integer.valueOf( 1 ); + entity.doubleDouble = 1.19d; + entity.decimalDecimal = BigDecimal.valueOf( 1.19d ); + entity.doubleDecimal = 1.19d; + entity.decimalDouble = BigDecimal.valueOf( 1.19d ); + + s.persist( entity ); + + DecimalEntity entity2 = new DecimalEntity(); + entity2.key = Integer.valueOf( 2 ); + entity2.doubleDouble = 0.3d; + entity2.decimalDecimal = BigDecimal.valueOf( 0.3d ); + entity2.doubleDecimal = 0.3d; + entity2.decimalDouble = BigDecimal.valueOf( 0.3d ); + + s.persist( entity2 ); + + s.flush(); + + s.getTransaction().commit(); + + s.clear(); + + Query legacyQuery = s.createQuery( "select b from " + ENTITY_NAME + " b order by key asc", DecimalEntity.class ); + + List retrievedEntities = legacyQuery.getResultList(); + + assertEquals(2, retrievedEntities.size()); + + DecimalEntity retrievedEntity = retrievedEntities.get( 0 ); + assertEquals( Integer.valueOf( 1 ), retrievedEntity.key ); + assertEquals( 1.19d, retrievedEntity.doubleDouble, 0 ); + assertEquals( new BigDecimal( "1.190000000000000" ), retrievedEntity.decimalDecimal ); + assertEquals( 1.189999999999999d, retrievedEntity.doubleDecimal, 0 ); + assertEquals( new BigDecimal( "1.19" ), retrievedEntity.decimalDouble ); + + retrievedEntity = retrievedEntities.get( 1 ); + assertEquals( Integer.valueOf( 2 ), retrievedEntity.key ); + assertEquals( 0.3d, retrievedEntity.doubleDouble, 0 ); + assertEquals( new BigDecimal( "0.300000000000000" ), retrievedEntity.decimalDecimal ); + assertEquals( 0.299999999999999d, retrievedEntity.doubleDecimal, 0 ); + assertEquals( new BigDecimal( "0.3" ), retrievedEntity.decimalDouble ); + + } + + @Test + @TestForIssue(jiraKey = "HHH-12995") + public void testDecimalTypeDefault() throws Exception { + rebuildSessionFactory(); + + Session s = openSession(); + s.beginTransaction(); + + DecimalEntity entity = new DecimalEntity(); + entity.key = Integer.valueOf( 1 ); + entity.doubleDouble = 1.19d; + entity.decimalDecimal = BigDecimal.valueOf( 1.19d ); + entity.doubleDecimal = 1.19d; + entity.decimalDouble = BigDecimal.valueOf( 1.19d ); + + s.persist( entity ); + + DecimalEntity entity2 = new DecimalEntity(); + entity2.key = Integer.valueOf( 2 ); + entity2.doubleDouble = 0.3d; + entity2.decimalDecimal = BigDecimal.valueOf( 0.3d ); + entity2.doubleDecimal = 0.3d; + entity2.decimalDouble = BigDecimal.valueOf( 0.3d ); + + s.persist( entity2 ); + + s.flush(); + + s.getTransaction().commit(); + + s.clear(); + + Query legacyQuery = s.createQuery( "select b from " + ENTITY_NAME + " b order by key asc", DecimalEntity.class ); + + List retrievedEntities = legacyQuery.getResultList(); + + assertEquals(2, retrievedEntities.size()); + + DecimalEntity retrievedEntity = retrievedEntities.get( 0 ); + assertEquals( Integer.valueOf( 1 ), retrievedEntity.key ); + assertEquals( 1.19d, retrievedEntity.doubleDouble, 0 ); + assertEquals( new BigDecimal( "1.190000000000000" ), retrievedEntity.decimalDecimal ); + assertEquals( 1.189999999999999d, retrievedEntity.doubleDecimal, 0 ); + assertEquals( new BigDecimal( "1.19" ), retrievedEntity.decimalDouble ); + + retrievedEntity = retrievedEntities.get( 1 ); + assertEquals( Integer.valueOf( 2 ), retrievedEntity.key ); + assertEquals( 0.3d, retrievedEntity.doubleDouble, 0 ); + assertEquals( new BigDecimal( "0.300000000000000" ), retrievedEntity.decimalDecimal ); + assertEquals( 0.299999999999999d, retrievedEntity.doubleDecimal, 0 ); + assertEquals( new BigDecimal( "0.3" ), retrievedEntity.decimalDouble ); + } + + @Test + @TestForIssue(jiraKey = "HHH-12995") + public void testDecimalTypeTrue() throws Exception { + rebuildSessionFactory( configuration -> { + configuration.setProperty( "hibernate.dialect.hana.treat_double_typed_fields_as_decimal", Boolean.TRUE.toString() ); + } ); + + Session s = openSession(); + s.beginTransaction(); + + DecimalEntity entity = new DecimalEntity(); + entity.key = Integer.valueOf( 1 ); + entity.doubleDouble = 1.19d; + entity.decimalDecimal = BigDecimal.valueOf( 1.19d ); + entity.doubleDecimal = 1.19d; + entity.decimalDouble = BigDecimal.valueOf( 1.19d ); + + s.persist( entity ); + + DecimalEntity entity2 = new DecimalEntity(); + entity2.key = Integer.valueOf( 2 ); + entity2.doubleDouble = 0.3d; + entity2.decimalDecimal = BigDecimal.valueOf( 0.3d ); + entity2.doubleDecimal = 0.3d; + entity2.decimalDouble = BigDecimal.valueOf( 0.3d ); + + s.persist( entity2 ); + + s.flush(); + + s.getTransaction().commit(); + + s.clear(); + + Query legacyQuery = s.createQuery( "select b from " + ENTITY_NAME + " b order by key asc", DecimalEntity.class ); + + List retrievedEntities = legacyQuery.getResultList(); + + assertEquals(2, retrievedEntities.size()); + + DecimalEntity retrievedEntity = retrievedEntities.get( 0 ); + assertEquals( Integer.valueOf( 1 ), retrievedEntity.key ); + assertEquals( 1.19d, retrievedEntity.doubleDouble, 0 ); + assertEquals( new BigDecimal( "1.190000000000000" ), retrievedEntity.decimalDecimal ); + assertEquals( 1.19d, retrievedEntity.doubleDecimal, 0 ); + assertEquals( new BigDecimal( "1.19" ), retrievedEntity.decimalDouble ); + + retrievedEntity = retrievedEntities.get( 1 ); + assertEquals( Integer.valueOf( 2 ), retrievedEntity.key ); + assertEquals( 0.3d, retrievedEntity.doubleDouble, 0 ); + assertEquals( new BigDecimal( "0.300000000000000" ), retrievedEntity.decimalDecimal ); + assertEquals( 0.3d, retrievedEntity.doubleDecimal, 0 ); + assertEquals( new BigDecimal( "0.3" ), retrievedEntity.decimalDouble ); + } + + @Override + protected boolean createSchema() { + return false; + } + + @Override + protected java.lang.Class[] getAnnotatedClasses() { + return new java.lang.Class[]{ + DecimalEntity.class + }; + } + + @Entity(name = ENTITY_NAME) + public static class DecimalEntity { + + @Id + public Integer key; + + public double doubleDouble; + + public BigDecimal decimalDecimal; + + public double doubleDecimal; + + public BigDecimal decimalDouble; + } + +}