From 68a40425b188d07d0faee21e087b5b5bacf632e5 Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Mon, 17 Jul 2017 23:00:14 +0200 Subject: [PATCH] HHH-11176: Add support for Tuple results for native queries --- .../AbstractSharedSessionContract.java | 16 +- .../spi/CriteriaQueryTupleTransformer.java | 1 + .../jpa/spi/NativeQueryTupleTransformer.java | 119 +++++ .../jpa/test/query/TupleNativeQueryTest.java | 406 ++++++++++++++++++ 4 files changed, 540 insertions(+), 2 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java create mode 100644 hibernate-core/src/test/java/org/hibernate/jpa/test/query/TupleNativeQueryTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 3b8629eb14..60287e9aee 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -61,6 +61,7 @@ import org.hibernate.engine.transaction.spi.TransactionImplementor; import org.hibernate.id.uuid.StandardRandomStrategy; import org.hibernate.jpa.internal.util.FlushModeTypeHelper; +import org.hibernate.jpa.spi.NativeQueryTupleTransformer; import org.hibernate.jpa.spi.TupleBuilderTransformer; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.procedure.ProcedureCall; @@ -781,7 +782,7 @@ protected QueryImplementor createQuery(NamedQueryDefinition namedQueryDef @SuppressWarnings({"WeakerAccess", "unchecked"}) protected NativeQueryImplementor createNativeQuery(NamedSQLQueryDefinition queryDefinition, Class resultType) { - if ( resultType != null ) { + if ( resultType != null && !Tuple.class.equals(resultType)) { resultClassChecking( resultType, queryDefinition ); } @@ -790,6 +791,9 @@ protected NativeQueryImplementor createNativeQuery(NamedSQLQueryDefinition q this, factory.getQueryPlanCache().getSQLParameterMetadata( queryDefinition.getQueryString(), false ) ); + if (Tuple.class.equals(resultType)) { + query.setResultTransformer(new NativeQueryTupleTransformer()); + } query.setHibernateFlushMode( queryDefinition.getFlushMode() ); query.setComment( queryDefinition.getComment() != null ? queryDefinition.getComment() : queryDefinition.getName() ); if ( queryDefinition.getLockOptions() != null ) { @@ -876,7 +880,7 @@ public NativeQueryImplementor createNativeQuery(String sqlString, Class resultCl try { NativeQueryImplementor query = createNativeQuery( sqlString ); - query.addEntity( "alias1", resultClass.getName(), LockMode.READ ); + handleNativeQueryResult(query, resultClass); return query; } catch ( RuntimeException he ) { @@ -884,6 +888,14 @@ public NativeQueryImplementor createNativeQuery(String sqlString, Class resultCl } } + private void handleNativeQueryResult(NativeQueryImplementor query, Class resultClass) { + if (Tuple.class.equals(resultClass)) { + query.setResultTransformer(new NativeQueryTupleTransformer()); + } else { + query.addEntity( "alias1", resultClass.getName(), LockMode.READ ); + } + } + @Override public NativeQueryImplementor createNativeQuery(String sqlString, String resultSetMapping) { checkOpen(); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/CriteriaQueryTupleTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/CriteriaQueryTupleTransformer.java index e92182a334..41c3f1d9de 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/CriteriaQueryTupleTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/CriteriaQueryTupleTransformer.java @@ -140,6 +140,7 @@ public X get(int i, Class type) { } public Object[] toArray() { + // todo : make a copy? return tuples; } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java new file mode 100644 index 0000000000..81b216a613 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java @@ -0,0 +1,119 @@ +package org.hibernate.jpa.spi; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.persistence.Tuple; +import javax.persistence.TupleElement; + +import org.hibernate.HibernateException; +import org.hibernate.transform.BasicTransformerAdapter; + +/** + * ResultTransformer adapter for handling Tuple results from Native queries + * + * @author Arnold Galovics + */ +public class NativeQueryTupleTransformer extends BasicTransformerAdapter { + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + return new NativeTupleImpl( tuple, aliases ); + } + + private static class NativeTupleElementImpl implements TupleElement { + + private final Class javaType; + + private final String alias; + + public NativeTupleElementImpl(Class javaType, String alias) { + this.javaType = javaType; + this.alias = alias; + } + + @Override + public Class getJavaType() { + return javaType; + } + + @Override + public String getAlias() { + return alias; + } + } + + private static class NativeTupleImpl implements Tuple { + + private Object[] tuple; + + private Map aliasToValue = new LinkedHashMap<>(); + + public NativeTupleImpl(Object[] tuple, String[] aliases) { + if ( tuple == null || aliases == null || tuple.length != aliases.length ) { + throw new HibernateException( "Got different size of tuples and aliases" ); + } + this.tuple = tuple; + for ( int i = 0; i < tuple.length; i++ ) { + aliasToValue.put( aliases[i].toLowerCase(), tuple[i] ); + } + } + + @Override + public X get(String alias, Class type) { + final Object untyped = get( alias ); + + return ( untyped != null ) ? type.cast( untyped ) : null; + } + + @Override + public Object get(String alias) { + Object tupleElement = aliasToValue.get( alias.toLowerCase() ); + + if ( tupleElement == null ) { + throw new IllegalArgumentException( "Unknown alias [" + alias + "]" ); + } + return tupleElement; + } + + @Override + public X get(int i, Class type) { + final Object untyped = get( i ); + + return ( untyped != null ) ? type.cast( untyped ) : null; + } + + @Override + public Object get(int i) { + if ( i < 0 ) { + throw new IllegalArgumentException( "requested tuple index must be greater than zero" ); + } + if ( i >= aliasToValue.size() ) { + throw new IllegalArgumentException( "requested tuple index exceeds actual tuple size" ); + } + return tuple[i]; + } + + @Override + public Object[] toArray() { + // todo : make a copy? + return tuple; + } + + @Override + public List> getElements() { + List> elements = new ArrayList<>( aliasToValue.size() ); + + for ( Map.Entry entry : aliasToValue.entrySet() ) { + elements.add( new NativeTupleElementImpl<>( entry.getValue().getClass(), entry.getKey() ) ); + } + return elements; + } + + @Override + public X get(TupleElement tupleElement) { + return get( tupleElement.getAlias(), tupleElement.getJavaType() ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/test/query/TupleNativeQueryTest.java b/hibernate-core/src/test/java/org/hibernate/jpa/test/query/TupleNativeQueryTest.java new file mode 100644 index 0000000000..e78c4c471e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/jpa/test/query/TupleNativeQueryTest.java @@ -0,0 +1,406 @@ +package org.hibernate.jpa.test.query; + +import org.hibernate.dialect.H2Dialect; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; +import org.hibernate.testing.RequiresDialect; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.persistence.*; +import javax.persistence.criteria.CriteriaDelete; +import java.math.BigInteger; +import java.util.List; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +@RequiresDialect(H2Dialect.class) +public class TupleNativeQueryTest extends BaseEntityManagerFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{User.class}; + } + + @Before + public void setUp() { + doInJPA(this::entityManagerFactory, entityManager -> { + User user = new User("Arnold"); + entityManager.persist(user); + }); + } + + @After + public void tearDown() { + doInJPA(this::entityManagerFactory, entityManager -> { + CriteriaDelete delete = entityManager.getCriteriaBuilder().createCriteriaDelete(User.class); + delete.from(User.class); + entityManager.createQuery(delete).executeUpdate(); + }); + } + + @Test + public void testPositionalGetterShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get(0)); + assertEquals("Arnold", tuple.get(1)); + }); + } + + @Test + public void testPositionalGetterWithClassShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get(0, BigInteger.class)); + assertEquals("Arnold", tuple.get(1, String.class)); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterShouldThrowExceptionWhenLessThanZeroGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + tuple.get(-1); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithClassShouldThrowExceptionWhenLessThanZeroGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + tuple.get(-1); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterShouldThrowExceptionWhenTupleSizePositionGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + tuple.get(2); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithClassShouldThrowExceptionWhenTupleSizePositionGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + tuple.get(2); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterShouldThrowExceptionWhenExceedingPositionGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + tuple.get(3); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithClassShouldThrowExceptionWhenExceedingPositionGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + tuple.get(3); + }); + } + + + @Test + public void testAliasGetterWithoutExplicitAliasShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get("ID")); + assertEquals("Arnold", tuple.get("FIRSTNAME")); + }); + } + + + public void testAliasGetterShouldWorkWithoutExplicitAliasWhenLowerCaseAliasGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + tuple.get("id"); + }); + } + + @Test(expected = IllegalArgumentException.class) + public void testAliasGetterShouldThrowExceptionWithoutExplicitAliasWhenWrongAliasGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + tuple.get("e"); + }); + } + + @Test + public void testAliasGetterWithClassWithoutExplicitAliasShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleResult(entityManager); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get("ID", BigInteger.class)); + assertEquals("Arnold", tuple.get("FIRSTNAME", String.class)); + }); + } + + + @Test + public void testAliasGetterWithExplicitAliasShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleAliasedResult(entityManager); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get("ALIAS1")); + assertEquals("Arnold", tuple.get("ALIAS2")); + }); + } + + @Test + public void testAliasGetterWithClassWithExplicitAliasShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = getTupleAliasedResult(entityManager); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get("ALIAS1", BigInteger.class)); + assertEquals("Arnold", tuple.get("ALIAS2", String.class)); + }); + } + + @Test + public void testToArrayShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List tuples = getTupleResult(entityManager); + Object[] result = tuples.get(0).toArray(); + assertArrayEquals(new Object[]{BigInteger.ONE, "Arnold"}, result); + }); + } + + @Test + public void testGetElementsShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List tuples = getTupleResult(entityManager); + List> result = tuples.get(0).getElements(); + assertEquals(2, result.size()); + assertEquals(BigInteger.class, result.get(0).getJavaType()); + assertEquals("id", result.get(0).getAlias()); + assertEquals(String.class, result.get(1).getJavaType()); + assertEquals("firstname", result.get(1).getAlias()); + }); + } + + @Test + public void testPositionalGetterWithNamedNativeQueryShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get(0)); + assertEquals("Arnold", tuple.get(1)); + }); + } + + @Test + public void testPositionalGetterWithNamedNativeQueryWithClassShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get(0, BigInteger.class)); + assertEquals("Arnold", tuple.get(1, String.class)); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithNamedNativeQueryShouldThrowExceptionWhenLessThanZeroGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + tuple.get(-1); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithNamedNativeQueryWithClassShouldThrowExceptionWhenLessThanZeroGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + tuple.get(-1); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithNamedNativeQueryShouldThrowExceptionWhenTupleSizePositionGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + tuple.get(2); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithNamedNativeQueryWithClassShouldThrowExceptionWhenTupleSizePositionGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + tuple.get(2); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithNamedNativeQueryShouldThrowExceptionWhenExceedingPositionGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + tuple.get(3); + }); + } + + + @Test(expected = IllegalArgumentException.class) + public void testPositionalGetterWithNamedNativeQueryWithClassShouldThrowExceptionWhenExceedingPositionGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + tuple.get(3); + }); + } + + + @Test + public void testAliasGetterWithNamedNativeQueryWithoutExplicitAliasShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get("ID")); + assertEquals("Arnold", tuple.get("FIRSTNAME")); + }); + } + + + public void testAliasGetterWithNamedNativeQueryShouldWorkWithoutExplicitAliasWhenLowerCaseAliasGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + tuple.get("id"); + }); + } + + @Test(expected = IllegalArgumentException.class) + public void testAliasGetterWithNamedNativeQueryShouldThrowExceptionWithoutExplicitAliasWhenWrongAliasGiven() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + tuple.get("e"); + }); + } + + @Test + public void testAliasGetterWithNamedNativeQueryWithClassWithoutExplicitAliasShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get("ID", BigInteger.class)); + assertEquals("Arnold", tuple.get("FIRSTNAME", String.class)); + }); + } + + + @Test + public void testAliasGetterWithNamedNativeQueryWithExplicitAliasShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard_with_alias", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get("ALIAS1")); + assertEquals("Arnold", tuple.get("ALIAS2")); + }); + } + + @Test + public void testAliasGetterWithNamedNativeQueryWithClassWithExplicitAliasShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List result = entityManager.createNamedQuery("standard_with_alias", Tuple.class).getResultList(); + Tuple tuple = result.get(0); + assertEquals(BigInteger.ONE, tuple.get("ALIAS1", BigInteger.class)); + assertEquals("Arnold", tuple.get("ALIAS2", String.class)); + }); + } + + @Test + public void testToArrayShouldWithNamedNativeQueryWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List tuples = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + Object[] result = tuples.get(0).toArray(); + assertArrayEquals(new Object[]{BigInteger.ONE, "Arnold"}, result); + }); + } + + @Test + public void testGetElementsWithNamedNativeQueryShouldWorkProperly() { + doInJPA(this::entityManagerFactory, entityManager -> { + List tuples = entityManager.createNamedQuery("standard", Tuple.class).getResultList(); + List> result = tuples.get(0).getElements(); + assertEquals(2, result.size()); + assertEquals(BigInteger.class, result.get(0).getJavaType()); + assertEquals("id", result.get(0).getAlias()); + assertEquals(String.class, result.get(1).getJavaType()); + assertEquals("firstname", result.get(1).getAlias()); + }); + } + + @SuppressWarnings("unchecked") + private List getTupleAliasedResult(EntityManager entityManager) { + Query query = entityManager.createNativeQuery("SELECT id AS alias1, firstname AS alias2 FROM users", Tuple.class); + return (List) query.getResultList(); + } + + + @SuppressWarnings("unchecked") + private List getTupleResult(EntityManager entityManager) { + Query query = entityManager.createNativeQuery("SELECT id, firstname FROM users", Tuple.class); + return (List) query.getResultList(); + } + + @Entity + @Table(name = "users") + @NamedNativeQueries({ + @NamedNativeQuery( + name = "standard", + query = "SELECT id, firstname FROM users" + ), + @NamedNativeQuery( + name = "standard_with_alias", + query = "SELECT id AS alias1, firstname AS alias2 FROM users" + ) + }) + public static class User { + @Id + private long id; + + private String firstName; + + public User() { + } + + public User(String firstName) { + this.id = 1L; + this.firstName = firstName; + } + } +}