Spring Data JPA 介紹和使用
本文參考了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
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"/>
<!--啟用註解配置和包掃描-->
<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型別,我們可以呼叫它的方法獲取總頁數和可迭代的資料集合。下面是一個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 {