The third part of my Spring Data JPA tutorialdescribed how you can create custom queries by using query methods. This blog entry will describe how you can implement more advanced queries by using the JPA criteria API.
If you have read the previous part of this tutorial, you might remember that the search function which I used as an example returned all persons whose last name matched with the given search criteria. This requirement is now replaced with a new one:
- The search function must return only such persons whose last name begins with the given search term.
I am going to walk you through the implementation of this requirement next.
Required Steps
The steps required to implement the new search function are following:
- Creating the JPA criteria query.
- Extending the repository to support JPA criteria queries.
- Using the created criteria query and repository.
Each of these steps is described with more details in following.
Building the JPA Criteria Query
Spring Data JPA uses the specification pattern for providing an API which is used to create queries with the JPA criteria API. The heart of this API is the Specification interface which contains a single method called toPredicate(). In order to build the required criteria query, you must create a new implementation of the Specification interface and build the predicate in thetoPredicate() method.
Before going in to the details, I will introduce the source code of my static meta model classwhich is used to create type safe queries with the JPA criteria API. The source code of thePerson_ class is given in following:
import
javax.persistence.metamodel.SingularAttribute
;
import
javax.persistence.metamodel.StaticMetamodel
;
/**
* A meta model class used to create type safe queries from person
* information.
* @author Petri Kainulainen
*/
@StaticMetamodel
(Person.
class
)
public
class
Person_
{
public
static
volatile
SingularAttribute
<Person, String
>
lastName
;
}
A clean way to create specifications is to implement a specification builder class and use static methods to build the actual specification instances. My specification builder class is calledPersonSpecifications and its source code given in following:
import
org.springframework.data.jpa.domain.Specification
;
import
javax.persistence.criteria.CriteriaBuilder
;
import
javax.persistence.criteria.CriteriaQuery
;
import
javax.persistence.criteria.Predicate
;
import
javax.persistence.criteria.Root
;
/**
* A class which is used to create Specification objects which are used
* to create JPA criteria queries for person information.
* @author Petri Kainulainen
*/
public
class
PersonSpecifications
{
/**
* Creates a specification used to find persons whose last name begins with
* the given search term. This search is case insensitive.
* @param searchTerm
* @return
*/
public
static
Specification
<Person
>
lastNameIsLike
(
final
String
searchTerm
)
{
return
new
Specification
<Person
>
(
)
{
@Override
public
Predicate toPredicate
(Root
<Person
>
personRoot, CriteriaQuery
<?>
query, CriteriaBuilder cb
)
{
String
likePattern
=
getLikePattern
(searchTerm
)
;
return
cb.
like
(cb.
lower
(personRoot.
<String
>get
(Person_.
lastName
)
), likePattern
)
;
}
private
String
getLikePattern
(
final
String
searchTerm
)
{
StringBuilder pattern
=
new
StringBuilder
(
)
;
pattern.
append
(searchTerm.
toLowerCase
(
)
)
;
pattern.
append
(
"%"
)
;
return
pattern.
toString
(
)
;
}
}
;
}
}
Testing my specification builder implementation is pretty straightforward. I used Mockito mocking framework to mock the JPA criteria API. The source code of my test class is given following:
import
org.junit.Before
;
import
org.junit.Test
;
import
org.springframework.data.jpa.domain.Specification
;
import
javax.persistence.criteria.*
;
import
static
junit.
framework.
Assert.
assertEquals
;
import
static
org.
mockito.
Mockito.
*;
public
class
PersonSpecificationsTest
{
private
static
final
String
SEARCH_TERM
=
"Foo"
;
private
static
final
String
SEARCH_TERM_LIKE_PATTERN
=
"foo%"
;
private
CriteriaBuilder criteriaBuilderMock
;
private
CriteriaQuery criteriaQueryMock
;
private
Root
<Person
>
personRootMock
;
@Before
public
void
setUp
(
)
{
criteriaBuilderMock
=
mock
(CriteriaBuilder.
class
)
;
criteriaQueryMock
=
mock
(CriteriaQuery.
class
)
;
personRootMock
=
mock
(Root.
class
)
;
}
@Test
public
void
lastNameIsLike
(
)
{
Path lastNamePathMock
=
mock
(Path.
class
)
;
when
(personRootMock.
get
(Person_.
lastName
)
).
thenReturn
(lastNamePathMock
)
;
Expression lastNameToLowerExpressionMock
=
mock
(Expression.
class
)
;
when
(criteriaBuilderMock.
lower
(lastNamePathMock
)
).
thenReturn
(lastNameToLowerExpressionMock
)
;
Predicate lastNameIsLikePredicateMock
=
mock
(Predicate.
class
)
;
when
(criteriaBuilderMock.
like
(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN
)
).
thenReturn
(lastNameIsLikePredicateMock
)
;
Specification
<Person
>
actual
=
PersonSpecifications.
lastNameIsLike
(SEARCH_TERM
)
;
Predicate actualPredicate
=
actual.
toPredicate
(personRootMock, criteriaQueryMock, criteriaBuilderMock
)
;
verify
(personRootMock, times
(
1
)
).
get
(Person_.
lastName
)
;
verifyNoMoreInteractions
(personRootMock
)
;
verify
(criteriaBuilderMock, times
(
1
)
).
lower
(lastNamePathMock
)
;
verify
(criteriaBuilderMock, times
(
1
)
).
like
(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN
)
;
verifyNoMoreInteractions
(criteriaBuilderMock
)
;
verifyZeroInteractions
(criteriaQueryMock, lastNamePathMock, lastNameIsLikePredicateMock
)
;
assertEquals
(lastNameIsLikePredicateMock, actualPredicate
)
;
}
}
However, if you have to build more complex queries by using this approach, testing your queries will become more troublesome because the JPA criteria API is not the easiest one to mock. In this case it is a good idea to divide the search conditions into multiple specifications and use theSpecifications class to combine your specification instances. This way your unit tests don’t become so complex but you can still harness the power of the JPA criteria API in your application.
Extending the Repository
Extending your Spring Data JPA repository to support JPA criteria queries is quite easy. All you have to do is to extend the JpaSpecificationExecutor interface. This gives you access to thefindAll(Specification spec) method which returns all entities fulfilling the search conditions specified by the specification. The source code of my PersonRepository is given in following:
import
org.springframework.data.jpa.repository.JpaRepository
;
import
org.springframework.data.jpa.repository.JpaSpecificationExecutor
;
/**
* Specifies methods used to obtain and modify person related information
* which is stored in the database.
* @author Petri Kainulainen
*/
public
interface
PersonRepository
extends
JpaRepository
<Person, Long
>, JpaSpecificationExecutor
<Person
>
{
}
Using the Specification Builder and the Repository
The last step is to implement the service class which uses the created specification builder and the repository. The search() method of the PersonService interface takes the used search term as a parameter. The relevant part of the PersonService interface is given in following:
/**
* Declares methods used to obtain and modify person information.
* @author Petri Kainulainen
*/
public
interface
PersonService
{
/**
* Searches persons by using the given search term as a parameter.
* @param searchTerm
* @return A list of persons whose last name begins with the given search term. If no persons is found, this method
* returns an empty list. This search is case insensitive.
*/
public
List
<Person
>
search
(
String
searchTerm
)
;
}
The implementation of the search() method is very simple. It simply passes the search term to the lastNameIsLike() method of the PersonSpecifications class and gives the created specification object to the PersonRepository. The source code of the implementation of the search() method is given in following:
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
javax.annotation.Resource
;
/**
* This implementation of the PersonService interface communicates with
* the database by using a Spring Data JPA repository.
* @author Petri Kainulainen
*/
@Service
public
class
RepositoryPersonService
implements
PersonService
{
private
static
final
Logger LOGGER
=
LoggerFactory.
getLogger
(RepositoryPersonService.
class
)
;
@Resource
private
PersonRepository personRepository
;
@Transactional
(readOnly
=
true
)
@Override
public
List
<Person
>
search
(
String
searchTerm
)
{
LOGGER.
debug
(
"Searching persons with search term: "
+
searchTerm
)
;
//Passes the specification created by PersonSpecifications class to the repository.
return
personRepository.
findAll
(lastNameIsLike
(searchTerm
)
)
;
}
}
This solution is not perfect from the architectural point of view because it introduces a dependency between the service layer and the Spring Data JPA. A general guideline is that an upper layer should not have any knowledge about the implementation details of the layers located below it.
One solution to this problem would be to create a custom search method and integrate it with the generic repository abstraction as described in the reference manual of Spring Data JPA. However, this would mean that you would have to write a lot of boilerplate code which does not really add any value to your application. Also, since the goal of the Spring Data JPA is to reduce the amount of boilerplate code, I think that my solution is an acceptable compromise between over engineering and getting things done in a clean way.
Oh, this reminds me of something. We should not forget the unit test of the search() method. The source code of this unit test is given in following:
import
org.junit.Before
;
import
org.junit.Test
;
import
org.springframework.data.jpa.domain.Specification
;
import
static
junit.
framework.
Assert.
assertEquals
;
import
static
org.
mockito.
Mockito.
*;
public
class
RepositoryPersonServiceTest
{
private
static
final
String
SEARCH_TERM
=
"foo"
;
private
RepositoryPersonService personService
;
private
PersonRepository personRepositoryMock
;
@Before
public
void
setUp
(
)
{
personService
=
new
RepositoryPersonService
(
)
;
personRepositoryMock
=
mock
(PersonRepository.
class
)
;
personService.
setPersonRepository
(personRepositoryMock
)
;
}
@Test
public
void
search
(
)
{
List
<Person
>
expected
=
new
ArrayList
<Person
>
(
)
;
when
(personRepositoryMock.
findAll
(any
(Specification.
class
)
)
).
thenReturn
(expected
)
;
List
<Person
>
actual
=
personService.
search
(SEARCH_TERM
)
;
verify
(personRepositoryMock, times
(
1
)
).
findAll
(any
(Specification.
class
)
)
;
verifyNoMoreInteractions
(personRepositoryMock
)
;
assertEquals
(expected, actual
)
;
}
}
What is Next?
I have now demonstrated to you how you can use the JPA criteria API with Spring Data JPA. As always, the example application of this blog entry is available at Github. The next part of my Spring Data JPA tutorial describes how you can use Querydsl for building queries with Spring Data JPA.