前言
本文將從示例、原理、應用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
<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 { <span class="hljs-meta">@Id</span>
<span class="hljs-meta">@GeneratedValue</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">long</span> id;
<span class="hljs-meta">@Column</span>(nullable = <span class="hljs-literal">false</span>, unique = <span class="hljs-literal">true</span>)
<span class="hljs-keyword">private</span> String userName;
<span class="hljs-meta">@Column</span>(nullable = <span class="hljs-literal">false</span>)
<span class="hljs-keyword">private</span> String password;
<span class="hljs-meta">@Column</span>(nullable = <span class="hljs-literal">false</span>)
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> age;
}
聲明 UserRepository
接口,繼承JpaRepository
,默認支持簡單的 CRUD 操作,非常方便
public interface UserRepository extends JpaRepository<User, Long> { <span class="hljs-function">User <span class="hljs-title">findByUserName</span><span class="hljs-params">(String userName)</span></span>;
}
單測
@Slf4j
public class UserTest extends ApplicationTests { <span class="hljs-meta">@Autowired</span>
<span class="hljs-keyword">private</span> <span class="hljs-type">UserRepository</span> userRepository;
<span class="hljs-meta">@Test</span>
<span class="hljs-meta">@Transactional</span>
public void userTest() {
<span class="hljs-type">User</span> user = <span class="hljs-keyword">new</span> <span class="hljs-type">User</span>();
user.setUserName(<span class="hljs-string">"wyk"</span>);
user.setAge(<span class="hljs-number">30</span>);
user.setPassword(<span class="hljs-string">"aaabbb"</span>);
userRepository.save(user);
<span class="hljs-type">User</span> item = userRepository.findByUserName(<span class="hljs-string">"wyk"</span>);
log.info(<span class="hljs-type">JsonUtils</span>.toJson(item));
}
}
這里標記@Transactional
,開啟事務功能是為了單元測試的時候不造成垃圾數據
源代碼下載: 請戳這里
原理
很多人會有疑問,直接聲明接口不需要具體實現就能完成數據庫的操作?下面就簡單介紹下 spring data jpa 的實現原理。
如何工作
對單測進行debug,可以發現userRepository
被注入了一個動態代理,被代理的類是JpaRepository
的一個實現SimpleJpaRespositry
繼續往下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> queries
。RepositoryQuery
的直接抽象子類是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
的接口,然后遍歷這些接口,針對每個接口依次創建如下幾個實例:
-
SimpleJpaRespositry
—— 用來進行默認的 DAO 操作,是所有 Repository 的默認實現 -
JpaRepositoryFactoryBean
—— 裝配 bean,裝載了動態代理 Proxy,會以對應的 DAO 的 beanName 為 key 注冊到DefaultListableBeanFactory
中,在需要被注入的時候從這個 bean 中取出對應的動態代理 Proxy 注入給 DAO -
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 { <span class="hljs-function">City <span class="hljs-title">getCity</span><span class="hljs-params">()</span></span>;
<span class="hljs-function">String <span class="hljs-title">getName</span><span class="hljs-params">()</span></span>;
<span class="hljs-function">Double <span class="hljs-title">getAverageRating</span><span class="hljs-params">()</span></span>;
<span class="hljs-function"><span class="hljs-keyword">default</span> Integer <span class="hljs-title">getAverageRatingRounded</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> getAverageRating() == <span class="hljs-keyword">null</span> ? <span class="hljs-keyword">null</span> : (<span class="hljs-keyword">int</span>) 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查詢數據,那就是想讓狗去幫你拿耗子了。
參考
</div>
Powered by .NET Core on Kubernetes