【spring boot 系列】spring data jpa 全面解析(實踐 + 源碼分析)

前言

本文將從示例、原理、應用3個方面介紹spring data jpa。java

如下分析基於spring boot 2.0 + spring 5.0.4版本源碼mysql

概述

JPA是什麼?

JPA (Java Persistence API) 是 Sun 官方提出的 Java 持久化規範。它爲 Java 開發人員提供了一種對象/關聯映射工具來管理 Java 應用中的關係數據。他的出現主要是爲了簡化現有的持久化開發工做和整合 ORM 技術,結束如今 Hibernate,TopLink,JDO 等 ORM 框架各自爲營的局面。值得注意的是,JPA 是在充分吸取了現有 Hibernate,TopLink,JDO 等ORM框架的基礎上發展而來的,具備易於使用,伸縮性強等優勢。從目前的開發社區的反應上看,JPA 受到了極大的支持和讚賞,其中就包括了 Spring 與 EJB3.0 的開發團隊。git

注意:JPA 是一套規範,不是一套產品,那麼像 Hibernate,TopLink,JDO 他們是一套產品,若是說這些產品實現了這個 JPA 規範,那麼咱們就能夠叫他們爲 JPA 的實現產品。

spring data jpa

Spring Data JPA 是 Spring 基於 ORM 框架、JPA 規範的基礎上封裝的一套 JPA 應用框架,底層使用了 Hibernate 的 JPA 技術實現,可以使開發者用極簡的代碼便可實現對數據的訪問和操做。它提供了包括增刪改查等在內的經常使用功能,且易於擴展!學習並使用 Spring Data JPA 能夠極大提升開發效率!程序員

spring data jpa 讓咱們解脫了 DAO 層的操做,基本上全部 CRUD 均可以依賴於它來實現

示例

配置

maven

<dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
            
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

爲什麼不指定版本號呢?
由於 spring boot 的 pom 依賴了 parent,部分 jar 包的版本已在 parent 中指定,故不建議顯示指定正則表達式

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

application.properties

spring.datasource.url=jdbc:mysql://*:*/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
spring.datasource.username=
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true

配置就這麼簡單,下面簡單介紹下spring.jpa.properties.hibernate.hbm2ddl.auto有幾種配置:算法

  • create:每次加載Hibernate時都會刪除上一次生成的表(包括數據),而後從新生成新表,即便兩次沒有任何修改也會這樣執行。適用於每次執行單測前清空數據庫的場景。
  • create-drop:每次加載Hibernate時都會生成表,但當SessionFactory關閉時,所生成的表將自動刪除。
  • update:最經常使用的屬性值,第一次加載Hibernate時建立數據表(前提是須要先有數據庫),之後加載Hibernate時不會刪除上一次生成的表,會根據實體更新,只新增字段,不會刪除字段(即便實體中已經刪除)。
  • validate:每次加載Hibernate時都會驗證數據表結構,只會和已經存在的數據表進行比較,根據model修改表結構,但不會建立新表。

不配置此項,表示禁用自動建表功能spring

Repository

創建 entitysql

@Entity
@Data
public class User {

    @Id
    @GeneratedValue
    private long id;
    @Column(nullable = false, unique = true)
    private String userName;
    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    private int age;
}

聲明 UserRepository接口,繼承JpaRepository,默認支持簡單的 CRUD 操做,很是方便數據庫

public interface UserRepository extends JpaRepository<User, Long> {

    User findByUserName(String userName);

}

單測

@Slf4j
public class UserTest extends ApplicationTests {

    @Autowired
    private UserRepository userRepository;

    @Test
    @Transactional
    public void userTest() {
        User user = new User();
        user.setUserName("wyk");
        user.setAge(30);
        user.setPassword("aaabbb");
        userRepository.save(user);
        User item = userRepository.findByUserName("wyk");
        log.info(JsonUtils.toJson(item));
    }
}

這裏標記@Transactional,開啓事務功能是爲了單元測試的時候不形成垃圾數據緩存

源代碼下載: 請戳這裏

原理

不少人會有疑問,直接聲明接口不須要具體實現就能完成數據庫的操做?下面就簡單介紹下 spring data jpa 的實現原理。

如何工做

對單測進行debug,能夠發現userRepository被注入了一個動態代理,被代理的類是JpaRepository的一個實現SimpleJpaRespositry

clipboard.png

繼續往下debug,在進到findByUserName方法的時候,發現被上文提到的JdkDynamicAopProxy捕獲,而後通過一系列的方法攔截,最終進到QueryExecutorMethodInterceptor.doInvoke中。這個攔截器主要作的事情就是判斷方法類型,而後執行對應的操做.
咱們的findByUserName屬於自定義查詢,因而就進入了查詢策略對應的execute方法。在執行execute時,會先選取對應的JpaQueryExecution,調用AbtractJpaQuery.getExecution()

protected JpaQueryExecution getExecution() {
  if (method.isStreamQuery()) {
    return new StreamExecution();
  } else if (method.isProcedureQuery()) {
    return new ProcedureExecution();
  } else if (method.isCollectionQuery()) {
    return new CollectionExecution();
  } else if (method.isSliceQuery()) {
    return new SlicedExecution(method.getParameters());
  } else if (method.isPageQuery()) {
    return new PagedExecution(method.getParameters());
  } else if (method.isModifyingQuery()) {
    return method.getClearAutomatically() ? new ModifyingExecution(method, em) : new ModifyingExecution(method, null);
  } else {
    return new SingleEntityExecution();
  }
}

如上述代碼所示,根據method變量實例化時的查詢設置方式,實例化不一樣的JpaQueryExecution子類實例去運行。咱們的findByUserName最終落入了SingleEntityExecution —— 返回單個實例的 Execution。繼續跟蹤execute方法,發現底層使用了 hibernate 的 CriteriaQueryImpl 完成了sql的拼裝,這裏就不作贅述了。

再來看看這類的method。在 spring-data-jpa 中,JpaQueryMethod就是Repository接口中帶有@Query註解方法的所有信息,包括註解,類名,實參等的存儲類,因此Repository接口有多少個@Query註解方法,就會包含多少個JpaQueryMethod實例被加入監聽序列。實際運行時,一個RepositoryQuery實例持有一個JpaQueryMethod實例,JpaQueryMethod又持有一個Method實例。

再來看看RepositoryQuery,在QueryExecutorMethodInterceptor中維護了一個Map<Method, RepositoryQuery> queriesRepositoryQuery的直接抽象子類是AbstractJpaQuery,能夠看到,一個RepositoryQuery實例持有一個JpaQueryMethod實例,JpaQueryMethod又持有一個Method實例,因此RepositoryQuery實例的用途很明顯,一個RepositoryQuery表明了Repository接口中的一個方法,根據方法頭上註解不一樣的形態,將每一個Repository接口中的方法分別映射成相對應的RepositoryQuery實例。

下面咱們就來看看spring-data-jpa對RepositoryQuery實例的具體分類:
1.SimpleJpaQuery
方法頭上@Query註解的nativeQuery屬性缺省值爲false,也就是使用JPQL,此時會建立SimpleJpaQuery實例,並經過兩個StringQuery類實例分別持有query jpql語句和根據query jpql計算拼接出來的countQuery jpql語句;

2.NativeJpaQuery
方法頭上@Query註解的nativeQuery屬性若是顯式的設置爲nativeQuery=true,也就是使用原生SQL,此時就會建立NativeJpaQuery實例;

3.PartTreeJpaQuery
方法頭上未進行@Query註解,將使用spring-data-jpa首創的方法名識別的方式進行sql語句拼接,此時在spring-data-jpa內部就會建立一個PartTreeJpaQuery實例;

4.NamedQuery
使用javax.persistence.NamedQuery註解訪問數據庫的形式,此時在spring-data-jpa內部就會根據此註解選擇建立一個NamedQuery實例;

5.StoredProcedureJpaQuery
顧名思義,在Repository接口的方法頭上使用org.springframework.data.jpa.repository.query.Procedure註解,也就是調用存儲過程的方式訪問數據庫,此時在spring-data-jpa內部就會根據@Procedure註解而選擇建立一個StoredProcedureJpaQuery實例。

那麼問題來了,sql 拼接的時候怎麼知道是根據userName進行查詢呢?是取自方法名中的 byUsername 仍是方法參數 userName 呢? spring 具體是在何時知道查詢參數的呢 ?

數據如何注入

spring 在啓動的時候會實例化一個 Repositories,它會去掃描全部的 class,而後找出由咱們定義的、繼承自org.springframework.data.repository.Repositor的接口,而後遍歷這些接口,針對每一個接口依次建立以下幾個實例:

  1. SimpleJpaRespositry —— 用來進行默認的 DAO 操做,是全部 Repository 的默認實現
  2. JpaRepositoryFactoryBean —— 裝配 bean,裝載了動態代理 Proxy,會以對應的 DAO 的 beanName 爲 key 註冊到DefaultListableBeanFactory中,在須要被注入的時候從這個 bean 中取出對應的動態代理 Proxy 注入給 DAO
  3. JdkDynamicAopProxy —— 動態代理對應的InvocationHandler,負責攔截 DAO 接口的全部的方法調用,而後作相應處理,好比findByUsername被調用的時候會先通過這個類的 invoke 方法

JpaRepositoryFactoryBean.getRepository()方法被調用的過程當中,仍是在實例化QueryExecutorMethodInterceptor這個攔截器的時候,spring 會去爲咱們的方法建立一個PartTreeJpaQuery,在它的構造方法中同時會實例化一個PartTree對象。PartTree定義了一系列的正則表達式,所有用於截取方法名,經過方法名來分解查詢的條件,排序方式,查詢結果等等,這個分解的步驟是在進程啓動時加載 Bean 的過程當中進行的,當執行查詢的時候直接取方法對應的PartTree用來進行 sql 的拼裝,而後進行 DB 的查詢,返回結果。

到此爲止,咱們整個JpaRepository接口相關的鏈路就算走通啦,簡單的總結以下:
spring 會在啓動的時候掃描全部繼承自 Repository 接口的 DAO 接口,而後爲其實例化一個動態代理,同時根據它的方法名、參數等爲其裝配一系列DB操做組件,在須要注入的時候爲對應的接口注入這個動態代理,在 DAO 方法被調用的時會走這個動態代理,而後通過一系列的方法攔截路由到最終的 DB 操做執行器JpaQueryExecution,而後拼裝 sql,執行相關操做,返回結果。

應用

基本查詢

基本查詢分爲兩種,一種是 spring data 默認已經實現(只要繼承JpaRepository),一種是根據查詢的方法來自動解析成 SQL。

預先生成

public interface UserRepository extends JpaRepository<User, Long> {
}

@Test
public void testBaseQuery() throws Exception {
    User user=new User();
    userRepository.findAll();
    userRepository.findOne(1l);
    userRepository.save(user);
    userRepository.delete(user);
    userRepository.count();
    userRepository.exists(1l);
    // ...
}

自定義簡單查詢

自定義的簡單查詢就是根據方法名來自動生成SQL,主要的語法是findXXBy,readAXXBy,queryXXBy,countXXBy, getXXBy後面跟屬性名稱,舉幾個例子:

User findByUserName(String userName);

User findByUserNameOrEmail(String username, String email);

Long deleteById(Long id);

Long countByUserName(String userName);

List<User> findByEmailLike(String email);

User findByUserNameIgnoreCase(String userName);

List<User> findByUserNameOrderByEmailDesc(String email);

具體的關鍵字,使用方法和生產成 SQL 以下表所示

Keyword Sample JPQL snippet
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age ⇐ ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<age> ages)</age> … where x.age in ?1
NotIn findByAgeNotIn(Collection<age> age)</age> … where x.age not in ?1
TRUE findByActiveTrue() … where x.active = true
FALSE findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

複雜查詢

在實際的開發中咱們須要用到分頁、刪選、連表等查詢的時候就須要特殊的方法或者自定義 SQL

分頁查詢

分頁查詢在實際使用中很是廣泛了,spring data jpa已經幫咱們實現了分頁的功能,在查詢的方法中,須要傳入參數Pageable
,當查詢中有多個參數的時候Pageable建議作爲最後一個參數傳入。Pageable是 spring 封裝的分頁實現類,使用的時候須要傳入頁數、每頁條數和排序規則

Page<User> findALL(Pageable pageable);

Page<User> findByUserName(String userName,Pageable pageable);
@Test
public void testPageQuery() throws Exception {
    int page=1,size=10;
    Sort sort = new Sort(Direction.DESC, "id");
    Pageable pageable = new PageRequest(page, size, sort);
    userRepository.findALL(pageable);
    userRepository.findByUserName("testName", pageable);
}

有時候咱們只須要查詢前N個元素,或者支取前一個實體。

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

自定義SQL查詢

其實 Spring data 大部分的 SQL 均可以根據方法名定義的方式來實現,可是因爲某些緣由咱們想使用自定義的 SQL 來查詢,spring data 也是完美支持的;在 SQL 的查詢方法上面使用 @Query 註解,如涉及到刪除和修改在須要加上 @Modifying 。也能夠根據須要添加 @Transactional 對事物的支持,查詢超時的設置等

@Modifying
@Query("update User u set u.userName = ?1 where c.id = ?2")
int modifyByIdAndUserId(String  userName, Long id);

@Transactional
@Modifying
@Query("delete from User where id = ?1")
void deleteByUserId(Long id);

@Transactional(timeout = 10)
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);

多表查詢

多表查詢在 spring data jpa 中有兩種實現方式,第一種是利用 hibernate 的級聯查詢來實現,第二種是建立一個結果集的接口來接收連表查詢後的結果,這裏介紹第二種方式。

首先須要定義一個結果集的接口類。

public interface HotelSummary {

    City getCity();

    String getName();

    Double getAverageRating();

    default Integer getAverageRatingRounded() {
        return getAverageRating() == null ? null : (int) Math.round(getAverageRating());
    }

}

查詢的方法返回類型設置爲新建立的接口

@Query("select h.city as city, h.name as name, avg(r.rating) as averageRating from Hotel h left outer join h.reviews r where h.city = ?1 group by h")
Page<HotelSummary> findByCity(City city, Pageable pageable);

@Query("select h.name as name, avg(r.rating) as averageRating from Hotel h left outer join h.reviews r group by h")
Page<HotelSummary> findByCity(Pageable pageable);
Page<HotelSummary> hotels = this.hotelRepository.findByCity(new PageRequest(0, 10, Direction.ASC, "name"));
for(HotelSummary summay:hotels){
    System.out.println("Name" +summay.getName());
}

在運行中 Spring 會給接口(HotelSummary)自動生產一個代理類來接收返回的結果,代碼會使用 getXX 的形式來獲取

和 mybatis 的比較

spring data jpa 底層採用 hibernate 作爲 ORM 框架,因此 spring data jpa 和 mybatis 的比較其實就是 hibernate 和 mybatis 的比較。下面從幾個方面來對比下二者

基本概念

從基本概念和框架目標上看,兩個框架差異仍是很大的。hibernate 是一個自動化更強、更高級的框架,畢竟在java代碼層面上,省去了絕大部分 sql 編寫,取而代之的是用面向對象的方式操做關係型數據庫的數據。而 MyBatis 則是一個可以靈活編寫 sql 語句,並將 sql 的入參和查詢結果映射成 POJOs 的一個持久層框架。因此,從表面上看,hibernate 能方便、自動化更強,而 MyBatis 在 Sql 語句編寫方面則更靈活自由。

性能

正如上面介紹的, Hibernate 比 MyBatis 抽象封裝的程度更高,理論上單個語句之心的性能會低一點(全部的框架都是同樣,排除算法上的差別,越是底層,執行效率越高)。

但 Hibernate 會設置緩存,對於重複查詢有必定的優化,並且從編碼效率來講,Hibernate 的編碼效果確定是會高一點的。因此,從總體的角度來看性能的話,其實二者不能徹底說誰勝誰劣。

ORM

Hibernate 是完備的 ORM 框架,是符合 JPA 規範的, MyBatis 沒有按照JPA那套規範實現。目前 Spring 以及 Spring Boot 官方都沒有針對 MyBatis 有具體的支持,但對 Hibernate 的集成一直是有的。但這並非說 mybatis 和 spring 沒法集成,MyBatis 官方社區自身也是有 對 Spring,Spring boot 集成作支持的,因此在技術上,二者都不存在問題。

總結

總結下 mybatis 的優勢:

  • 簡單易學
  • 靈活,MyBatis不會對應用程序或者數據庫的現有設計強加任何影響。 註解或者使用 SQL 寫在 XML 裏,便於統一管理和優化。經過 SQL 基本上能夠實現咱們不使用數據訪問框架能夠實現的全部功能,或許更多。
  • 解除 SQL 與程序代碼的耦合,SQL 和代碼的分離,提升了可維護性。
  • 提供映射標籤,支持對象與數據庫的 ORM 字段關係映射。
  • 提供對象關係映射標籤,支持對象關係組建維護。
  • 提供XML標籤,支持編寫動態SQL。

hibernate 的優勢:
JPA 的宗旨是爲 POJO 提供持久化標準規範,實現使用的 Hibernate,Hibernate 是一個全自動的持久層框架,而且提供了面向對象的 SQL 支持,不須要編寫複雜的 SQL 語句,直接操做 Java 對象便可,從而大大下降了代碼量,讓即便不懂 SQL 的開發人員,也使程序員更加專一於業務邏輯的實現。對於關聯查詢,也僅僅是使用一些註解便可完成一些複雜的 SQL功能。

最後再作一個簡單的總結:

  • 若是能有很好的數據庫規範的話,使用這兩個哪一個都不會差
  • 若是有能力而且特別想掌控 SQL,那就選 MyBaits,不然就依賴 JPA 的魔力來快速完成業務開發
  • 我的認爲二者最本質的不一樣點,hibernate 的理念是面向對象,mybatis 的理念是面向過程,相似於 JAVA 和 PYTHON。固然,用hibernate也能夠寫出面向關係代碼和系統,但卻得不到面向關係的各類好處,最大的即是編寫 sql 的靈活性,同時也失去面向對象意義和好處——一句話,不三不四。那麼,面向對象和關係型模型有什麼不一樣,體如今哪裏呢?實際上二者要面對的領域和要解決的問題是根本不一樣的:面向對象致力於解決計算機邏輯問題,而關係模型致力於解決數據的高效存取問題。咱們不妨對比一下面向對象的概念原則和關係型數據庫的不一樣之處:面向對象考慮的是對象的整個生命週期包括在對象的建立、持久化、狀態的改變和行爲等,對象的持久化只是對象的一種狀態,而面向關係型數據庫的概念則更關注數據的高效存儲和讀取;面向對象更強調對象狀態的封裝性,對象封裝本身的狀態(或數據)不容許外部對象隨意修改,只暴露一些合法的行爲方法供外部對象調用;而關係型數據庫則是開放的,能夠供用戶隨意讀取和修改關係,並能夠和其餘表任意的關聯(只要sql正確容許的狀況下);面向對象試圖爲動態的世界建模,他要描述的是世界的過程和規律,進而適應發展和變化,面向對象老是在變化中處理各類各樣的變化。而關係型模型爲靜態世界建模,它經過數據快照記錄了世界在某一時候的狀態,它是靜態的。從上面二者基本概念和思想的對比來看,能夠得出結論hibernate和MyBatis兩個框架的側重點徹底不一樣。因此咱們就兩個框架選擇上,就須要根據不一樣的項目需求選擇不一樣的框架。在框架的使用中,也要考慮考慮框架的優點和劣勢,揚長避短,發揮出框架的最大效用,才能真正的提升項目研發效率、完成項目的目標。但相反,若是使用Spring Data JPA和hibernate等ORM的框架而沒有以面向對象思想和方法去分析和設計系統,而是抱怨框架不能靈活操做sql查詢數據,那就是想讓狗去幫你拿耗子了。

參考

相關文章
相關標籤/搜索