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

前言

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

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

概述

JPA是什麼?

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

注意: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修改表結構,但不會創建新表。

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

Repository

建立 entity

@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

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jHUfDS2z-1576947961520)(/img/bVbaRnz)]

繼續往下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 ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection 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查詢數據,那就是想讓狗去幫你拿耗子了。

參考

  • https://juejin.im/post/595da437f265da6c4f34d2d8

  • http://www.luckyzz.com/java/spring-data-jpa/

  • https://www.jianshu.com/p/3927c2b6acc0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章