From 3f95c2eadbe36c56a4a8f3bc063f7c4333a12570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Thu, 26 Jul 2018 10:06:51 +0200 Subject: [PATCH] HHH-7318 Test auto-discovery of result types in native queries --- ...ativeQueryResultTypeAutoDiscoveryTest.java | 596 ++++++++++++++++++ 1 file changed, 596 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/jpa/test/query/NativeQueryResultTypeAutoDiscoveryTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/test/query/NativeQueryResultTypeAutoDiscoveryTest.java b/hibernate-core/src/test/java/org/hibernate/jpa/test/query/NativeQueryResultTypeAutoDiscoveryTest.java new file mode 100644 index 0000000000..d15d2d0395 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/jpa/test/query/NativeQueryResultTypeAutoDiscoveryTest.java @@ -0,0 +1,596 @@ +/* + * 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.jpa.test.query; + +import java.math.BigDecimal; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Month; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; + +import org.hibernate.Hibernate; +import org.hibernate.Session; +import org.hibernate.annotations.Nationalized; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.PostgreSQL81Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.jpa.AvailableSettings; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.spi.Bootstrap; +import org.hibernate.jpa.test.PersistenceUnitDescriptorAdapter; +import org.hibernate.type.AbstractSingleColumnStandardBasicType; +import org.hibernate.type.descriptor.java.BigDecimalTypeDescriptor; +import org.hibernate.type.descriptor.java.BooleanTypeDescriptor; +import org.hibernate.type.descriptor.java.FloatTypeDescriptor; +import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor; +import org.hibernate.type.descriptor.java.StringTypeDescriptor; +import org.hibernate.type.descriptor.sql.BinaryTypeDescriptor; +import org.hibernate.type.descriptor.sql.BitTypeDescriptor; +import org.hibernate.type.descriptor.sql.CharTypeDescriptor; +import org.hibernate.type.descriptor.sql.NumericTypeDescriptor; + +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.SkipForDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.CustomRunner; +import org.hibernate.testing.transaction.TransactionUtil; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.hamcrest.CoreMatchers.instanceOf; + +/** + * Test how the type of results are detected from the JDBC type in native queries, + * when the type is not otherwise explicitly set on the query. + * + * This behavior is (more or less) implemented in implementations of + * {@code org.hibernate.loader.custom.ResultColumnProcessor#performDiscovery(org.hibernate.loader.custom.JdbcResultMetadata, List, List)}. + * + * We use one entity type per JDBC type, just in case some types are not supported in some dialects, + * so that we can more easily disable testing of a particular type in a particular dialect. + */ +@TestForIssue(jiraKey = "HHH-7318") +@RunWith(CustomRunner.class) +public class NativeQueryResultTypeAutoDiscoveryTest { + + private static final Dialect DIALECT = Dialect.getDialect(); + + private SessionFactoryImplementor entityManagerFactory; + + @After + public void cleanupEntityManagerFactory() { + if ( entityManagerFactory != null ) { + entityManagerFactory.close(); + entityManagerFactory = null; + } + } + + @Test + public void commonNumericTypes() { + createEntityManagerFactory( + BooleanEntity.class, + BigintEntity.class, + IntegerEntity.class, + SmallintEntity.class, + DoubleEntity.class, + NumericEntity.class + ); + + doTest( BooleanEntity.class, true ); + doTest( BigintEntity.class, 9223372036854775807L ); + doTest( IntegerEntity.class, 2147483647 ); + doTest( SmallintEntity.class, (short)32767 ); + doTest( DoubleEntity.class, 445146115151.45845 ); + doTest( NumericEntity.class, new BigDecimal( "5464384284258458485484848458.48465843584584684" ) ); + } + + @Test + public void bitType() { + createEntityManagerFactory( BitEntity.class ); + doTest( BitEntity.class, false ); + } + + @Test + // Postgresql turns tinyints into shorts in resultsets, and advertises the type as short in the metadata. + // Not much we can do about that. + @SkipForDialect(PostgreSQL81Dialect.class) + public void tinyintType() { + createEntityManagerFactory( TinyintEntity.class ); + doTest( TinyintEntity.class, (byte)127 ); + } + + @Test + // H2 turns floats into doubles in resultsets, and advertises the type as double in the metadata. + // Not much we can do about that. + @SkipForDialect(H2Dialect.class) + public void floatType() { + createEntityManagerFactory( FloatEntity.class ); + doTest( FloatEntity.class, 15516.125f ); + } + + @Test + // MariaDB/MySQL turn reals into doubles in resultsets, and advertise the type as double in the metadata. + // Not much we can do about that. + @SkipForDialect(MySQLDialect.class) + public void realType() { + createEntityManagerFactory( RealEntity.class ); + doTest( RealEntity.class, 15516.125f ); + } + + @Test + public void decimalType() { + createEntityManagerFactory( DecimalEntity.class ); + doTest( DecimalEntity.class, new BigDecimal( "5464384284258458485484848458.48465843584584684" ) ); + } + + @Test + public void commonTextTypes() { + createEntityManagerFactory( + VarcharEntity.class, + NvarcharEntity.class, + CharEntity.class, + LongvarcharEntity.class + ); + + doTest( VarcharEntity.class, "some text" ); + doTest( NvarcharEntity.class, "some text" ); + doTest( LongvarcharEntity.class, "some text" ); + } + + @Test + // MariaDB/MySQL give a precision of 3 for the column corresponding to the CHAR(1) (go figure), + // leading to Hibernate interpreting the type as String. + @SkipForDialect(MySQLDialect.class) + public void charType() { + createEntityManagerFactory( CharEntity.class ); + doTest( CharEntity.class, 'c' ); + } + + @Test + // Most other dialects define java.sql.Types.CHAR as "CHAR(1)" instead of "CHAR($l)", so they ignore the length + @RequiresDialect(H2Dialect.class) + public void char255Type() { + createEntityManagerFactory( Char255Entity.class ); + doTest( Char255Entity.class, "some text" ); + } + + @Test + public void binaryTypes() { + createEntityManagerFactory( + BinaryEntity.class, + VarbinaryEntity.class, + LongvarbinaryEntity.class + ); + + doTest( BinaryEntity.class, "some text".getBytes() ); + doTest( VarbinaryEntity.class, "some text".getBytes() ); + doTest( LongvarbinaryEntity.class, "some text".getBytes() ); + } + + @Test + // Lobs are apparently handled differently in other dialects, queries return Strings instead of the CLOB/BLOB + @RequiresDialect(H2Dialect.class) + public void lobTypes() { + createEntityManagerFactory( + ClobEntity.class, + BlobEntity.class + ); + + doTest( + ClobEntity.class, + Clob.class, + session -> Hibernate.getLobCreator( session ).createClob( "some text" ) + ); + doTest( + BlobEntity.class, + Blob.class, + session -> Hibernate.getLobCreator( session ).createBlob( "some text".getBytes() ) + ); + } + + @Test + public void dateTimeTypes() { + createEntityManagerFactory( + DateEntity.class, + TimeEntity.class, + TimestampEntity.class + ); + + ZonedDateTime zonedDateTime = ZonedDateTime.of( + 2014, Month.NOVEMBER.getValue(), 15, + 18, 0, 0, 0, + ZoneId.of( "UTC" ) + ); + + doTest( DateEntity.class, new java.sql.Date( zonedDateTime.toInstant().toEpochMilli() ) ); + doTest( TimeEntity.class, new Time( zonedDateTime.toLocalTime().toNanoOfDay() / 1000 ) ); + doTest( TimestampEntity.class, new Timestamp( zonedDateTime.toInstant().toEpochMilli() ) ); + } + + @SuppressWarnings("unchecked") + private , T> void doTest(Class entityType, T testedValue) { + this.doTest( entityType, (Class) testedValue.getClass(), ignored -> testedValue ); + } + + private , T> void doTest(Class entityType, Class testedValueClass, + Function testedValueProvider) { + String entityName = entityManagerFactory.getMetamodel().entity( entityType ).getName(); + // Expecting all entities to use the entity name as table name in these tests, because it's simpler + String tableName = entityName; + + // Create a single record in the test database. + TransactionUtil.doInJPA( () -> entityManagerFactory, em -> { + try { + E entity = entityType.getConstructor().newInstance(); + T testedValue = testedValueProvider.apply( em.unwrap( Session.class ) ); + entity.setTestedProperty( testedValue ); + em.persist( entity ); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new IllegalStateException( "Unexpected checked exception: " + e.getMessage(), e ); + } + } ); + + TransactionUtil.doInJPA( () -> entityManagerFactory, em -> { + // Execute a native query to get the entity that was just created. + Object result = em.createNativeQuery( + "SELECT testedProperty FROM " + tableName + ) + .getSingleResult(); + + Assert.assertThat( result, instanceOf( testedValueClass ) ); + } ); + } + + private void createEntityManagerFactory(Class ... entityTypes) { + cleanupEntityManagerFactory(); + EntityManagerFactoryBuilderImpl entityManagerFactoryBuilder = + (EntityManagerFactoryBuilderImpl) Bootstrap.getEntityManagerFactoryBuilder( + new PersistenceUnitDescriptorAdapter(), + buildSettings( entityTypes ) + ); + entityManagerFactory = entityManagerFactoryBuilder.build().unwrap( SessionFactoryImplementor.class ); + } + + private Map buildSettings(Class ... entityTypes) { + Map settings = new HashMap<>(); + + settings.put( org.hibernate.cfg.AvailableSettings.HBM2DDL_AUTO, "create-drop" ); + settings.put( org.hibernate.cfg.AvailableSettings.DIALECT, DIALECT.getClass().getName() ); + settings.put( AvailableSettings.LOADED_CLASSES, Arrays.asList( entityTypes ) ); + + return settings; + } + + @MappedSuperclass + static abstract class TestedEntity { + private Long id; + + protected T testedProperty; + + @Id + @GeneratedValue + public Long getId() { + return id; + } + + protected void setId(Long id) { + this.id = id; + } + + // Only define the setter here, not the getter, so that subclasses can define the @Type/@Column themselves + public void setTestedProperty(T value) { + this.testedProperty = value; + } + } + + @Entity(name = "bigintEntity") + public static class BigintEntity extends TestedEntity { + public Long getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "integerEntity") + public static class IntegerEntity extends TestedEntity { + public Integer getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "smallintEntity") + public static class SmallintEntity extends TestedEntity { + public Short getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "tinyintEntity") + public static class TinyintEntity extends TestedEntity { + public Byte getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "doubleEntity") + public static class DoubleEntity extends TestedEntity { + public Double getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "floatEntity") + public static class FloatEntity extends TestedEntity { + public Float getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "realEntity") + @TypeDef(name = FloatAsRealType.NAME, typeClass = FloatAsRealType.class) + public static class RealEntity extends TestedEntity { + /** + * The custom type sets the SQL type to {@link java.sql.Types#REAL} + * instead of the default {@link java.sql.Types#FLOAT}. + */ + @Type(type = FloatAsRealType.NAME) + public Float getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "numericEntity") + public static class NumericEntity extends TestedEntity { + @Column(precision = 50, scale = 15) + public BigDecimal getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "decimalEntity") + @TypeDef(name = BigDecimalAsDecimalType.NAME, typeClass = BigDecimalAsDecimalType.class) + public static class DecimalEntity extends TestedEntity { + /** + * The custom type sets the SQL type to {@link java.sql.Types#DECIMAL} + * instead of the default {@link java.sql.Types#NUMERIC}. + */ + @Type(type = BigDecimalAsDecimalType.NAME) + @Column(precision = 50, scale = 15) + public BigDecimal getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "varcharEntity") + public static class VarcharEntity extends TestedEntity { + public String getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "nvarcharEntity") + public static class NvarcharEntity extends TestedEntity { + @Nationalized + public String getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "charEntity") + public static class CharEntity extends TestedEntity { + @Column(length = 1) + public Character getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "char255Entity") + @TypeDef(name = StringAsNonVarCharType.NAME, typeClass = StringAsNonVarCharType.class) + public static class Char255Entity extends TestedEntity { + /** + * The custom type sets the SQL type to {@link java.sql.Types#CHAR} + * instead of the default {@link java.sql.Types#VARCHAR}. + */ + @Type(type = StringAsNonVarCharType.NAME) + @Column(length = 255) + public String getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "longvarcharEntity") + public static class LongvarcharEntity extends TestedEntity { + /** + * The custom type sets the SQL type to {@link java.sql.Types#LONGVARCHAR} + * instead of the default {@link java.sql.Types#VARCHAR}. + */ + @Type(type = "text") + public String getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "binaryEntity") + @TypeDef(name = ByteArrayAsNonVarBinaryType.NAME, typeClass = ByteArrayAsNonVarBinaryType.class) + public static class BinaryEntity extends TestedEntity { + /** + * The custom type sets the SQL type to {@link java.sql.Types#BINARY} + * instead of the default {@link java.sql.Types#VARBINARY}. + */ + @Type(type = ByteArrayAsNonVarBinaryType.NAME) + public byte[] getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "varbinaryEntity") + public static class VarbinaryEntity extends TestedEntity { + public byte[] getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "longvarbinaryEntity") + public static class LongvarbinaryEntity extends TestedEntity { + /** + * The custom type sets the SQL type to {@link java.sql.Types#LONGVARBINARY} + * instead of the default {@link java.sql.Types#VARBINARY}. + */ + @Type(type = "image") + public byte[] getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "clobEntity") + public static class ClobEntity extends TestedEntity { + public Clob getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "blobEntity") + public static class BlobEntity extends TestedEntity { + public Blob getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "dateEntity") + public static class DateEntity extends TestedEntity { + public java.sql.Date getTestedProperty() { + return testedProperty; + } + } + + @Entity(name = "timeEntity") + public static class TimeEntity extends TestedEntity