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

在本人的Spring Data JPA教程的第二部分描述了如何用Spring Data JPA创建一个简单的CRUD应用,本博文将描述如何在Spring Data JPA中使用query方法创建自定义查询,为了有一个合理的示例,我为我的应用创建了三个要求:

  • 实现通过他们的last name作为搜索条件搜索到person.
  • 搜索功能必须返回这样的person,它们的last name准确匹配搜索条件。
  • 搜索不区分大小写.

现在开始扩展示例应用.

所需步骤

完成要求的步骤如下:

  • 创建一个query方法.
  • 使用该创建的query方法.

Spring Data JPA提供了三种query方法的不同方式来创建自定义查询,每一种方式描述如下.

通过解析方法名创建查询

Spring Data JPA有一个内置的查询创建机制,可用于直接从查询方法的方法名解析查询。这种机制首先从查询方法移除共同的前缀,并且从方法名称的余下部分解析查询的约束。查询生成器机制更多的细节Defining Query Methods Subsection of Spring Data JPA reference documentation

使用这种方式是相当简单的。你所做的就是确保你的repository接口的方法名称的创建是与entity对象的属性名称与支持的关键词相结合的。Query Creation Subsection of the Spring Data JPA reference documentation 有很好的关于支持的关键词的用法的例子。

用这种方式的repository方法的源码如下:

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

/**
 * 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> {

    /**
     * Finds persons by using the last name as a search criteria.
     * @param lastName  
     * @return  A list of persons which last name is an exact match with the given last name.
     *          If no persons is found, this method returns an empty list.
     */
    public List<Person> findByLastName(String lastName);
}

 这种方式的优势是,它是相当快速的实现简单的查询。另一方面,如果你的查询有很多参数,方法名称将是相当冗长、丑陋。另外,如果你需要的关键词不被Spring Data JPA支持,你就倒霉了。

一个很好的例子是如此情形:此刻你无法在你的方法的名称使用小写的关键词。这意味着,这种方式不能用于完成我在开始指定的要求。

JPA 命名查询

Spring Data JPA还提供JPA命名查询的支持你有以下声明命名查询方案:

  • 您可以以JPA查询语言使用 named-queryXML元素或@ NamedQuery注释来创建命名查询。
  • 如果你准备将你的应用与具体的数据平台绑定,您可以以SQL方式使用named-native-queryXML元素或@NamedNative查询创建来创建查询。

使用所创建的命名查询你所要做的唯一的事情是,你的资料库界面,以配合您的命名查询的名称命名的查询方法。我选择在我的实体类中使用@ NamedQuery注释指定命名查询

Person类的源码如下:

import org.apache.commons.lang.builder.ToStringBuilder;

import javax.persistence.*;

/**
 * An entity class which contains the information of a single person.
 * @author Petri Kainulainen
 */
@Entity
@NamedQuery(name = "Person.findByName", query = "SELECT p FROM Person p WHERE LOWER(p.lastName) = LOWER(?1)")
@Table(name = "persons")
public class Person {
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    
    @Column(name = "creation_time", nullable = false)
    private Date creationTime;
    
    @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name", nullable = false)
    private String lastName;
    
    @Column(name = "modification_time", nullable = false)
    private Date modificationTime;
    
    @Version
    private long version = 0;

    public Long getId() {
        return id;
    }

    /**
     * Gets a builder which is used to create Person objects.
     * @param firstName The first name of the created user.
     * @param lastName  The last name of the created user.
     * @return  A new Builder instance.
     */
    public static Builder getBuilder(String firstName, String lastName) {
        return new Builder(firstName, lastName);
    }
    
    public Date getCreationTime() {
        return creationTime;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    /**
     * Gets the full name of the person.
     * @return  The full name of the person.
     */
    @Transient
    public String getName() {
        StringBuilder name = new StringBuilder();
        
        name.append(firstName);
        name.append(" ");
        name.append(lastName);
        
        return name.toString();
    }

    public Date getModificationTime() {
        return modificationTime;
    }

    public long getVersion() {
        return version;
    }

    public void update(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    @PreUpdate
    public void preUpdate() {
        modificationTime = new Date();
    }
    
    @PrePersist
    public void prePersist() {
        Date now = new Date();
        creationTime = now;
        modificationTime = now;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

    /**
     * A Builder class used to create new Person objects.
     */
    public static class Builder {
        Person built;

        /**
         * Creates a new Builder instance.
         * @param firstName The first name of the created Person object.
         * @param lastName  The last name of the created Person object.
         */
        Builder(String firstName, String lastName) {
            built = new Person();
            built.firstName = firstName;
            built.lastName = lastName;
        }

        /**
         * Builds the new Person object.
         * @return  The created Person object.
         */
        public Person build() {
            return built;
        }
    }

    /**
     * This setter method should only be used by unit tests.
     * @param id
     */
    protected void setId(Long id) {
        this.id = id;
    }
}

我的PersonRepository接口的相关部分如下:

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

/**
 * 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> {

    /**
     * Finds person by using the last name as a search criteria.
     * @param lastName
     * @return  A list of persons whose last name is an exact match with the given last name.
     *          If no persons is found, this method returns null.
     */
    public List<Person> findByName(String lastName);
}

如果你的应用规模小或者你不得不使用原生态的查询,使用命名查询是有效地选择,如果你的应用有大量的自定义查询,这种方式将你的entity类的代码充斥垃圾查询声明,你当然可以使用XML配置来避免这种情况,不过在本人看来这种方式更恐怖 

Using named queries is valid option if your application is small or if you have to use native queries. If your application has a lot of custom queries, this approach will litter the code of your entity class with query declarations (You can of course use the XML configuration to avoid this but in my opinion this approach is even more horrible).

@Query注解

@Query注解被用于通过使用JPA查询语言创建查询,并且直接绑定这些查询到你的repository接口的方法,当查询方法别调用,Spring Data JPA将执行通过@Query注解指定的查询(如果@Query 注解与命名查询有冲突,将执行通过使用@Query 注解指定的查询)。

通过使用这种方式实现的repository方法的源码如下:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

/**
 * 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> {

    /**
     * Finds a person by using the last name as a search criteria.
     * @param lastName
     * @return  A list of persons whose last name is an exact match with the given last name.
     *          If no persons is found, this method returns an empty list.
     */
    @Query("SELECT p FROM Person p WHERE LOWER(p.lastName) = LOWER(:lastName)")
    public List<Person> find(@Param("lastName") String lastName);
}

 这种方式使你可以访问JPA查询语言,并且在它们所属的repository层保持你的查询。另一方面,如果JPA查询语言不能不能用于创建你需要的查询,你不能使用@Query 注解(在本教程的下一部分我将描述更多的高级策略)

使用创建的查询方法

在Spring Data JPA中,本人已经向你描述三种方式来创建查询方法,下一步是来看一看用于创建查询方法的服务类.

SearchType枚举标示用于查询的方法,其源码如下:

/**
 * Describes the search type of the search. Legal values are:
 * <ul>
 *     <li>METHOD_NAME which means that the query is obtained from the method name of the query method.</li>
 *     <li>NAMED_QUERY which means that a named query is used.</li>
 *     <li>QUERY_ANNOTATION which means that the query method annotated with @Query annotation is used.</li>
 * </ul>
 * @author Petri Kainulainen
 */
public enum SearchType {
    METHOD_NAME,
    NAMED_QUERY,
    QUERY_ANNOTATION;
}

SearchDTO是一个简单的DTO对象,它包含了用户给出的、标识同于查询方法的搜索条件,其源码如下:

import org.apache.commons.lang.builder.ToStringBuilder;

/**
 * A DTO class which is used as a form object in the search form.
 * @author Petri Kainulainen
 */
public class SearchDTO {

    private String searchTerm;

    private SearchType searchType;

    public SearchDTO() {

    }

    public String getSearchTerm() {
        return searchTerm;
    }

    public void setSearchTerm(String searchTerm) {
        this.searchTerm = searchTerm;
    }

    public SearchType getSearchType() {
        return searchType;
    }

    public void setSearchType(SearchType searchType) {
        this.searchType = searchType;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }
}

PersonService接口得到一新方法, PersonService接口相关部分如下:

/**
 * Declares methods used to obtain and modify person information.
 * @author Petri Kainulainen
 */
public interface PersonService {

    /**
     * Searches persons by using the search criteria given as a parameter.
     * @param searchCriteria
     * @return  A list of persons matching with the search criteria. If no persons is found, this method
     *          returns an empty list.
     * @throws IllegalArgumentException if search type is not given.
     */
    public List<Person> search(SearchDTO searchCriteria);
}

 search()方法的具体实现在于选择正确的查询方法并传递给出的搜索词,我的search()方法实现源码如下:

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(SearchDTO searchCriteria) {
        LOGGER.debug("Searching persons with search criteria: " + searchCriteria);
        
        String searchTerm = searchCriteria.getSearchTerm();
        SearchType searchType = searchCriteria.getSearchType();
        
        if (searchType == null) {
            throw new IllegalArgumentException();
        }
         
        return findPersonsBySearchType(searchTerm, searchType);
    }
    
    private List<Person> findPersonsBySearchType(String searchTerm, SearchType searchType) {
        List<Person> persons;

        if (searchType == SearchType.METHOD_NAME) {
            LOGGER.debug("Searching persons by using method name query creation.");
            persons = personRepository.findByLastName(searchTerm);
        }
        else if (searchType == SearchType.NAMED_QUERY) {
            LOGGER.debug("Searching persons by using named query");
            persons = personRepository.findByName(searchTerm);
        }
        else {
            LOGGER.debug("Searching persons by using query annotation");
            persons = personRepository.find(searchTerm);
        }

        return persons;
    }
}

 当然,创建的search()必须进行测试。给出了以下相关的单元测试的源代码:

import org.junit.Before;
import org.junit.Test;

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

public class RepositoryPersonServiceTest {

    private static final String LAST_NAME = "Bar";
    
    private RepositoryPersonService personService;

    private PersonRepository personRepositoryMock;

    @Before
    public void setUp() {
        personService = new RepositoryPersonService();

        personRepositoryMock = mock(PersonRepository.class);
        personService.setPersonRepository(personRepositoryMock);
    }

    @Test
    public void searchWhenSearchTypeIsMethodName() {
        SearchDTO searchCriteria = createSearchDTO(LAST_NAME, SearchType.METHOD_NAME);
        List<Person> expected = new ArrayList<Person>();
        when(personRepositoryMock.findByLastName(searchCriteria.getSearchTerm())).thenReturn(expected);
        
        List<Person> actual = personService.search(searchCriteria);
        
        verify(personRepositoryMock, times(1)).findByLastName(searchCriteria.getSearchTerm());
        verifyNoMoreInteractions(personRepositoryMock);
        
        assertEquals(expected, actual);
    }

    @Test
    public void searchWhenSearchTypeIsNamedQuery() {
        SearchDTO searchCriteria = createSearchDTO(LAST_NAME, SearchType.NAMED_QUERY);
        List<Person> expected = new ArrayList<Person>();
        when(personRepositoryMock.findByName(searchCriteria.getSearchTerm())).thenReturn(expected);

        List<Person> actual = personService.search(searchCriteria);

        verify(personRepositoryMock, times(1)).findByName(searchCriteria.getSearchTerm());
        verifyNoMoreInteractions(personRepositoryMock);

        assertEquals(expected, actual);
    }

    @Test
    public void searchWhenSearchTypeIsQueryAnnotation() {
        SearchDTO searchCriteria = createSearchDTO(LAST_NAME, SearchType.QUERY_ANNOTATION);
        List<Person> expected = new ArrayList<Person>();
        when(personRepositoryMock.find(searchCriteria.getSearchTerm())).thenReturn(expected);

        List<Person> actual = personService.search(searchCriteria);

        verify(personRepositoryMock, times(1)).find(searchCriteria.getSearchTerm());
        verifyNoMoreInteractions(personRepositoryMock);

        assertEquals(expected, actual);
    }

    @Test(expected = IllegalArgumentException.class)
    public void searchWhenSearchTypeIsNull() {
        SearchDTO searchCriteria = createSearchDTO(LAST_NAME, null);

        personService.search(searchCriteria);

        verifyZeroInteractions(personRepositoryMock);
    }
    
    private SearchDTO createSearchDTO(String searchTerm, SearchType searchType) {
        SearchDTO searchCriteria = new SearchDTO();
        searchCriteria.setSearchTerm(searchTerm);
        searchCriteria.setSearchType(searchType);
        return searchCriteria;
    }
}

下一步内容?

本人已经向你描述了在Spring Data JPA中如何使用query方法来创建自定义查询,如果你对查看我的实践的示例应用感兴趣,你可以从Github获取,我的Spring Data JPA教程的下一部分描述如何用Spring Data JPA创建JPA条件查询.

--------------------------------------------------------------------------- 

本系列Spring Data JPA 教程翻译系本人原创

作者 博客园 刺猬的温驯 

本文链接http://www.cnblogs.com/chenying99/archive/2013/06/19/3143539.html

本文版权归作者所有,未经作者同意,严禁转载及用作商业传播,否则将追究法律责任。

原文地址:https://www.cnblogs.com/chenying99/p/3143539.html