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:
parent
6e9c1893a1
commit
4b19bdc619
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue