Spring Data JPA教程, 第四部分: JPA Criteria Queries(未翻译)
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.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 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.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.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.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.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.
Spring Data JPA教程, 第四部分: JPA Criteria Queries(未翻译)的更多相关文章
- Spring Data Solr教程(翻译) 开源的搜索服务器
Solr是一个使用开源的搜索服务器,它采用Lucene Core的索引和搜索功能构建,它可以用于几乎所有的编程语言实现可扩展的搜索引擎. Solr的虽然有很多优点,建立开发环境是不是其中之一.此博客条 ...
- Spring Data Solr教程(翻译)
大多数应用都必须具有某种搜索功能,问题是搜索功能往往是巨大的资源消耗并且它们由于沉重的数据库加载而拖垮你的应用的性能 这就是为什么转移负载到一个外部的搜索服务器是一个不错的主意,Apache Solr ...
- spring cloud系列教程第四篇-Eureka基础知识
通过前三篇文章学习,我们搭建好了两个微服务工程.即:order80和payment8001这两个服务.有了这两个基础的框架之后,我们将要开始往里面添加东西了.还记得分布式架构的几个维度吗?我们要通过一 ...
- Spring入门详细教程(四)
前言 本篇紧接着spring入门详细教程(三),建议阅读本篇前,先阅读第一篇,第二篇以及第三篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/ ...
- Spring Boot2 系列教程 (十四) | 统一异常处理
如题,今天介绍 SpringBoot 是如何统一处理全局异常的.SpringBoot 中的全局异常处理主要起作用的两个注解是 @ControllerAdvice 和 @ExceptionHandler ...
- Spring Boot2 系列教程(十四)CORS 解决跨域问题
今天和小伙伴们来聊一聊通过CORS解决跨域问题. 同源策略 很多人对跨域有一种误解,以为这是前端的事,和后端没关系,其实不是这样的,说到跨域,就不得不说说浏览器的同源策略. 同源策略是由 Netsca ...
- Spring Boot系列教程十四:Spring boot同时支持HTTP和HTTPS
自签证书 openssl生成服务端证书,不使用CA证书直接生成 -in server.csr -signkey server.key -out server.crt # 5.server证书转换成ke ...
- Spring Cloud Stream教程(四)消费群体
虽然发布订阅模型可以轻松地通过共享主题连接应用程序,但通过创建给定应用程序的多个实例来扩展的能力同样重要.当这样做时,应用程序的不同实例被放置在竞争的消费者关系中,其中只有一个实例预期处理给定消息. ...
- Spring Cloud Config教程(四)快速开始
Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持.使用Config Server,您可以在所有环境中管理应用程序的外部属性.客户端和服务器上的概念映射与Spring ...
随机推荐
- 如何使用UIAutomation进行iOS 自动化测试(Part I)
转自:http://www.cnblogs.com/vowei/archive/2012/08/10/2631949.html 写在前面 研究iOS的自动化测试也有些日子了,刚开始的时候,一直苦于找不 ...
- LA 2889 (找规律) Palindrome Numbers
输出第n个回文数. 规律就是一位和两位的回文数各有9个,三位四位的回文数各有90个,以此类推. 给出n,可以先判定一下第n个回文数的位数,然后后面也不难推,但是有很多细节需要注意. #include ...
- Mysql分支
MySQL是历史上最受欢迎的免费开源程序之一.它是成千上万个网站的数据库骨干,并且可以将它(和linux)作为过去10年里Internet呈指数级增长的一个有力证明. 那么,如果MySQL真的这么重要 ...
- IIS修改队列长度(IIS6+IIS7)
Internet Information Services (IIS) 限制了在任何给定时间可在队列中等待的应用程序池请求的最大数量.如果达到此限制,则所有新请求都将被拒绝,而且用户将收到错误消息“5 ...
- 通过AJAX与ASP.NET结合实现的仿GridView增删改查功能
jQurey代码部分: 1. <script type="text/javascript"> 2. var flag = 0; 3. 4. ...
- 动态加载so文件
在开发过程中,经常会用到第三方库,比如地图.视频.文档编辑.图表之类.依赖这些库,需要添加其SDK,有时需要用到jni层的So文件,比如百度地图等. 那么问题来了,如果两个不同的库之间的so文件发生冲 ...
- 通过userAgent判断手机浏览器类型
我们可以通过userAgent来判断,比如检测某些关键字,例如:AppleWebKit*****Mobile或AppleWebKit,需要注意的是有些浏览器的userAgent中并不包含AppleWe ...
- mac下SSH很快被断开
解决方法: 1. 切换到root账号:sudo bash -c 'su - root' 2. 修改/etc/ssh_config文件 ServerAliveCountMax 5 ServerAlive ...
- Android中GridView滚动到底部加载数据终极版
之前在项目中有一个需求是需要GridView控件,滚动到底部自动加载.但是呢GridView控件并不提供诸如ListView监听滚动到底部的onScrollListener方法,为了实现这样一个效果, ...
- linux命令——rm
rm是常用的命令,该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除.对于链接文件,只是删除了链接,原有文件均保持不变. rm是一个危险的命令,使用的 ...