/*
* Copyright 2008-2017 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 static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
import javax.persistence.LockModeType;
import javax.persistence.QueryHint;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.QueryMethod;
/**
* Unit test for {@link QueryMethod}.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class JpaQueryMethodUnitTests {
static final Class<?> DOMAIN_CLASS = User.class;
static final String METHOD_NAME = "findByFirstname";
@Mock QueryExtractor extractor;
@Mock RepositoryMetadata metadata;
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
Method invalidReturnType, pageableAndSort, pageableTwice, sortableTwice, findWithLockMethod, findsProjections,
findsProjection, queryMethodWithCustomEntityFetchGraph;
/**
* @throws Exception
*/
@Before
public void setUp() throws Exception {
invalidReturnType = InvalidRepository.class.getMethod(METHOD_NAME, String.class, Pageable.class);
pageableAndSort = InvalidRepository.class.getMethod(METHOD_NAME, String.class, Pageable.class, Sort.class);
pageableTwice = InvalidRepository.class.getMethod(METHOD_NAME, String.class, Pageable.class, Pageable.class);
sortableTwice = InvalidRepository.class.getMethod(METHOD_NAME, String.class, Sort.class, Sort.class);
findWithLockMethod = ValidRepository.class.getMethod("findOneLocked", Integer.class);
findsProjections = ValidRepository.class.getMethod("findsProjections");
findsProjection = ValidRepository.class.getMethod("findsProjection");
queryMethodWithCustomEntityFetchGraph = ValidRepository.class.getMethod("queryMethodWithCustomEntityFetchGraph",
Integer.class);
}
@Test
public void testname() throws Exception {
JpaQueryMethod method = getQueryMethod(UserRepository.class, "findByLastname", String.class);
assertEquals("User.findByLastname", method.getNamedQueryName());
assertThat(method.isCollectionQuery(), is(true));
assertThat(method.getAnnotatedQuery(), is(nullValue()));
assertThat(method.isNativeQuery(), is(false));
}
@Test(expected = IllegalArgumentException.class)
public void preventsNullRepositoryMethod() {
new JpaQueryMethod(null, metadata, factory, extractor);
}
@Test(expected = IllegalArgumentException.class)
public void preventsNullQueryExtractor() throws Exception {
Method method = UserRepository.class.getMethod("findByLastname", String.class);
new JpaQueryMethod(method, metadata, factory, null);
}
@Test
public void returnsCorrectName() throws Exception {
JpaQueryMethod method = getQueryMethod(UserRepository.class, "findByLastname", String.class);
assertThat(method.getName(), is("findByLastname"));
}
@Test
public void returnsQueryIfAvailable() throws Exception {
JpaQueryMethod method = getQueryMethod(UserRepository.class, "findByLastname", String.class);
assertThat(method.getAnnotatedQuery(), is(nullValue()));
method = getQueryMethod(UserRepository.class, "findByAnnotatedQuery", String.class);
assertThat(method.getAnnotatedQuery(), is(notNullValue()));
}
@Test(expected = IllegalStateException.class)
public void rejectsInvalidReturntypeOnPagebleFinder() {
new JpaQueryMethod(invalidReturnType, metadata, factory, extractor);
}
@Test(expected = IllegalStateException.class)
public void rejectsPageableAndSortInFinderMethod() {
new JpaQueryMethod(pageableAndSort, metadata, factory, extractor);
}
@Test(expected = IllegalStateException.class)
public void rejectsTwoPageableParameters() {
new JpaQueryMethod(pageableTwice, metadata, factory, extractor);
}
@Test(expected = IllegalStateException.class)
public void rejectsTwoSortableParameters() {
new JpaQueryMethod(sortableTwice, metadata, factory, extractor);
}
@Test
public void recognizesModifyingMethod() throws Exception {
JpaQueryMethod method = getQueryMethod(UserRepository.class, "renameAllUsersTo", String.class);
assertTrue(method.isModifyingQuery());
}
@Test(expected = IllegalArgumentException.class)
public void rejectsModifyingMethodWithPageable() throws Exception {
Method method = InvalidRepository.class.getMethod("updateMethod", String.class, Pageable.class);
new JpaQueryMethod(method, metadata, factory, extractor);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsModifyingMethodWithSort() throws Exception {
Method method = InvalidRepository.class.getMethod("updateMethod", String.class, Sort.class);
new JpaQueryMethod(method, metadata, factory, extractor);
}
@Test
public void discoversHintsCorrectly() throws Exception {
JpaQueryMethod method = getQueryMethod(UserRepository.class, "findByLastname", String.class);
List<QueryHint> hints = method.getHints();
assertNotNull(hints);
assertThat(hints.get(0).name(), is("foo"));
assertThat(hints.get(0).value(), is("bar"));
}
private JpaQueryMethod getQueryMethod(Class<?> repositoryInterface, String methodName, Class<?>... parameterTypes)
throws Exception {
Method method = repositoryInterface.getMethod(methodName, parameterTypes);
DefaultRepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(repositoryInterface);
return new JpaQueryMethod(method, repositoryMetadata, factory, extractor);
}
@Test
public void calculatesNamedQueryNamesCorrectly() throws Exception {
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
JpaQueryMethod queryMethod = getQueryMethod(UserRepository.class, "findByLastname", String.class);
assertThat(queryMethod.getNamedQueryName(), is("User.findByLastname"));
Method method = UserRepository.class.getMethod("renameAllUsersTo", String.class);
queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
assertThat(queryMethod.getNamedQueryName(), is("User.renameAllUsersTo"));
method = UserRepository.class.getMethod("findSpecialUsersByLastname", String.class);
queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
assertThat(queryMethod.getNamedQueryName(), is("SpecialUser.findSpecialUsersByLastname"));
}
@Test // DATAJPA-117
public void discoversNativeQuery() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "findByLastname", String.class);
assertThat(method.isNativeQuery(), is(true));
}
@Test // DATAJPA-129
public void considersAnnotatedNamedQueryName() throws Exception {
JpaQueryMethod queryMethod = getQueryMethod(ValidRepository.class, "findByNamedQuery");
assertThat(queryMethod.getNamedQueryName(), is("HateoasAwareSpringDataWebConfiguration.bar"));
}
@Test // DATAJPA-73
public void discoversLockModeCorrectly() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "findOneLocked", Integer.class);
LockModeType lockMode = method.getLockModeType();
assertEquals(LockModeType.PESSIMISTIC_WRITE, lockMode);
}
@Test // DATAJPA-142
public void returnsDefaultCountQueryName() throws Exception {
JpaQueryMethod method = getQueryMethod(UserRepository.class, "findByLastname", String.class);
assertThat(method.getNamedCountQueryName(), is("User.findByLastname.count"));
}
@Test // DATAJPA-142
public void returnsDefaultCountQueryNameBasedOnConfiguredNamedQueryName() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "findByNamedQuery");
assertThat(method.getNamedCountQueryName(), is("HateoasAwareSpringDataWebConfiguration.bar.count"));
}
@Test // DATAJPA-185
public void rejectsInvalidNamedParameter() throws Exception {
try {
getQueryMethod(InvalidRepository.class, "findByAnnotatedQuery", String.class);
fail();
} catch (IllegalStateException e) {
// Parameter from query
assertThat(e.getMessage(), containsString("foo"));
// Parameter name from annotation
assertThat(e.getMessage(), containsString("param"));
// Method name
assertThat(e.getMessage(), containsString("findByAnnotatedQuery"));
}
}
@Test // DATAJPA-207
@SuppressWarnings({ "rawtypes", "unchecked" })
public void returnsTrueIfReturnTypeIsEntity() {
when(metadata.getDomainType()).thenReturn((Class) User.class);
when(metadata.getReturnedDomainClass(findsProjections)).thenReturn((Class) Integer.class);
when(metadata.getReturnedDomainClass(findsProjection)).thenReturn((Class) Integer.class);
assertThat(new JpaQueryMethod(findsProjections, metadata, factory, extractor).isQueryForEntity(), is(false));
assertThat(new JpaQueryMethod(findsProjection, metadata, factory, extractor).isQueryForEntity(), is(false));
}
@Test // DATAJPA-345
public void detectsLockAndQueryHintsOnIfUsedAsMetaAnnotation() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotation");
assertThat(method.getLockModeType(), is(LockModeType.OPTIMISTIC_FORCE_INCREMENT));
assertThat(method.getHints(), hasSize(1));
assertThat(method.getHints().get(0).name(), is("foo"));
assertThat(method.getHints().get(0).value(), is("bar"));
}
@Test // DATAJPA-466
public void shouldStoreJpa21FetchGraphInformationAsHint() {
doReturn(User.class).when(metadata).getDomainType();
doReturn(User.class).when(metadata).getReturnedDomainClass(queryMethodWithCustomEntityFetchGraph);
JpaQueryMethod method = new JpaQueryMethod(queryMethodWithCustomEntityFetchGraph, metadata, factory, extractor);
assertThat(method.getEntityGraph(), is(notNullValue()));
assertThat(method.getEntityGraph().getName(), is("User.propertyLoadPath"));
assertThat(method.getEntityGraph().getType(), is(EntityGraphType.LOAD));
}
@Test // DATAJPA-612
public void shouldFindEntityGraphAnnotationOnOverriddenSimpleJpaRepositoryMethod() throws Exception {
doReturn(User.class).when(metadata).getDomainType();
doReturn(User.class).when(metadata).getReturnedDomainClass((Method) any());
JpaQueryMethod method = new JpaQueryMethod(JpaRepositoryOverride.class.getMethod("findAll"), metadata, factory,
extractor);
assertThat(method.getEntityGraph(), is(notNullValue()));
assertThat(method.getEntityGraph().getName(), is("User.detail"));
assertThat(method.getEntityGraph().getType(), is(EntityGraphType.FETCH));
}
@Test // DATAJPA-689
public void shouldFindEntityGraphAnnotationOnOverriddenSimpleJpaRepositoryMethodFindOne() throws Exception {
doReturn(User.class).when(metadata).getDomainType();
doReturn(User.class).when(metadata).getReturnedDomainClass((Method) any());
JpaQueryMethod method = new JpaQueryMethod(JpaRepositoryOverride.class.getMethod("findOne", Long.class), metadata,
factory, extractor);
assertThat(method.getEntityGraph(), is(notNullValue()));
assertThat(method.getEntityGraph().getName(), is("User.detail"));
assertThat(method.getEntityGraph().getType(), is(EntityGraphType.FETCH));
}
/**
* DATAJPA-696
*/
@Test
public void shouldFindEntityGraphAnnotationOnQueryMethodGetOneByWithDerivedName() throws Exception {
doReturn(User.class).when(metadata).getDomainType();
doReturn(User.class).when(metadata).getReturnedDomainClass((Method) any());
JpaQueryMethod method = new JpaQueryMethod(JpaRepositoryOverride.class.getMethod("getOneById", Long.class),
metadata, factory, extractor);
assertThat(method.getEntityGraph(), is(notNullValue()));
assertThat(method.getEntityGraph().getName(), is("User.getOneById"));
assertThat(method.getEntityGraph().getType(), is(EntityGraphType.FETCH));
}
@Test // DATAJPA-758
public void allowsPositionalBindingEvenIfParametersAreNamed() throws Exception {
getQueryMethod(ValidRepository.class, "queryWithPositionalBinding", String.class);
}
@Test // DATAJPA-871
public void usesAliasedValueForLockLockMode() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.getLockModeType(), is(LockModeType.PESSIMISTIC_FORCE_INCREMENT));
}
@Test // DATAJPA-871
public void usesAliasedValueForQueryHints() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.getHints(), hasSize(1));
assertThat(method.getHints().get(0).name(), is("foo"));
assertThat(method.getHints().get(0).value(), is("bar"));
}
@Test // DATAJPA-871
public void usesAliasedValueForQueryHintsCounting() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.applyHintsToCountQuery(), is(true));
}
@Test // DATAJPA-871
public void usesAliasedValueForModifyingClearAutomatically() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.isModifyingQuery(), is(true));
assertThat(method.getClearAutomatically(), is(true));
}
@Test // DATAJPA-871
public void usesAliasedValueForHintsApplyToCountQuery() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.applyHintsToCountQuery(), is(true));
}
@Test // DATAJPA-871
public void usesAliasedValueForQueryValue() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.getAnnotatedQuery(), is(equalTo("select u from User u where u.firstname = ?1")));
}
@Test // DATAJPA-871
public void usesAliasedValueForQueryCountQuery() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.getCountQuery(), is(equalTo("select u from User u where u.lastname = ?1")));
}
@Test // DATAJPA-871
public void usesAliasedValueForQueryCountQueryProjection() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.getCountQueryProjection(), is(equalTo("foo-bar")));
}
@Test // DATAJPA-871
public void usesAliasedValueForQueryNamedQueryName() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.getNamedQueryName(), is(equalTo("namedQueryName")));
}
@Test // DATAJPA-871
public void usesAliasedValueForQueryNamedCountQueryName() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.getNamedCountQueryName(), is(equalTo("namedCountQueryName")));
}
@Test // DATAJPA-871
public void usesAliasedValueForQueryNativeQuery() throws Exception {
JpaQueryMethod method = getQueryMethod(ValidRepository.class, "withMetaAnnotationUsingAliasFor");
assertThat(method.isNativeQuery(), is(true));
}
@Test // DATAJPA-871
public void usesAliasedValueForEntityGraph() throws Exception {
doReturn(User.class).when(metadata).getDomainType();
doReturn(User.class).when(metadata).getReturnedDomainClass((Method) any());
JpaQueryMethod method = new JpaQueryMethod(
JpaRepositoryOverride.class.getMethod("getOneWithCustomEntityGraphAnnotation"), metadata, factory, extractor);
assertThat(method.getEntityGraph(), is(notNullValue()));
assertThat(method.getEntityGraph().getName(), is("User.detail"));
assertThat(method.getEntityGraph().getType(), is(EntityGraphType.LOAD));
}
/**
* Interface to define invalid repository methods for testing.
*
* @author Oliver Gierke
*/
static interface InvalidRepository extends Repository<User, Long> {
// Invalid return type
User findByFirstname(String firstname, Pageable pageable);
// Should not use Pageable *and* Sort
Page<User> findByFirstname(String firstname, Pageable pageable, Sort sort);
// Must not use two Pageables
Page<User> findByFirstname(String firstname, Pageable first, Pageable second);
// Must not use two Pageables
Page<User> findByFirstname(String firstname, Sort first, Sort second);
// Not backed by a named query or @Query annotation
@Modifying
void updateMethod(String firstname);
// Modifying and Pageable is not allowed
@Modifying
Page<String> updateMethod(String firstname, Pageable pageable);
// Modifying and Sort is not allowed
@Modifying
void updateMethod(String firstname, Sort sort);
// Typo in named parameter
@Query("select u from User u where u.firstname = :foo")
List<User> findByAnnotatedQuery(@Param("param") String param);
}
static interface ValidRepository extends Repository<User, Long> {
@Query(value = "query", nativeQuery = true)
List<User> findByLastname(String lastname);
@Query(name = "HateoasAwareSpringDataWebConfiguration.bar")
List<User> findByNamedQuery();
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select u from User u where u.id = ?1")
List<User> findOneLocked(Integer primaryKey);
List<Integer> findsProjections();
Integer findsProjection();
@CustomAnnotation
void withMetaAnnotation();
// DATAJPA-466
@EntityGraph(value = "User.propertyLoadPath", type = EntityGraphType.LOAD)
User queryMethodWithCustomEntityFetchGraph(Integer id);
@Query("select u from User u where u.firstname = ?1")
User queryWithPositionalBinding(@Param("firstname") String firstname);
@CustomComposedAnnotationWithAliasFor
void withMetaAnnotationUsingAliasFor();
}
static interface JpaRepositoryOverride extends JpaRepository<User, Long> {
/**
* DATAJPA-612
*/
@Override
@EntityGraph("User.detail")
List<User> findAll();
/**
* DATAJPA-689
*/
@EntityGraph("User.detail")
Optional<User> findOne(Long id);
/**
* DATAJPA-696
*/
@EntityGraph
User getOneById(Long id);
@CustomComposedEntityGraphAnnotationWithAliasFor
User getOneWithCustomEntityGraphAnnotation();
}
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@QueryHints(@QueryHint(name = "foo", value = "bar"))
@Retention(RetentionPolicy.RUNTIME)
static @interface CustomAnnotation {
}
@Modifying
@Query
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@QueryHints(@QueryHint(name = "foo", value = "bar"))
@Retention(RetentionPolicy.RUNTIME)
static @interface CustomComposedAnnotationWithAliasFor {
@AliasFor(annotation = Modifying.class, attribute = "clearAutomatically")
boolean doClear() default true;
@AliasFor(annotation = Query.class, attribute = "value")
String querystring() default "select u from User u where u.firstname = ?1";
@AliasFor(annotation = Query.class, attribute = "countQuery")
String countQueryString() default "select u from User u where u.lastname = ?1";
@AliasFor(annotation = Query.class, attribute = "countProjection")
String countProjectionString() default "foo-bar";
@AliasFor(annotation = Query.class, attribute = "nativeQuery")
boolean isNativeQuery() default true;
@AliasFor(annotation = Query.class, attribute = "name")
String namedQueryName() default "namedQueryName";
@AliasFor(annotation = Query.class, attribute = "countName")
String namedCountQueryName() default "namedCountQueryName";
@AliasFor(annotation = Lock.class, attribute = "value")
LockModeType lock() default LockModeType.PESSIMISTIC_FORCE_INCREMENT;
@AliasFor(annotation = QueryHints.class, attribute = "value")
QueryHint[] hints() default @QueryHint(name = "foo", value = "bar")
;
@AliasFor(annotation = QueryHints.class, attribute = "forCounting")
boolean doCount() default true;
}
@EntityGraph
@Retention(RetentionPolicy.RUNTIME)
static @interface CustomComposedEntityGraphAnnotationWithAliasFor {
@AliasFor(annotation = EntityGraph.class, attribute = "value")
String graphName() default "User.detail";
@AliasFor(annotation = EntityGraph.class, attribute = "type")
EntityGraphType graphType() default EntityGraphType.LOAD;
@AliasFor(annotation = EntityGraph.class, attribute = "attributePaths")
String[] paths() default { "foo", "bar" };
}
}