本文參考了Spring Data JPA官方文檔,引用了部分文檔的代碼。html
Spring Data JPA是Spring基於Hibernate開發的一個JPA框架。若是用過Hibernate或者MyBatis的話,就會知道對象關係映射(ORM)框架有多麼方便。可是Spring Data JPA框架功能更進一步,爲咱們作了 一個數據持久層框架幾乎能作的任何事情。下面來逐步介紹它的強大功能。java
咱們能夠簡單的聲明Spring Data JPA的單獨依賴項。以Gradle爲例,依賴項以下,Spring Data JPA會自動添加它的Spring依賴項。當前版本須要Spring框架版本爲4.3.7.RELEASE
或更新,使用舊版本的Spring框架可能會出現bug。因爲Spring Data JPA基於Hibernate,因此別忘了添加Hibernate的依賴項。mysql
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去哪裏尋找並建立這些接口類。web
<!--啓用註解配置和包掃描--> <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>
前幾天學了一點Groovy,再回頭看看Java,實在是麻煩。因此這裏我用Groovy寫的實體類,不過語法和Java很類似。你們能看懂意思便可。不過確實Groovy能比Java少些不少代碼,對開發挺有幫助的。有興趣的同窗能夠看看個人Groovy學習筆記。spring
Groovy類的字段默認是私有的,方法默認是公有的,分號能夠省略,對於默認字段Groovy編譯器還會自動生成Getter和Setter,能夠減小很多代碼量。只不過equals等方法不能自動生成,多少有點遺憾。這裏使用了JPA註解,創建了一個實體類和數據表的映射。sql
@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> { }
而後測試一下,會發生什麼事情呢?查看一下數據庫就會發現數據已經成功插入了。好吧,好像沒什麼有魔力的事情。express
@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結尾的全部用戶!app
@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 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);
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 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對象。請求參數的詳細信息以下。
?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"; } }
若是查詢方法不能徹底知足須要,咱們可使用自定義查詢來知足需求。使用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)
在上面的方法中,查詢方法和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註解用來指定某個查詢是一個更新操做,這樣可讓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)));
前段時間在研究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