Spring Data JPA教程, 第八部分:Adding Functionality to a Repository (未翻译)

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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值