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 LIKE expressions
+
+ The query execution mechanism for manually defined queries using
+ @Query allow the definition of advanced
+ LIKE expressions inside the query definition.
+
+
+ Advanced LIKE expressions in
+ @Query
+
+ 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.
*