From 86d50bee2ed4a6852b035183d218e5c214622b29 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 15 Feb 2016 13:15:43 +0100 Subject: [PATCH] DATAJPA-864 - Projection execution now considers constructor expressions in manually defined queries. Previously we triggered a tuple query execution even if a query was manually defined and contained a constructor expression (e.g. new Dto(a.foo, a.bar)). We now explicitly detect that case and simply execute the query as is. --- .../query/AbstractStringBasedJpaQuery.java | 39 +++++++++++-------- .../jpa/repository/query/NativeJpaQuery.java | 10 +++-- .../data/jpa/repository/query/QueryUtils.java | 31 ++++++++++++++- .../jpa/repository/query/StringQuery.java | 18 +++++++-- .../query/StringQueryUnitTests.java | 25 ++++++++---- 5 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index a6127c3ee..776177a19 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2014 the original author or authors. + * Copyright 2008-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,22 +91,6 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { evaluationContextProvider, parser); } - /** - * Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery} - * type. - * - * @param queryString - * @return - */ - public Query createJpaQuery(String queryString) { - - ResultProcessor resultFactory = getQueryMethod().getResultProcessor(); - ReturnedType returnedType = resultFactory.getReturnedType(); - EntityManager em = getEntityManager(); - - return returnedType.isProjecting() ? em.createQuery(queryString, Tuple.class) : em.createQuery(queryString); - } - /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(java.lang.Object[]) @@ -134,4 +118,25 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { public StringQuery getCountQuery() { return countQuery; } + + /** + * Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery} + * type. + * + * @param queryString + * @return + */ + protected Query createJpaQuery(String queryString) { + + EntityManager em = getEntityManager(); + + if (this.query.hasConstructorExpression()) { + return em.createQuery(queryString); + } + + ResultProcessor resultFactory = getQueryMethod().getResultProcessor(); + ReturnedType returnedType = resultFactory.getReturnedType(); + + return returnedType.isProjecting() ? em.createQuery(queryString, Tuple.class) : em.createQuery(queryString); + } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index 3b4693ca9..46f08fdac 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; * {@link Query} from it. * * @author Thomas Darimont + * @author Oliver Gierke */ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { @@ -61,8 +62,9 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { * @see org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery#createJpaQuery(java.lang.String) */ @Override - public Query createJpaQuery(String queryString) { - return getQueryMethod().isQueryForEntity() ? getEntityManager().createNativeQuery(queryString, - getQueryMethod().getReturnedObjectType()) : getEntityManager().createNativeQuery(queryString); + protected Query createJpaQuery(String queryString) { + return getQueryMethod().isQueryForEntity() + ? getEntityManager().createNativeQuery(queryString, getQueryMethod().getReturnedObjectType()) + : getEntityManager().createNativeQuery(queryString); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index c748caa5d..4ccfa569c 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2015 the original author or authors. + * Copyright 2008-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -93,6 +93,8 @@ public abstract class QueryUtils { private static final Pattern NAMED_PARAMETER = Pattern.compile(":" + IDENTIFIER + "|\\#" + IDENTIFIER, CASE_INSENSITIVE); + private static final Pattern CONSTRUCTOR_EXPRESSION; + private static final Map> ASSOCIATION_TYPES; private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 2; @@ -127,6 +129,19 @@ public abstract class QueryUtils { persistentAttributeTypes.put(ELEMENT_COLLECTION, null); ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes); + + builder = new StringBuilder(); + builder.append("select"); + builder.append("\\s+"); // at least one space separating + builder.append("new"); + builder.append("\\s+"); // at least one space separating + builder.append(IDENTIFIER); + builder.append("\\s*"); // zero to unlimited space separating + builder.append("\\("); + builder.append(".*"); + builder.append("\\)"); + + CONSTRUCTOR_EXPRESSION = compile(builder.toString(), CASE_INSENSITIVE); } /** @@ -430,6 +445,20 @@ public abstract class QueryUtils { return orders; } + /** + * Returns whether the given JPQL query contains a constructor expression. + * + * @param query must not be {@literal null} or empty. + * @return + * @since 1.10 + */ + public static boolean hasConstructorExpression(String query) { + + Assert.hasText(query, "Query must not be null or empty!"); + + return CONSTRUCTOR_EXPRESSION.matcher(query).find(); + } + /** * Creates a criteria API {@link javax.persistence.criteria.Order} from the given {@link Order}. * diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index a688afa0e..e1c3f5142 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ class StringQuery { private final String query; private final List bindings; private final String alias; + private final boolean hasConstructorExpression; /** * Creates a new {@link StringQuery} from the given JPQL query. @@ -59,6 +60,7 @@ class StringQuery { this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, this.bindings); this.alias = QueryUtils.detectAlias(query); + this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query); } /** @@ -133,6 +135,16 @@ class StringQuery { throw new IllegalArgumentException(String.format("No parameter binding found for position %s!", position)); } + /** + * Returns whether the query is using a constructor expression. + * + * @return + * @since 1.10 + */ + public boolean hasConstructorExpression() { + return hasConstructorExpression; + } + /** * A parser that extracts the parameter bindings from a given query string. * @@ -328,8 +340,8 @@ class StringQuery { } /** - * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@keyword - * #AS_IS} in case no other {@link ParameterBindingType} could be found. + * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@keyword #AS_IS} in + * case no other {@link ParameterBindingType} could be found. * * @param typeSource * @return diff --git a/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java index 1a0b7530d..c1dd919f9 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -312,16 +312,16 @@ public class StringQueryUnitTests { public void rejectsDifferentBindingsForRepeatedParameter2() { new StringQuery("select u from User u where u.firstname like ?1 and u.lastname like %?1"); } - + /** * @see @DATAJPA-712 */ @Test public void shouldReplaceAllNamedExpressionParametersWithInClause() { - + StringQuery query = new StringQuery("select a from A a where a.b in :#{#bs} and a.c in :#{#cs}"); String queryString = query.getQueryString(); - + assertThat(queryString, is("select a from A a where a.b in :__$synthetic$__1 and a.c in :__$synthetic$__2")); } @@ -330,13 +330,24 @@ public class StringQueryUnitTests { */ @Test public void shouldReplaceAllPositionExpressionParametersWithInClause() { - + StringQuery query = new StringQuery("select a from A a where a.b in ?#{#bs} and a.c in ?#{#cs}"); String queryString = query.getQueryString(); - + assertThat(queryString, is("select a from A a where a.b in ?1 and a.c in ?2")); } - + + /** + * @see DATAJPA-864 + */ + @Test + public void detectsConstructorExpressions() { + + assertThat(new StringQuery("select new Dto(a.foo, a.bar) from A a").hasConstructorExpression(), is(true)); + assertThat(new StringQuery("select new Dto (a.foo, a.bar) from A a").hasConstructorExpression(), is(true)); + assertThat(new StringQuery("select a from A a").hasConstructorExpression(), is(false)); + } + private void assertPositionalBinding(Class bindingType, Integer position, ParameterBinding expectedBinding) {