From 285a994e95a16c56b3f2dd8a72609f0796ad58f7 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 13 Mar 2013 13:26:21 +0100 Subject: [PATCH] DATAJPA-292 - Manually defined queries can contain like expressions now. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces support for JPQL inline like expressions such as "… like ?1%". The implementation will detect those expressions and turn the query back into a valid JPQL one. On query execution the parameters will be bound according to the like expression defined in the query. --- src/docbkx/jpa.xml | 25 ++ .../jpa/repository/query/SimpleJpaQuery.java | 30 +- .../jpa/repository/query/StringQuery.java | 353 ++++++++++++++++++ .../query/StringQueryParameterBinder.java | 95 +++++ .../jpa/repository/UserRepositoryTests.java | 28 ++ .../query/LikeBindingUnitTests.java | 94 +++++ .../query/StringQueryUnitTests.java | 76 ++++ .../jpa/repository/sample/UserRepository.java | 12 + 8 files changed, 701 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/StringQueryParameterBinder.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java diff --git a/src/docbkx/jpa.xml b/src/docbkx/jpa.xml index 293021e3d..edf577990 100644 --- a/src/docbkx/jpa.xml +++ b/src/docbkx/jpa.xml @@ -508,6 +508,31 @@ public class User { } + + Using advanced <code>LIKE</code> expressions + + The query execution mechanism for manually defined queries using + @Query allow the definition of advanced + LIKE expressions inside the query definition. + + + Advanced <code>LIKE</code> expressions in + <interfacename>@Query</interfacename> + + public interface UserRepository extends JpaRepository<User, Long> { + + @Query("select u from User u where u.firstname like %?1") + List<User> findByFirstnameEndsWith(String firstname); +} + + + In the just shown sample LIKE delimiter character + % is recognized and the query transformed into a valid + JPQL query (removing the %). Upon query execution the + parameter handed into the method call gets augmented with the + previously recognized LIKE pattern. + + Native queries diff --git a/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index 0ba422464..8423541a1 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -37,9 +37,8 @@ final class SimpleJpaQuery extends AbstractJpaQuery { private static final Logger LOG = LoggerFactory.getLogger(SimpleJpaQuery.class); - private final String queryString; - private final String countQuery; - private final String alias; + private final StringQuery query; + private final StringQuery countQuery; private final JpaQueryMethod method; @@ -50,11 +49,10 @@ final class SimpleJpaQuery extends AbstractJpaQuery { super(method, em); - this.queryString = queryString; - this.alias = QueryUtils.detectAlias(queryString); - this.countQuery = method.getCountQuery() == null ? QueryUtils.createCountQueryFor(queryString) : method - .getCountQuery(); this.method = method; + this.query = new StringQuery(queryString); + this.countQuery = new StringQuery(method.getCountQuery() == null ? QueryUtils.createCountQueryFor(queryString) + : method.getCountQuery()); Parameters parameters = method.getParameters(); boolean hasPagingOrSortingParameter = parameters.hasPageableParameter() || parameters.hasSortParameter(); @@ -66,7 +64,7 @@ final class SimpleJpaQuery extends AbstractJpaQuery { // Try to create a Query object already to fail fast if (!method.isNativeQuery()) { try { - em.createQuery(queryString); + em.createQuery(query.getQuery()); } catch (RuntimeException e) { // Needed as there's ambiguities in how an invalid query string shall be expressed by the persistence provider // http://java.net/projects/jpa-spec/lists/jsr338-experts/archive/2012-07/message/17 @@ -82,6 +80,15 @@ final class SimpleJpaQuery extends AbstractJpaQuery { this(method, em, method.getAnnotatedQuery()); } + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#createBinder(java.lang.Object[]) + */ + @Override + protected ParameterBinder createBinder(Object[] values) { + return new StringQueryParameterBinder(getQueryMethod().getParameters(), values, query); + } + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#createQuery(java.lang.Object[]) @@ -90,7 +97,7 @@ final class SimpleJpaQuery extends AbstractJpaQuery { public Query doCreateQuery(Object[] values) { ParameterAccessor accessor = new ParametersParameterAccessor(method.getParameters(), values); - String sortedQueryString = QueryUtils.applySorting(queryString, accessor.getSort(), alias); + String sortedQueryString = QueryUtils.applySorting(query.getQuery(), accessor.getSort(), query.getAlias()); EntityManager em = getEntityManager(); Query query = null; @@ -111,8 +118,7 @@ final class SimpleJpaQuery extends AbstractJpaQuery { */ @Override protected TypedQuery doCreateCountQuery(Object[] values) { - - return createBinder(values).bind(getEntityManager().createQuery(countQuery, Long.class)); + return createBinder(values).bind(getEntityManager().createQuery(countQuery.getQuery(), Long.class)); } /** @@ -131,4 +137,4 @@ final class SimpleJpaQuery extends AbstractJpaQuery { return query == null ? null : new SimpleJpaQuery(queryMethod, em, query); } -} \ No newline at end of file +} 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 new file mode 100644 index 000000000..81a86da68 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -0,0 +1,353 @@ +/* + * Copyright 2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static java.util.regex.Pattern.*; +import static org.springframework.util.ObjectUtils.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Encapsulation of a String JPA query. + * + * @author Oliver Gierke + */ +class StringQuery { + + private static final Pattern LIKE_PATTERN; + private static final String MESSAGE = "Already found like binding with same index / parameter name but differing binding type! Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same like binding."; + + static { + + StringBuilder builder = new StringBuilder(); + builder.append("(?<=like)"); // starts with like + builder.append("(?: )+"); // some whitespace + builder.append("("); + builder.append("%?(\\?(\\d+))%?"); // position parameter with likes + builder.append("|"); // or + builder.append("%?(:(\\w+))%?"); // named parameter with likes; + builder.append(")"); + LIKE_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); + } + + private final String query; + private final List bindings; + private final String alias; + + /** + * Creates a new {@link StringQuery} from the given JPQL query. + * + * @param query must not be {@literal null} or empty. + */ + public StringQuery(String query) { + + Assert.hasText(query, "Query must not be null or empty!"); + + this.bindings = new ArrayList(); + this.query = parseLikeBindings(query); + this.alias = QueryUtils.detectAlias(query); + } + + /** + * Returns whether we have found some like bindings. + * + * @return + */ + public boolean hasLikeBindings() { + return !bindings.isEmpty(); + } + + /** + * Returns the {@link LikeBinding}s registered. + * + * @return + */ + List getLikeBindings() { + return bindings; + } + + /** + * Returns the JPQL query. + * + * @return + */ + public String getQuery() { + return query; + } + + /** + * Returns the main alias used in the query. + * + * @return the alias + */ + public String getAlias() { + return alias; + } + + /** + * Returns the {@link LikeBinding} for the given name. + * + * @param name + * @return + */ + public LikeBinding getBindingFor(String name) { + + for (StringQuery.LikeBinding binding : bindings) { + if (binding.hasName(name)) { + return binding; + } + } + + return null; + } + + /** + * Returns the {@link LikeBinding} for the given position. + * + * @param position + * @return + */ + public LikeBinding getBindingFor(int position) { + for (LikeBinding binding : bindings) { + if (binding.hasPosition(position)) { + return binding; + } + } + + return null; + } + + /** + * Parses {@link LikeBinding} instances from the given query and adds them to the registered bindings. Returns the + * cleaned up query. + * + * @param query + * @return + */ + private final String parseLikeBindings(String query) { + + Matcher matcher = LIKE_PATTERN.matcher(query); + String result = query; + + while (matcher.find()) { + + Type likeType = getLikeTypeFrom(matcher.group(1)); + String index = matcher.group(3); + String replacement = matcher.group(2); + + if (index != null) { + checkAndRegister(new LikeBinding(Integer.parseInt(index), likeType)); + } else { + checkAndRegister(new LikeBinding(matcher.group(5), likeType)); + replacement = matcher.group(4); + } + + result = StringUtils.replace(result, matcher.group(1), replacement); + } + + return result; + } + + private final void checkAndRegister(LikeBinding binding) { + + for (LikeBinding existing : bindings) { + if (existing.hasName(binding.name) || existing.hasPosition(binding.position)) { + Assert.isTrue(existing.equals(binding), String.format(MESSAGE, existing, binding)); + } + } + + this.bindings.add(binding); + } + + /** + * Extracts the like {@link Type} from the given JPA like expression. + * + * @param expression must not be {@literal null} or empty. + * @return + */ + private static Type getLikeTypeFrom(String expression) { + + Assert.hasText(expression); + + if (expression.matches("%.*%")) { + return Type.CONTAINING; + } + + if (expression.startsWith("%")) { + return Type.ENDING_WITH; + } + + if (expression.endsWith("%")) { + return Type.STARTING_WITH; + } + + throw new IllegalArgumentException(String.format("Illegal like pattern %s!", expression)); + } + + /** + * Represents a paramter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE + * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA. + * + * @author Oliver Gierke + */ + static class LikeBinding { + + private static final List SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, + Type.ENDING_WITH); + + private final String name; + private final Integer position; + private final Type type; + + /** + * Creates a new {@link LikeBinding} for the parameter with the given name and {@link Type}. + * + * @param name must not be {@literal null} or empty. + * @param type must not be {@literal null}. + */ + public LikeBinding(String name, Type type) { + + Assert.hasText(name, "Name must not be null or empty!"); + Assert.notNull(type, "Type must not be null!"); + Assert.isTrue(SUPPORTED_TYPES.contains(type), + String.format("Type must be one of %s!", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES))); + + this.name = name; + this.type = type; + this.position = null; + } + + /** + * Creates a new {@link LikeBinding} for the parameter with the given position and {@link Type}. + * + * @param position + * @param type must not be {@literal null}. + */ + public LikeBinding(int position, Type type) { + + Assert.isTrue(position > 0, "Position must be greater than zero!"); + Assert.notNull(type, "Type must not be null!"); + + this.position = position; + this.type = type; + this.name = null; + } + + /** + * Returns whether the binding has the given name. Will always be {@literal false} in case the {@link LikeBinding} + * has been set up from a position. + * + * @param name + * @return + */ + public boolean hasName(String name) { + return this.position == null && this.name != null && this.name.equals(name); + } + + /** + * Returns whether the binding has the given position. Will always be {@literal false} in case the + * {@link LikeBinding} has been set up from a name. + * + * @param position + * @return + */ + public boolean hasPosition(int position) { + return this.name == null && this.position == position; + } + + /** + * Returns the type of the {@link LikeBinding}. + * + * @return + */ + public Type getType() { + return type; + } + + /** + * Prepares the given raw value according to the like type. + * + * @param value + */ + public Object prepare(Object value) { + + if (value == null) { + return value; + } + + switch (type) { + case STARTING_WITH: + return String.format("%s%%", value.toString()); + case ENDING_WITH: + return String.format("%%%s", value.toString()); + case CONTAINING: + return String.format("%%%s%%", value.toString()); + default: + return value; + } + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (!(obj instanceof LikeBinding)) { + return false; + } + + LikeBinding that = (LikeBinding) obj; + + return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position) + && this.type.equals(that.type); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result += nullSafeHashCode(this.name); + result += nullSafeHashCode(this.position); + result += nullSafeHashCode(this.type); + + return result; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("LikeBinding [name: %s, position: %d, type: %s]", name, position, type); + } + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StringQueryParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/StringQueryParameterBinder.java new file mode 100644 index 000000000..086c8d002 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/StringQueryParameterBinder.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import javax.persistence.Query; + +import org.springframework.data.jpa.repository.query.StringQuery.LikeBinding; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.util.Assert; + +/** + * {@link ParameterBinder} that takes {@link LikeBinding}s encapsulated in a {@link StringQuery} into account. + * + * @author Oliver Gierke + */ +public class StringQueryParameterBinder extends ParameterBinder { + + private final StringQuery query; + + /** + * Creates a new {@link StringQueryParameterBinder} from the given {@link Parameters}, method arguments and + * {@link StringQuery}. + * + * @param parameters must not be {@literal null}. + * @param values must not be {@literal null}. + * @param query must not be {@literal null}. + */ + public StringQueryParameterBinder(Parameters parameters, Object[] values, StringQuery query) { + + super(parameters, values); + + Assert.notNull(query, "StringQuery must not be null!"); + this.query = query; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.ParameterBinder#bind(javax.persistence.Query, org.springframework.data.repository.query.Parameter, java.lang.Object, int) + */ + @Override + protected void bind(Query jpaQuery, Parameter methodParameter, Object value, int position) { + + Object valueToBind = value; + + if (query.hasLikeBindings()) { + + LikeBinding binding = getBindingFor(jpaQuery, position, methodParameter); + + if (binding != null) { + valueToBind = binding.prepare(valueToBind); + } + } + + super.bind(jpaQuery, methodParameter, valueToBind, position); + } + + /** + * Finds the {@link LikeBinding} to be applied before binding a parameter value to the query. + * + * @param jpaQuery must not be {@literal null}. + * @param position + * @param methodParameter must not be {@literal null}. + * @return the {@link LikeBinding} for the given parameters or {@literal null} if none available. + */ + private LikeBinding getBindingFor(Query jpaQuery, int position, Parameter methodParameter) { + + try { + + jpaQuery.getParameter(position); + return query.getBindingFor(position); + + } catch (IllegalArgumentException o_O) { + + if (hasNamedParameter(jpaQuery)) { + return query.getBindingFor(methodParameter.getName()); + } + } + + return null; + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 68d10d138..251798086 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -999,6 +999,34 @@ public class UserRepositoryTests { assertThat(page, hasItem(firstUser)); } + /** + * @see DATAJPA-292 + */ + @Test + public void executesManualQueryWithPositionLikeExpressionCorrectly() { + + flushTestUsers(); + + List result = repository.findByFirstnameLike("Da"); + + assertThat(result, hasSize(1)); + assertThat(result, hasItem(thirdUser)); + } + + /** + * @see DATAJPA-292 + */ + @Test + public void executesManualQueryWithNamedLikeExpressionCorrectly() { + + flushTestUsers(); + + List result = repository.findByFirstnameLikeNamed("Da"); + + assertThat(result, hasSize(1)); + assertThat(result, hasItem(thirdUser)); + } + private Page executeSpecWithSort(Sort sort) { flushTestUsers(); diff --git a/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java new file mode 100644 index 000000000..1fbb5ea56 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.jpa.repository.query.StringQuery.LikeBinding; +import org.springframework.data.repository.query.parser.Part.Type; + +/** + * @author Oliver Gierke + */ +public class LikeBindingUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullName() { + new LikeBinding(null, Type.CONTAINING); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsEmptyName() { + new LikeBinding("", Type.CONTAINING); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullType() { + new LikeBinding("foo", null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsInvalidType() { + new LikeBinding("foo", Type.SIMPLE_PROPERTY); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsInvalidPosition() { + new LikeBinding(0, Type.CONTAINING); + } + + @Test + public void setsUpInstanceForName() { + + LikeBinding binding = new LikeBinding("foo", Type.CONTAINING); + + assertThat(binding.hasName("foo"), is(true)); + assertThat(binding.hasName("bar"), is(false)); + assertThat(binding.hasName(null), is(false)); + assertThat(binding.hasPosition(0), is(false)); + assertThat(binding.getType(), is(Type.CONTAINING)); + } + + @Test + public void setsUpInstanceForIndex() { + + LikeBinding binding = new LikeBinding(1, Type.CONTAINING); + + assertThat(binding.hasName("foo"), is(false)); + assertThat(binding.hasName(null), is(false)); + assertThat(binding.hasPosition(0), is(false)); + assertThat(binding.hasPosition(1), is(true)); + assertThat(binding.getType(), is(Type.CONTAINING)); + } + + @Test + public void augmentsValueCorrectly() { + + assertAugmentedValue(Type.CONTAINING, "%value%"); + assertAugmentedValue(Type.ENDING_WITH, "%value"); + assertAugmentedValue(Type.STARTING_WITH, "value%"); + + assertThat(new LikeBinding(1, Type.CONTAINING).prepare(null), is(nullValue())); + } + + private static void assertAugmentedValue(Type type, Object value) { + + LikeBinding binding = new LikeBinding("foo", type); + assertThat(binding.prepare("value"), is(value)); + } +} 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 new file mode 100644 index 000000000..408a304d3 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.springframework.data.jpa.repository.query.StringQuery.LikeBinding; +import org.springframework.data.repository.query.parser.Part.Type; + +/** + * @author Oliver Gierke + */ +public class StringQueryUnitTests { + + @Test + public void detectsPositionalLikeBindings() { + + StringQuery query = new StringQuery("select u from User u where u.firstname like %?1% or u.lastname like %?2"); + + assertThat(query.hasLikeBindings(), is(true)); + assertThat(query.getQuery(), + is("select u from User u where u.firstname like ?1 or u.lastname like ?2")); + + List bindings = query.getLikeBindings(); + assertThat(bindings, hasSize(2)); + + LikeBinding binding = bindings.get(0); + assertThat(binding, is(notNullValue())); + assertThat(binding.hasPosition(1), is(true)); + assertThat(binding.getType(), is(Type.CONTAINING)); + + binding = bindings.get(1); + assertThat(binding, is(notNullValue())); + assertThat(binding.hasPosition(2), is(true)); + assertThat(binding.getType(), is(Type.ENDING_WITH)); + } + + @Test + public void detectsNamedLikeBindings() { + + StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname"); + + assertThat(query.hasLikeBindings(), is(true)); + assertThat(query.getQuery(), is("select u from User u where u.firstname like :firstname")); + + List bindings = query.getLikeBindings(); + assertThat(bindings, hasSize(1)); + + LikeBinding binding = bindings.get(0); + assertThat(binding, is(notNullValue())); + assertThat(binding.hasName("firstname"), is(true)); + assertThat(binding.getType(), is(Type.ENDING_WITH)); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsDifferentBindingsForRepeatedParameter() { + new StringQuery("select u from User u where u.firstname like %?1 and u.lastname like ?1%"); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index af36e4650..c9d699d89 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -124,6 +124,18 @@ public interface UserRepository extends JpaRepository, JpaSpecifi List findByFirstnameNotIn(Collection firstnames); + /** + * @see DATAJPA-292 + */ + @Query("select u from User u where u.firstname like ?1%") + List findByFirstnameLike(String firstname); + + /** + * @see DATAJPA-292 + */ + @Query("select u from User u where u.firstname like :firstname%") + List findByFirstnameLikeNamed(@Param("firstname") String firstname); + /** * Manipulating query to set all {@link User}'s names to the given one. *