The previous part of my tutorial described how you can paginate query results with Spring Data JPA. The example application of this blog entry has the same functional requirements, but it will use Querydsl instead of JPA criteria API. This blog entry assumes that you have got experience from both Querydsl And Spring Data JPA. If this is not the case, please read mySpring Data JPA and Querydsl tutorial.

If you have read the previous parts of my Spring Data JPA tutorial, you might have noticed that the described solutions introduced a dependency between the service layer and the Spring Data JPA. This blog entry describes how you can eliminate that dependency (or at least most of it) by adding custom functionality to a Spring Data repository.

Adding Functionality to a Spring Data Repository

You can add custom functionality to a Spring Data repository by following these steps:

  • Create an interface for the custom functionality.
  • Implement the created interface.
  • Create the Spring Data JPA repository.

Enough with the theory. It is time to take a look, how I added custom functionality to the person repository of my example application.

Implementing The Example Application

This Section describes how I added the custom functionality to the person repository of my example application. The steps described previously are discussed with more details in following Subsections.

Creating an interface for the custom functionality.

In order to eliminate the dependency between the service layer and the Spring Data JPA, I created an interface called PaginatingPersonRepository. My goal was to move all pagination and sorting logic behind this interface. The source code of my custom interface is given in following:

public interface PaginatingPersonRepository {

/**
     * Finds all persons stored in the database.
     * @return
     */
    public List<Person> findAllPersons();

/**
     * Finds the count of persons stored in the database.
     * @param searchTerm
     * @return
     */
    public long findPersonCount(String searchTerm);

/**
     * Finds persons for the requested page whose last name starts with the given search term.
     * @param searchTerm    The used search term.
     * @param page  The number of the requested page.
     * @return  A list of persons belonging to the requested page.
     */
    public List<Person> findPersonsForPage(String searchTerm, int page);
}

Implementing the Declared Interface

The next step is to create an implementation for the PaginatingPersonRepository interface. This implementation has following responsibilities:

  • Creating a new QueryDslJPARepository instance and delegating the method calls to it.
  • Obtaining the required Querydsl predicate when a search is performed.
  • Creating the Sort object which is used to specify the sort order of query results.
  • Creating the PageRequest object which id used the specify the wanted page and sort order of the search results.

The implementation of the PaginatingPersonRepository interface is using the PersonPredicatesclass for creating the required Querydsl predicates. The source code of this class is given in following:

import com.mysema.query.types.Predicate;
import net.petrikainulainen.spring.datajpa.model.QPerson;

public class PersonPredicates {

public static Predicate lastNameIsLike(final String searchTerm) {
        QPerson person = QPerson.person;
        return person.lastName.startsWithIgnoreCase(searchTerm);
    }
}

The repository architecture of Spring Data JPA is looking for the custom implementation from the package where the repository was found. The name of the class implementing the custom interface must be constructed by adding a special postfix to the name of the custom interface. The default value of this postfix is Impl, but you can change the postfix by adding a repository-impl-postfix attribute to the namespace configuration element of Spring Data JPA. An example of this is given in following:

<!-- Declares the base package for repositories and states that postfix used to identify custom implementations is FooBar. -->
<jpa:repositories base-package="net.petrikainulainen.spring.datajpa.repository" repository-impl-postfix="FooBar"/>

However, I am going to use the default configuration. Thus, the name of my custom repository class must be PaginatingPersonRepositoryImpl.

Note: If you are using Spring Data JPA 1.2.0 (or newer version), the name of the custom repository implementation must follow the following formula: [The name of the actual repository interface][postfix]. Because the example application use the default configuration, the name of the custom repository implementation must be PersonRepositoryImpl.

The source code of the PaginatingPersonRepositoryImpl class is given in the following:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation;
import org.springframework.data.jpa.repository.support.QueryDslJpaRepository;
import org.springframework.stereotype.Repository;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import static net.petrikainulainen.spring.datajpa.repository.PersonPredicates.lastNameIsLike;

@Repository
public class PaginatingPersonRepositoryImpl implements PaginatingPersonRepository {

private static final Logger LOGGER = LoggerFactory.getLogger(PaginatingPersonRepositoryImpl.class);

protected static final int NUMBER_OF_PERSONS_PER_PAGE = 5;

@PersistenceContext
    private EntityManager entityManager;

private QueryDslJpaRepository<Person, Long> personRepository;

public PaginatingPersonRepositoryImpl() {

}

@Override
    public List<Person> findAllPersons() {
        LOGGER.debug("Finding all persons");
        
        //Passes the Sort object to the repository
        return personRepository.findAll(sortByLastNameAsc());
    }

@Override
    public long findPersonCount(String searchTerm) {
        LOGGER.debug("Finding person count with search term: " + searchTerm);

//Passes the predicate to the repository
        return personRepository.count(lastNameIsLike(searchTerm));
    }

@Override
    public List<Person> findPersonsForPage(String searchTerm, int page) {
        LOGGER.debug("Finding persons for page " + page + " with search term: " + searchTerm);

//Passes the predicate and the page specification to the repository
        Page requestedPage =  personRepository.findAll(lastNameIsLike(searchTerm), constructPageSpecification(page));

return requestedPage.getContent();
    }

/**
     * Returns a new object which specifies the the wanted result page.
     * @param pageIndex The index of the wanted result page
     * @return
     */
    private Pageable constructPageSpecification(int pageIndex) {
        Pageable pageSpecification = new PageRequest(pageIndex, NUMBER_OF_PERSONS_PER_PAGE, sortByLastNameAsc());
        return pageSpecification;
    }

/**
     * Returns a Sort object which sorts persons in ascending order by using the last name.
     * @return
     */
    private Sort sortByLastNameAsc() {
        return new Sort(Sort.Direction.ASC, "lastName");
    }

/**
     * An initialization method which is run after the bean has been constructed.
     * This ensures that the entity manager is injected before we try to use it.
     */
    @PostConstruct
    public void init() {
        JpaEntityInformation<Person, Long> personEntityInfo = new JpaMetamodelEntityInformation<Person, Long>(Person.class, entityManager.getMetamodel());
        personRepository = new QueryDslJpaRepository<Person, Long>(personEntityInfo, entityManager);
    }

/**
     * This setter method should be used only by unit tests
     * @param personRepository
     */
    protected void setPersonRepository(QueryDslJpaRepository<Person, Long> personRepository) {
        this.personRepository = personRepository;
    }
}

We also have to verify that the created repository implementation is working as expected. This means that unit tests must be written. The source code of the unit tests is given in following:

import com.mysema.query.types.Predicate;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.QueryDslJpaRepository;

import static junit.framework.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

public class PaginatingPersonRepositoryImplTest {

private static final int PAGE_INDEX = 2;
    private static final long PERSON_COUNT = 5;
    private static final String PROPERTY_LASTNAME = "lastName";
    private static final String SEARCH_TERM = "searchTerm";

private PaginatingPersonRepositoryImpl repository;

private QueryDslJpaRepository personRepositoryMock;

@Before
    public void setUp() {
        repository = new PaginatingPersonRepositoryImpl();

personRepositoryMock = mock(QueryDslJpaRepository.class);
        repository.setPersonRepository(personRepositoryMock);
    }

@Test
    public void findAllPersons() {
        repository.findAllPersons();

ArgumentCaptor<Sort> sortArgument = ArgumentCaptor.forClass(Sort.class);
        verify(personRepositoryMock, times(1)).findAll(sortArgument.capture());

Sort sort = sortArgument.getValue();
        assertEquals(Sort.Direction.ASC, sort.getOrderFor(PROPERTY_LASTNAME).getDirection());
    }

@Test
    public void findPersonCount() {
        when(personRepositoryMock.count(any(Predicate.class))).thenReturn(PERSON_COUNT);

long actual = repository.findPersonCount(SEARCH_TERM);

verify(personRepositoryMock, times(1)).count(any(Predicate.class));
        verifyNoMoreInteractions(personRepositoryMock);

assertEquals(PERSON_COUNT, actual);
    }

@Test
    public void findPersonsForPage() {
        List<Person> expected = new ArrayList<Person>();
        Page foundPage = new PageImpl<Person>(expected);

when(personRepositoryMock.findAll(any(Predicate.class), any(Pageable.class))).thenReturn(foundPage);

List<Person> actual = repository.findPersonsForPage(SEARCH_TERM, PAGE_INDEX);

ArgumentCaptor<Pageable> pageSpecificationArgument = ArgumentCaptor.forClass(Pageable.class);
        verify(personRepositoryMock, times(1)).findAll(any(Predicate.class), pageSpecificationArgument.capture());
        verifyNoMoreInteractions(personRepositoryMock);

Pageable pageSpecification = pageSpecificationArgument.getValue();

assertEquals(PAGE_INDEX, pageSpecification.getPageNumber());
        assertEquals(PaginatingPersonRepositoryImpl.NUMBER_OF_PERSONS_PER_PAGE, pageSpecification.getPageSize());
        assertEquals(Sort.Direction.ASC, pageSpecification.getSort().getOrderFor(PROPERTY_LASTNAME).getDirection());

assertEquals(expected, actual);
    }
}

Creating the Spring Data JPA repository

Now it is time to make the custom functionality available to the users of the repository. This is done by making the repository interface extend the created custom interface. As you might remember, the repository interface of my example application is called PersonRepository. Its source code is given in following:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;

public interface PersonRepository extends JpaRepository<Person, Long>, PaginatingPersonRepository, QueryDslPredicateExecutor<Person> {

}

Using the Custom Functionality

Now when the logic related to pagination and sorting has been moved to the custom repository implementation, the service layer will basically just delegate the method calls forward to the repository and act as a transaction boundary. The PersonService interface has stayed intact, but there has been some changes to the following methods of the RepositoryPersonService class:

  • public long count(String searchTerm)
  • public List<Person> findAll()
  • public List<Person> search(String searchTerm, int pageIndex)

All these methods are simply delegating the method call forward to the repository. The source code of the relevant parts of the RepositoryPersonService class 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;

@Service
public class RepositoryPersonService implements PersonService {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryPersonService.class);

protected static final int NUMBER_OF_PERSONS_PER_PAGE = 5;

@Resource
    private PersonRepository personRepository;

@Transactional
    @Override
    public long count(String searchTerm) {
        LOGGER.debug("Getting person count for search term: " + searchTerm);
        return personRepository.findPersonCount(searchTerm);
    }

@Transactional(readOnly = true)
    @Override
    public List<Person> findAll() {
        LOGGER.debug("Finding all persons");
        return personRepository.findAllPersons();
    }

@Transactional(readOnly = true)
    @Override
    public List<Person> search(String searchTerm, int pageIndex) {
        LOGGER.debug("Searching persons with search term: " + searchTerm);
        return personRepository.findPersonsForPage(searchTerm, pageIndex);
    }
}

Changes made to the RepositoryPersonService class means that its unit tests must be changed as well. The source code of the changed unit tests is given in following:

import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.*;

public class RepositoryPersonServiceTest {

private static final int PAGE_INDEX = 1;
    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 count() {
        personService.count(SEARCH_TERM);

verify(personRepositoryMock, times(1)).findPersonCount(SEARCH_TERM);
        verifyNoMoreInteractions(personRepositoryMock);
    }

@Test
    public void findAll() {
        List<Person> persons = new ArrayList<Person>();
        when(personRepositoryMock.findAllPersons()).thenReturn(persons);

List<Person> returned = personService.findAll();

verify(personRepositoryMock, times(1)).findAllPersons();
        verifyNoMoreInteractions(personRepositoryMock);

assertEquals(persons, returned);
    }

@Test
    public void search() {
        personService.search(SEARCH_TERM, PAGE_INDEX);

verify(personRepositoryMock, times(1)).findPersonsForPage(SEARCH_TERM, PAGE_INDEX);
        verifyNoMoreInteractions(personRepositoryMock);
    }
}

What is Next?

I have now described to you how you can add custom functionality to a Spring Data repository. Even though this can be a handy in some situations, I think that the situation described in this blog entry is not one of them. To be honest, the benefits obtained from the proposed solution are purely theoretical, and I cannot recommend using this approach for eliminating the dependency between the service layer and Spring Data JPA. You simply don’t get enough bang for your buck.

Sometimes adding functionality to a single repository is not enough. You can also add custom functionality to all repositories. The implementation of this is left as an exercise for the reader.

The last part of my Spring Data JPA tutorial will summarize what we have learned and give some suggestions about the usage of Spring Data JPA.

PS. If you want to a take a closer look of my example application, you can get it from Github.

Spring Data JPA教程, 第八部分:Adding Functionality to a Repository (未翻译)的更多相关文章

  1. Spring Data JPA教程, 第三部分: Custom Queries with Query Methods(翻译)

    在本人的Spring Data JPA教程的第二部分描述了如何用Spring Data JPA创建一个简单的CRUD应用,本博文将描述如何在Spring Data JPA中使用query方法创建自定义 ...

  2. Spring Data JPA 教程(翻译)

    写那些数据挖掘之类的博文 写的比较累了,现在翻译一下关于spring data jpa的文章,觉得轻松多了. 翻译正文: 你有木有注意到,使用Java持久化的API的数据访问代码包含了很多不必要的模式 ...

  3. Spring Data JPA教程, 第二部分: CRUD(翻译)

    我的Spring Data Jpa教程的第一部分描述了,如何配置Spring Data JPA,本博文进一步描述怎样使用Spring Data JPA创建一个简单的CRUD应用.该应用要求如下: pe ...

  4. Spring Data JPA教程,第一部分: Configuration(翻译)

    Spring Data JPA项目旨在简化基于仓库的JPA的创建并减少与数据库交互的所需的代码量.本人在自己的工作和个人爱好项目中已经使用一段时间,它却是是事情如此简单和清洗,现在是时候与你分享我的知 ...

  5. Spring Data JPA教程, 第七部分: Pagination(未翻译)

    The previous part of my Spring Data JPA tutorialdescribed how you can sort query results with Spring ...

  6. Spring Data JPA教程, 第六部分: Sorting(未翻译)

    The fifth part of my Spring Data JPA tutorialdescribed how you can create advanced queries with Spri ...

  7. Spring Data JPA教程, 第五部分: Querydsl(未翻译)

    The fourth part of my Spring Data JPA tutorialdescribed how you can implement more advanced queries ...

  8. Spring Data JPA教程, 第四部分: JPA Criteria Queries(未翻译)

    The third part of my Spring Data JPA tutorialdescribed how you can create custom queries by using qu ...

  9. SpringBoot系列之Spring Data Jpa集成教程

    SpringBoot系列之Spring Data Jpa集成教程 Spring Data Jpa是属于Spring Data的一个子项目,Spring data项目是一款集成了很多数据操作的项目,其下 ...

随机推荐

  1. 概述什么是OSGi框架

    现 在越来越多的Java开发人员在谈论OSGi是有其道理的.在几年前上学的时候我进行了比较多的Eclipse插件开发,当时就亲身感觉到Eclipse 插件体系的灵活与强大,而该体系与OSGi也可谓一脉 ...

  2. 装个Redmine真是麻烦啊

    弄个大半天终于看到这个界面出来了,不容易啊

  3. Codeforces Round #271 (Div. 2)

    A. Keyboard 题意:一个人打字,可能会左偏一位,可能会右偏一位,给出一串字符,求它本来的串 和紫书的破损的键盘一样 #include<iostream> #include< ...

  4. ASP.NET MVC @helper使用说明

    简单的 @helper 方法应用场景 Razor中的@helper语法让您能够轻松创建可重用的方法,此方法可以在您的视图模板中封装输出功能.他们使代码能更好地重用,也使代码更具有可读性. 在我们定义@ ...

  5. fancybox 点击 js脚本判断验证,fancybox的宽度高度设置

    当我们在使用fancybox做弹出窗口的时候,可能在弹窗之前就需要判断一些验证条件,例如我这里有个案例,用户必须先得勾选一个 那么怎么做呢?我们用到fancybox的一个onStart方法就可以了 $ ...

  6. Darwin Streaming Server Relay Setting

    安装完Darwin Streaming Server,就可以使用VLC通过RTSP协议播放流媒体文件了.但是我现在有一个需求,需要将一台DSS(假设为A机)上的媒体文件发送到另一台DSS(假设为B机) ...

  7. “自私”的Linux

    导读 “如果当时我真的知道从头建立一个操作系统的难度,肯定是不会有勇气去做的.”1991年8月25日,随着林纳斯·托瓦兹(Linus Torvalds)这句“天真”的描述,Linux系统正式与世人见面 ...

  8. sql test

    1.用户表 找出id=2及他的朋友 select * from user t where id=2 or t.id=(select friend from user where id=2); sele ...

  9. 翻译【ElasticSearch Server】第一章:开始使用ElasticSearch集群(4)

    停止ElasticSearch(Shutting down ElasticSearch) 尽管我们期望集群(或节点)终生完美运行,我们最终可能需要重启或者正确的停止它(例如,维护).有三种方式来停止E ...

  10. "_ITERATOR_DEBUG_LEVEL"的不匹配项: 值"0"不匹配值"2"

    error: 1>vtkCommon.lib(vtkDebugLeaksManager.obj) : error LNK2038: 检测到“_ITERATOR_DEBUG_LEVEL”的不匹配项 ...