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
This commit is contained in:
Jonathan Bregler 2018-09-27 13:17:09 +02:00 committed by Vlad Mihalcea
parent 6e9c1893a1
commit 4b19bdc619
2 changed files with 278 additions and 0 deletions

View File

@ -94,6 +94,8 @@ import org.hibernate.type.descriptor.sql.BlobTypeDescriptor;
import org.hibernate.type.descriptor.sql.BooleanTypeDescriptor; import org.hibernate.type.descriptor.sql.BooleanTypeDescriptor;
import org.hibernate.type.descriptor.sql.CharTypeDescriptor; import org.hibernate.type.descriptor.sql.CharTypeDescriptor;
import org.hibernate.type.descriptor.sql.ClobTypeDescriptor; 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.NCharTypeDescriptor;
import org.hibernate.type.descriptor.sql.NClobTypeDescriptor; import org.hibernate.type.descriptor.sql.NClobTypeDescriptor;
import org.hibernate.type.descriptor.sql.NVarcharTypeDescriptor; 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" ); 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" ); 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" ); 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 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_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE = Boolean.FALSE;
private static final Boolean USE_UNICODE_STRING_TYPES_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 ); 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 useLegacyBooleanType = USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE.booleanValue();
private boolean useUnicodeStringTypes = USE_UNICODE_STRING_TYPES_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 * Tables named "TYPE" need to be quoted
@ -1127,6 +1139,8 @@ public abstract class AbstractHANADialect extends Dialect {
return this.useUnicodeStringTypes ? NVarcharTypeDescriptor.INSTANCE : VarcharTypeDescriptor.INSTANCE; return this.useUnicodeStringTypes ? NVarcharTypeDescriptor.INSTANCE : VarcharTypeDescriptor.INSTANCE;
case Types.CHAR: case Types.CHAR:
return this.useUnicodeStringTypes ? NCharTypeDescriptor.INSTANCE : CharTypeDescriptor.INSTANCE; return this.useUnicodeStringTypes ? NCharTypeDescriptor.INSTANCE : CharTypeDescriptor.INSTANCE;
case Types.DOUBLE:
return this.treatDoubleTypedFieldsAsDecimal ? DecimalTypeDescriptor.INSTANCE : DoubleTypeDescriptor.INSTANCE;
default: default:
return super.getSqlTypeDescriptorOverride( sqlCode ); return super.getSqlTypeDescriptorOverride( sqlCode );
} }
@ -1565,6 +1579,13 @@ public abstract class AbstractHANADialect extends Dialect {
if ( this.useLegacyBooleanType ) { if ( this.useLegacyBooleanType ) {
registerColumnType( Types.BOOLEAN, "tinyint" ); 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() { public SqlTypeDescriptor getBlobTypeDescriptor() {

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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<DecimalEntity> legacyQuery = s.createQuery( "select b from " + ENTITY_NAME + " b order by key asc", DecimalEntity.class );
List<DecimalEntity> 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<DecimalEntity> legacyQuery = s.createQuery( "select b from " + ENTITY_NAME + " b order by key asc", DecimalEntity.class );
List<DecimalEntity> 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<DecimalEntity> legacyQuery = s.createQuery( "select b from " + ENTITY_NAME + " b order by key asc", DecimalEntity.class );
List<DecimalEntity> 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;
}
}