本文參考了Spring Data JPA官方文檔,引用了部分文檔的代碼。
Spring Data JPA是Spring基于Hibernate開發的一個JPA框架。如果用過Hibernate或者MyBatis的話,就會知道對象關系映射(ORM)框架有多么方便。但是Spring Data JPA框架功能更進一步,為我們做了 一個數據持久層框架幾乎能做的任何事情。下面來逐步介紹它的強大功能。
添加依賴
我們可以簡單的聲明Spring Data JPA的單獨依賴項。以Gradle為例,依賴項如下,Spring Data JPA會自動添加它的Spring依賴項。當前版本需要Spring框架版本為4.3.7.RELEASE
或更新,使用舊版本的Spring框架可能會出現bug。由于Spring Data JPA基于Hibernate,所以別忘了添加Hibernate的依賴項。
compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.11.1.RELEASE'
compile group: 'org.hibernate', name: 'hibernate-core', version: '5.2.8.Final'
基本使用
創建環境
Spring Data JPA也是一個JPA框架,因此我們需要數據源、JPA Bean、數據庫驅動、事務管理器等等。下面以XML配置為例,我們來配置一下所需的Bean。重點在于<jpa:repositories base-package="yitian.study.dao"/>
一句,它告訴Spring去哪里尋找并創建這些接口類。
<!--啟用注解配置和包掃描-->
<context:annotation-config/>
<context:component-scan base-package="yitian.study"/>
<!--創建Spring Data JPA實例對象-->
<jpa:repositories base-package="yitian.study.dao"/>
<!--數據源-->
<bean id="dataSource"
class="com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource">
<property name="useSSL" value="false"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="user" value="root"/>
<property name="password" value="12345678"/>
</bean>
<!--JPA工廠對象-->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="packagesToScan" value="yitian.study.entity"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="generateDdl" value="true"/>
<property name="showSql" value="true"/>
</bean>
</property>
</bean>
<!--事務管理器-->
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!--事務管理-->
<tx:advice id="transactionAdvice"
transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="daoPointCut" expression="execution(* yitian.study.dao.*.*(..))"/>
<aop:advisor advice-ref="transactionAdvice" pointcut-ref="daoPointCut"/>
</aop:config>
創建DAO對象
前幾天學了一點Groovy,再回頭看看Java,實在是麻煩。所以這里我用Groovy寫的實體類,不過語法和Java很相似。大家能看懂意思即可。不過確實Groovy能比Java少些很多代碼,對開發挺有幫助的。有興趣的同學可以看看我的Groovy學習筆記。
Groovy類的字段默認是私有的,方法默認是公有的,分號可以省略,對于默認字段Groovy編譯器還會自動生成Getter和Setter,可以減少不少代碼量。只不過equals等方法不能自動生成,多少有點遺憾。這里使用了JPA注解,建立了一個實體類和數據表的映射。
@Entity
class User {
@Id
@GeneratedValue
int id
@Column(unique = true, nullable = false)
String username
@Column(nullable = false)
String nickname
@Column
String email
@Column
LocalDate birthday
@Column(nullable = false)
LocalDateTime registerTime
String toString() {
"User(id:$id,username:$username,nickname:$nickname,email:$email,birthday:$birthday,registerTime:$registerTime)"
}
}
然后就是Spring Data JPA的魔法部分了!我們繼承Spring提供的一個接口,放到前面jpa:repositories
指定的包下。
interface CommonUserRepository extends CrudRepository<User, Integer> {
}
然后測試一下,會發生什么事情呢?查看一下數據庫就會發現數據已經成功插入了。好吧,好像沒什么有魔力的事情。
@RunWith(SpringRunner)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
class DaoTest {
@Autowired
CommonUserRepository commonUserRepository
@Test
void testCrudRepository() {
User user = new User(username: 'yitian', nickname: '易天', registerTime: LocalDateTime.now())
commonUserRepository.save(user)
}
}
這次我們在接口中再定義一個方法。
interface CommonUserRepository extends CrudRepository<User, Integer> {
List<User> getByUsernameLike(String username)
}
我們再測試一下。這里也是用的Groovy代碼,意思應該很容易懂,就是循環20次,然后插入20個用戶,用戶的名字和郵箱都是由循環變量生成的。然后調用我們剛剛的方法。這次真的按照我們的要求查詢出了用戶名以2結尾的所有用戶!
@Test
void testCrudRepository() {
(1..20).each {
User user = new User(username: "user$it", nickname: "用戶$it", email: "user$it@yitian.com", registerTime: LocalDateTime.now())
commonUserRepository.save(user)
}
List<User> users = commonUserRepository.getByUsernameLike('%2')
println(users)
}
//結果如下
//[User(id:3,username:user2,nickname:用戶2,email:user2@yitian.com,birthday:null,registerTime:2017-03-08T20:25:58), User(id:13,username:user12,nickname:用戶12,email:user12@yitian.com,birthday:null,registerTime:2017-03-08T20:25:59)]
Spring Data 接口
從上面的例子中我們可以看到Spring Data JPA的真正功能了。我們只要繼承它提供的接口,然后按照命名規則定義相應的查詢方法。Spring就會自動創建實現了該接口和查詢方法的對象,我們直接使用就可以了。也就是說,Spring Data JPA連查詢方法都可以幫我們完成,我們幾乎什么也不用干了。
下面來介紹一下Spring的這些接口。上面的例子中,我們繼承了CrudRepository
接口。CrudRepository
接口的定義如下。如果我們需要增刪查改功能。只需要繼承該接口就可以立即獲得該接口的所有功能。CrudRepository
接口有兩個泛型參數,第一個參數是實際儲存的類型,第二個參數是主鍵。
public interface CrudRepository<T, ID extends Serializable>
extends Repository<T, ID> {
<S extends T> S save(S entity);
T findOne(ID primaryKey);
Iterable<T> findAll();
Long count();
void delete(T entity);
boolean exists(ID primaryKey);
// … more functionality omitted.
}
CrudRepository
接口雖然方便,但是暴露了增刪查改的所有方法,如果你的DAO層不需要某些方法,就不要繼承該接口。Spring提供了其他幾個接口,org.springframework.data.repository.Repository
接口沒有任何方法。
如果對數據訪問需要詳細控制,就可以使用該接口。PagingAndSortingRepository
接口則提供了分頁和排序功能。PagingAndSortingRepository
接口的方法接受額外的Pagable和Sort對象,用來指定獲取結果的頁數和排序方式。返回類型則是Page<T>類型,我們可以調用它的方法獲取總頁數和可迭代的數據集合。下面是一個Groovy寫的例子。注意Pageable是一個接口,如果我們需要創建Pageable對象,使用PageRequest類并指定獲取的頁數和每頁的數據量。頁是從0開始計數的。
@Test
void testPagingRepository() {
int countPerPage = 5
long totalCount = pageableUserRepository.count()
int totalPage = totalCount % 5 == 0L ? totalCount / 5 : totalCount / 5 + 1
(0..totalPage - 1).each {
Page<User> users = pageableUserRepository.findAll(new PageRequest(it, countPerPage))
println "第${it}頁數據,共${users.totalPages}頁"
users.each {
println it
}
}
}
查詢方法
查詢方法可以由我們聲明的命名查詢生成,也可以像前面那樣由方法名解析。下面是官方文檔的例子。方法名稱規則如下。如果需要詳細說明的話可以查看官方文檔Appendix C: Repository query keywords一節。
- 方法名以
find…By
,read…By
,query…By
,count…By
和get…By
做開頭。在By之前可以添加Distinct表示查找不重復數據。By之后是真正的查詢條件。 - 可以查詢某個屬性,也可以使用條件進行比較復雜的查詢,例如
Between
,LessThan
,GreaterThan
,Like
,And
,Or
等。 - 字符串屬性后面可以跟
IgnoreCase
表示不區分大小寫,也可以后跟AllIgnoreCase
表示所有屬性都不區分大小寫。 - 可以使用
OrderBy
對結果進行升序或降序排序。 - 可以查詢屬性的屬性,直接將幾個屬性連著寫即可,如果可能出現歧義屬性,可以使用下劃線分隔多個屬性。
public interface PersonRepository extends Repository<User, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// 唯一查詢
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// 對某一屬性不區分大小寫
List<Person> findByLastnameIgnoreCase(String lastname);
// 所有屬性不區分大小寫
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// 啟用靜態排序
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
//查詢Person.Address.ZipCode
List<Person> findByAddressZipCode(ZipCode zipCode);
//避免歧義可以這樣
List<Person> findByAddress_ZipCode(ZipCode zipCode);
如果需要限制查詢結果也很簡單。
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
如果查詢很費時間,也可以方便的使用異步查詢。只要添加@Async注解,然后將返回類型設定為異步的即可。
@Async
Future<User> findByFirstname(String firstname);
@Async
CompletableFuture<User> findOneByFirstname(String firstname);
@Async
ListenableFuture<User> findOneByLastname(String lastname);
Spring Data擴展功能
Querydsl擴展
Querydsl擴展能讓我們以流式方式代碼編寫查詢方法。該擴展需要一個接口QueryDslPredicateExecutor
,它定義了很多查詢方法。
public interface QueryDslPredicateExecutor<T> {
T findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
long count(Predicate predicate);
boolean exists(Predicate predicate);
// … more functionality omitted.
}
只要我們的接口繼承了該接口,就可以使用該接口提供的各種方法了。
interface UserRepository extends CrudRepository<User, Long>, QueryDslPredicateExecutor<User> {
}
查詢方法可以這樣簡單的編寫。
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
Spring Web Mvc集成
這個功能需要我們引入Spring Web Mvc的相應依賴包。然后在程序中啟用Spring Data支持。使用Java配置的話,在配置類上添加@EnableSpringDataWebSupport注解。
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration { }
使用XML配置的話,添加下面的Bean聲明。
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
<!-- 如果使用Spring HATEOAS 的話用下面這個替換上面這個 -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
不管使用哪種方式,都會向Spring額外注冊幾個組件,支持Spring Data的額外功能。首先會注冊一個DomainClassConverter
,它可以自動將查詢參數或者路徑參數轉換為領域模型對象。下面的例子中,Spring Data會自動用主鍵查詢對應的用戶,然后我們直接就可以從處理方法參數中獲得用戶實例。注意,Spring Data需要調用findOne
方法查詢對象,現版本下我們必須繼承CrudRepository
,才能實現該功能。
@Controller
@RequestMapping("/users")
public class UserController {
@RequestMapping("/{id}")
public String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
另外Spring會注冊HandlerMethodArgumentResolver
、PageableHandlerMethodArgumentResolver
和SortHandlerMethodArgumentResolver
等幾個實例。它們支持從請求參數中讀取分頁和排序信息。
@Controller
@RequestMapping("/users")
public class UserController {
@Autowired UserRepository repository;
@RequestMapping
public String showUsers(Model model, Pageable pageable) {
model.addAttribute("users", repository.findAll(pageable));
return "users";
}
}
對于上面的例子,如果在請求參數中包含sort、page、size等幾個參數,它們就會被映射為Spring Data的Pageable和Sort對象。請求參數的詳細信息如下。
- page 想要獲取的頁數,默認是0,以零開始計數的。
- size 每頁的數據大小,默認是20.
- 數據的排序規則,默認是升序,也可以對多個屬性執行排序,這時候需要多個sort參數,例如
?sort=firstname&sort=lastname,asc
如果需要多個分頁對象,我們可以用@Qualifier注解,然后請求對象就可以寫成foo_page
,bar_page
這樣的了。
public String showUsers(Model model,
@Qualifier("foo") Pageable first,
@Qualifier("bar") Pageable second) { … }
如果需要自定義這些行為,可以讓配置類繼承SpringDataWebConfiguration
基類,然后重寫pageableResolver()
和sortResolver()
方法。這樣就不需要使用@EnableXXX注解了。
最后一個功能就是Querydsl 了。如果相關Jar包在類路徑上,@EnableSpringDataWebSupport
注解同樣會啟用該功能。比方說,在前面的例子中,如果在用戶用戶參數上添加下面的查詢參數。
?firstname=Dave&lastname=Matthews
那么就會被QuerydslPredicateArgumentResolver
解析為下面的查詢語句。
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
還可以將QuerydslPredicate
注解到對應類型的方法參數上,Spring會自動實例化相應的參數。為了Spring能夠準確找到應該查找什么領域對象,我們最好指定root屬性。
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
官方文檔的其他內容
JPA命名查詢
如果查詢方法不能完全滿足需要,我們可以使用自定義查詢來滿足需求。使用XML配置的話,在類路徑下添加META/orm.xml
文件,類似下面這樣。我們用named-query
就定義命名查詢了。
<?xml version="1.0" ?>
<entity-mappings
xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm
http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<named-query name="User.findByNickname">
<query>select u from User u where u.nickname=?1</query>
</named-query>
</entity-mappings>
還可以使用注解,在對應實體類上注解命名查詢。
@Entity
@NamedQuery(name = "User.findByNickname",
query = "select u from User u where u.nickname=?1")
public class User {
}
之后,在接口中聲明對應名稱的查詢方法。這樣我們就可以使用JPQL語法自定義查詢方法了。
List<User> findByNickname(String nickname)
使用Query注解
在上面的方法中,查詢方法和JPQL是對應的,但是卻不在同一個地方定義。如果查詢方法很多的話,查找和修改就很麻煩。這時候可以改用@Query注解。下面的例子直接在方法上定義了JPQL語句,如果需要引用orm.xml文件中的查詢語句,使用注解的name屬性,如果沒有指定,會使用領域模型名.方法名
作為命名查詢語句的名稱。
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}
細心的同學會發現,該注解還有一個nativeQuery屬性,用作直接執行SQL使用。如果我們將該屬性指定為true,查詢語句也要相應的修改為SQL語句。
Modifying注解
@Modifying注解用來指定某個查詢是一個更新操作,這樣可以讓Spring執行相應的優化。
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
投影
有時候數據庫和實體類之間并不存在一一對應的關系,或者根據某些情況需要隱藏數據庫中的某些字段。這可以通過投影實現。來看看Spring的例子。
假設有下面的實體類和倉庫。我們在獲取人的時候會順帶獲取它的地址。
@Entity
public class Person {
@Id @GeneratedValue
private Long id;
private String firstName, lastName;
@OneToOne
private Address address;
…
}
@Entity
public class Address {
@Id @GeneratedValue
private Long id;
private String street, state, country;
…
}
interface PersonRepository extends CrudRepository<Person, Long> {
Person findPersonByFirstName(String firstName);
}
如果不希望同時獲取地址的話,可以定義一個新接口,其中定義一些Getter方法,暴露你需要的屬性。然后倉庫方法也做相應修改。
interface NoAddresses {
String getFirstName();
String getLastName();
}
interface PersonRepository extends CrudRepository<Person, Long> {
NoAddresses findByFirstName(String firstName);
}
利用@Value注解和SpEl,我們可以靈活的組織屬性。例如下面,定義一個接口,重命名了lastname屬性。關于Spring表達式,可以看看我的文章Spring EL 簡介。
interface RenamedProperty {
String getFirstName();
@Value("#{target.lastName}")
String getName();
}
或者組合多個屬性也可以,下面的例子將姓和名組合成全名。Spring El的使用很靈活,合理使用可以達到事半功倍的效果。
interface FullNameAndCountry {
@Value("#{target.firstName} #{target.lastName}")
String getFullName();
@Value("#{target.address.country}")
String getCountry();
}
規范
這里說的規范指的是JPA 2 引入的新的編程方式實現查詢的規范。其他框架比如Hibernate也廢棄了自己的Criteria查詢方法,改為使用JPA規范的Criteria。這種方式的好處就是完全是編程式的,不需要額外的功能,使用IDE的代碼提示功能即可。但是我個人不太喜歡,一來沒怎么詳細了解,二來感覺不如JPQL這樣的查詢簡單粗暴。
廢話不多說,直接看官方的例子吧。首先倉庫接口需要繼承JpaSpecificationExecutor接口。
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
…
}
這樣倉庫接口就繼承了一組以Specification接口作參數的查詢方法,類似下面這樣。
List<T> findAll(Specification<T> spec);
而Specification又是這么個東西。所以我們要使用JPA規范的查詢方法,就需要實現toPredicate方法。
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder);
}
官方文檔有這么個例子,這個類中包含了多個靜態方法,每個方法都返回一個實現了的Specification對象。
public class CustomerSpecs {
public static Specification<Customer> isLongTermCustomer() {
return new Specification<Customer>() {
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
LocalDate date = new LocalDate().minusYears(2);
return builder.lessThan(root.get(_Customer.createdAt), date);
}
};
}
//其他方法
}
之后我們將Specification對象傳遞給倉庫中定義的方法即可。
List<Customer> customers = customerRepository.findAll(isLongTermCustomer());
多個規范組合起來的查詢也可以。
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));
Example查詢
前段時間在研究Spring的時候,發現Spring對Hibernate有一個封裝類HibernateTemplate
,它將Hibernate的Session
封裝起來,由Spring的事務管理器管理,我們只需要調用HibernateTemplate
的方法即可。在HibernateTemplate
中有一組Example方法我沒搞明白啥意思,后來才發現這是Spring提供的一組簡便查詢方式。不過這種查詢方式的介紹居然在Spring Data這個框架中。
這種方式的優點就是比較簡單,如果使用上面的JPA規范,還需要再學習很多知識。使用Example查詢的話要學習的東西就少很多了。我們只要使用已有的實體對象,創建一個例子,然后在例子上設置各種約束(即查詢條件),然后將例子扔給查詢方法即可。這種方式也有缺點,就是不能實現所有的查詢功能,我們只能進行前后綴匹配等的字符串查詢和其他類型屬性的精確查詢。
首先,倉庫接口需要繼承QueryByExampleExecutor
接口,這樣會引入一組以Example作參數的方法。然后創建一個ExampleMatcher
對象,最后再用Example
的of方法構造相應的Example對象并傳遞給相關查詢方法。我們看看Spring的例子。
ExampleMatcher
用于創建一個查詢對象,下面的代碼就創建了一個查詢對象。withIgnorePaths
方法用來排除某個屬性的查詢。withIncludeNullValues
方法讓空值也參與查詢,如果我們設置了對象的姓,而名為空值,那么實際查詢條件也是這樣的。
Person person = new Person();
person.setFirstname("Dave");
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("lastname")
.withIncludeNullValues()
.withStringMatcherEnding();
Example<Person> example = Example.of(person, matcher);
withStringMatcher
方法用于指定字符串查詢。例如下面的例子就是查詢所有昵稱以2結尾的用戶。雖然用的Groovy代碼但是大家應該很容易看懂吧。
@Test
void testExamples() {
User user = new User(nickname: '2')
ExampleMatcher matcher = ExampleMatcher.matching()
.withStringMatcher(ExampleMatcher.StringMatcher.ENDING)
.withIgnorePaths('id')
Example<User> example = Example.of(user, matcher)
Iterable<User> users = exampleRepository.findAll(example)
users.each {
println it
}
}
如果用Java 8的話還可以使用lambda表達式寫出漂亮的matcher語句。
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}
基本的審計
文章寫得非常長了,所以這里最后就在寫一個小特性吧,那就是審計功能。這里說的是很基本的審計功能,也就是追蹤誰創建和修改相關實體類。相關的注解有4個:@CreatedBy
, @LastModifiedBy
,@CreatedDate
和@LastModifiedDate
,分別代表創建和修改實體類的對象和時間。
這幾個時間注解支持JodaTime、java.util.Date
、Calender、Java 8 的新API以及long
基本類型。在我們的程序中這幾個注解可以幫我們省不少事情,比如說,一個博客系統中的文章,就可以使用這些注解輕松實現新建和修改文章的時間記錄。
class Customer {
@CreatedBy
private User user;
@CreatedDate
private DateTime createdDate;
// … further properties omitted
}
當然不是直接用了這兩個注解就行了。我們還需要啟用審計功能。審計功能需要spring-aspects.jar
這個包,因此首先需要引入Spring Aspects。在Gradle項目中是這樣的。
compile group: 'org.springframework', name: 'spring-aspects', version: '4.3.7.RELEASE'
如果使用Java配置的話,在配置類上使用@EnableJpaAuditing注解。
@Configuration
@EnableJpaAuditing
class Config {
如果使用XML配置的話,添加下面的一行。
<jpa:auditing/>
最后在實體類上添加@EntityListeners(AuditingEntityListener)
注解。這樣,以后當我們創建和修改實體類時,不需要管@LastModifiedDate
和@CreatedDate
這種字段,Spring會幫我們完成一切。
@Entity
@EntityListeners(AuditingEntityListener)
class Article {