本文將從示例、原理、應用3個方面介紹spring data jpa。java
如下分析基於spring boot 2.0 + spring 5.0.4
版本源碼mysql
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 基於 ORM 框架、JPA 規範的基礎上封裝的一套 JPA 應用框架,底層使用了 Hibernate 的 JPA 技術實現,可以使開發者用極簡的代碼便可實現對數據的訪問和操做。它提供了包括增刪改查等在內的經常使用功能,且易於擴展!學習並使用 Spring Data JPA 能夠極大提升開發效率!程序員
spring data jpa 讓咱們解脫了 DAO 層的操做,基本上全部 CRUD 均可以依賴於它來實現
<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>
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
有幾種配置:算法
不配置此項,表示禁用自動建表功能spring
創建 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
繼續往下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 注入給 DAOJdkDynamicAopProxy
—— 動態代理對應的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);
其實 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 的形式來獲取
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 的編碼效果確定是會高一點的。因此,從總體的角度來看性能的話,其實二者不能徹底說誰勝誰劣。
Hibernate 是完備的 ORM 框架,是符合 JPA 規範的, MyBatis 沒有按照JPA那套規範實現。目前 Spring 以及 Spring Boot 官方都沒有針對 MyBatis 有具體的支持,但對 Hibernate 的集成一直是有的。但這並非說 mybatis 和 spring 沒法集成,MyBatis 官方社區自身也是有 對 Spring,Spring boot 集成作支持的,因此在技術上,二者都不存在問題。
總結下 mybatis 的優勢:
hibernate 的優勢:
JPA 的宗旨是爲 POJO 提供持久化標準規範,實現使用的 Hibernate,Hibernate 是一個全自動的持久層框架,而且提供了面向對象的 SQL 支持,不須要編寫複雜的 SQL 語句,直接操做 Java 對象便可,從而大大下降了代碼量,讓即便不懂 SQL 的開發人員,也使程序員更加專一於業務邏輯的實現。對於關聯查詢,也僅僅是使用一些註解便可完成一些複雜的 SQL功能。
最後再作一個簡單的總結: