欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringData JPA基本/高級/多數(shù)據(jù)源的使用詳解

 更新時間:2022年02月24日 11:04:41   作者:純潔的微笑  
這篇文章主要介紹了SpringData JPA基本/高級/多數(shù)據(jù)源的使用詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教

一、Spring Data JPA基本用法

Spring Data JPA 是 Spring Boot 體系中約定優(yōu)于配置的最佳實(shí)現(xiàn),大大簡化了項(xiàng)目中數(shù)據(jù)庫的操作

1、概念

JPA由來

ORM 框架能夠?qū)?Java 對象映射到關(guān)系數(shù)據(jù)庫中,能夠直接持久化復(fù)雜的 Java 對象。ORM 框架的出現(xiàn),可以讓開發(fā)者從數(shù)據(jù)庫編程中解脫出來,把更多的精力放在了業(yè)務(wù)模型與業(yè)務(wù)邏輯上。目前比較流行的 ORM 框架有 Hibernate、MyBatis、TopLink、Spring JDBC 等。

在 JPA 規(guī)范之前,由于沒有官方的標(biāo)準(zhǔn),使得各 ORM 框架之間的 API 差別很大,使用了某種 ORM 框架的系統(tǒng)會嚴(yán)重受制于該 ORM 的標(biāo)準(zhǔn)?;诖?,Sun 引入新的 JPA ORM,主要的原因有:其一,簡化現(xiàn)有 Java EE 和 Java SE 應(yīng)用開發(fā)工作;其二,Sun 希望整合 ORM 技術(shù),實(shí)現(xiàn)統(tǒng)一的 API 調(diào)用接口。

JPA是什么

JPA(Java Persistence API)是 Sun 官方提出的 Java 持久化規(guī)范。它為 Java 開發(fā)人員提供了一種對象 / 關(guān)聯(lián)映射工具來管理 Java 應(yīng)用中的關(guān)系數(shù)據(jù)。它的出現(xiàn)主要是為了簡化現(xiàn)有的持久化開發(fā)工作和整合 ORM 技術(shù),結(jié)束現(xiàn)在 Hibernate、TopLink、JDO 等 ORM 框架各自為營的局面。

注意:JPA 是一套規(guī)范,不是一套產(chǎn)品,那么像 Hibernate、TopLink、JDO 它們是一套產(chǎn)品,如果說這些產(chǎn)品實(shí)現(xiàn)了這個 JPA 規(guī)范,那么我們就可以稱他們?yōu)?JPA 的實(shí)現(xiàn)產(chǎn)品。

Spring Data JPA

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 規(guī)范的基礎(chǔ)上封裝的一套 JPA 應(yīng)用框架,可以讓開發(fā)者用極簡的代碼即可實(shí)現(xiàn)對數(shù)據(jù)的訪問和操作。它提供了包括增、刪、改、查等在內(nèi)的常用功能,且易于擴(kuò)展,學(xué)習(xí)并使用 Spring Data JPA 可以極大提高開發(fā)效率。Spring Data JPA 其實(shí)就是 Spring 基于 Hibernate 之上構(gòu)建的 JPA 使用解決方案,方便在 Spring Boot 項(xiàng)目中使用 JPA 技術(shù)。

Spring Data JPA 讓我們解脫了 DAO 層的操作,基本上所有 CRUD 都可以依賴于它實(shí)現(xiàn)。

2、快速上手

1.添加依賴

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

2.添加配置文件

spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
#SQL 輸出
spring.jpa.show-sql=true
#format 一下 SQL 進(jìn)行輸出
spring.jpa.properties.hibernate.format_sql=true

hibernate.hbm2ddl.auto 參數(shù)的作用主要用于:自動創(chuàng)建、更新、驗(yàn)證數(shù)據(jù)庫表結(jié)構(gòu),有四個值。

  • create:每次加載 Hibernate 時都會刪除上一次生成的表,然后根據(jù) model 類再重新來生成新表,哪怕兩次沒有任何改變也要這樣執(zhí)行,這就是導(dǎo)致數(shù)據(jù)庫表數(shù)據(jù)丟失的一個重要原因。
  • create-drop:每次加載 Hibernate 時根據(jù) model 類生成表,但是 sessionFactory 一關(guān)閉,表就自動刪除。
  • update:最常用的屬性,第一次加載 Hibernate 時根據(jù) model 類會自動建立起表的結(jié)構(gòu)(前提是先建立好數(shù)據(jù)庫),以后加載 Hibernate 時根據(jù) model 類自動更新表結(jié)構(gòu),即使表結(jié)構(gòu)改變了,但表中的行仍然存在,不會刪除以前的行。要注意的是當(dāng)部署到服務(wù)器后,表結(jié)構(gòu)是不會被馬上建立起來的,是要等應(yīng)用第一次運(yùn)行起來后才會。
  • validate:每次加載 Hibernate 時,驗(yàn)證創(chuàng)建數(shù)據(jù)庫表結(jié)構(gòu),只會和數(shù)據(jù)庫中的表進(jìn)行比較,不會創(chuàng)建新表,但是會插入新值。

其他

  • dialect 主要是指定生成表名的存儲引擎為 InnoDB
  • show-sql 是否在日志中打印出自動生成的 SQL,方便調(diào)試的時候查看

3.實(shí)體類

@Entity
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, unique = true)
? ? private String email;
? ? @Column(nullable = true, unique = true)
? ? private String nickName;
? ? @Column(nullable = false)
? ? private String regTime;
? ? //省略 getter settet 方法、構(gòu)造方法
}
  • @Entity(name="EntityName") 必須,用來標(biāo)注一個數(shù)據(jù)庫對應(yīng)的實(shí)體,數(shù)據(jù)庫中創(chuàng)建的表名默認(rèn)和類名一致。其中,name 為可選,對應(yīng)數(shù)據(jù)庫中一個表,使用此注解標(biāo)記 Pojo 是一個 JPA 實(shí)體。
  • @Table(name="",catalog="",schema="") 可選,用來標(biāo)注一個數(shù)據(jù)庫對應(yīng)的實(shí)體,數(shù)據(jù)庫中創(chuàng)建的表名默認(rèn)和類名一致。通常和@Entity配合使用,只能標(biāo)注在實(shí)體的 class 定義處,表示實(shí)體對應(yīng)的數(shù)據(jù)庫表的信息。
  • @Id必須,@Id定義了映射到數(shù)據(jù)庫表的主鍵的屬性,一個實(shí)體只能有一個屬性被映射為主鍵。
  • @GeneratedValue(strategy=GenerationType,generator="") 可選,strategy: 表示主鍵生成策略,有 AUTO、INDENTITY、SEQUENCE 和 TABLE 4 種,分別表示讓 ORM 框架自動選擇,generator: 表示主鍵生成器的名稱。
  • @Column(name = "user_code", nullable = false, length=32) 可選,@Column 描述了數(shù)據(jù)庫表中該字段的詳細(xì)定義,這對于根據(jù) JPA 注解生成數(shù)據(jù)庫表結(jié)構(gòu)的工具。name: 表示數(shù)據(jù)庫表中該字段的名稱,默認(rèn)情形屬性名稱一致;nullable: 表示該字段是否允許為 null,默認(rèn)為 true;unique: 表示該字段是否是唯一標(biāo)識,默認(rèn)為 false;length: 表示該字段的大小,僅對 String 類型的字段有效。
  • @Transient可選,@Transient 表示該屬性并非一個到數(shù)據(jù)庫表的字段的映射,ORM 框架將忽略該屬性。
  • @Enumerated 可選,使用枚舉的時候,我們希望數(shù)據(jù)庫中存儲的是枚舉對應(yīng)的 String 類型,而不是枚舉的索引值,需要在屬性上面添加 @Enumerated(EnumType.STRING) 注解。

4.Repository構(gòu)建

創(chuàng)建的 Repository 只要繼承 JpaRepository 即可,就會幫我們自動生成很多內(nèi)置方法。另外還有一個功能非常實(shí)用,可以根據(jù)方法名自動生產(chǎn) SQL,比如 findByUserName 會自動生產(chǎn)一個以 userName 為參數(shù)的查詢方法,比如 findAll 會自動查詢表里面的所有數(shù)據(jù)等。

public interface UserRepository extends JpaRepository<User,Long> {
? ? User findByUserName(String userName);
? ? User findByUserNameOrEmail(String username,String email);
}

我們只需要在對應(yīng)的 Repository 中創(chuàng)建好方法,使用的時候直接將接口注入到類中調(diào)用即可。在 IDEA 中打開類 UserRepository,在這個類的大括號內(nèi)的區(qū)域右鍵單擊,選擇 Diagrams | Show Diagram 選項(xiàng),即可打開類圖,如下:

通過上圖我們發(fā)現(xiàn) JpaRepository 繼承 PagingAndSortingRepository 和 QueryByExampleExecutor,PagingAndSortingRepository 類主要負(fù)責(zé)排序和分頁內(nèi)容,QueryByExampleExecutor 提供了很多示例的查詢方法,如下:

public interface QueryByExampleExecutor<T> {?
? ? <S extends T> S findOne(Example<S> example); ? //根據(jù)“實(shí)例”查找一個對象
? ? <S extends T> Iterable<S> findAll(Example<S> example); ? ? //根據(jù)“實(shí)例”查找一批對象
? ? <S extends T> Iterable<S> findAll(Example<S> example,Sort sort); ?//根據(jù)“實(shí)例”查找一批對象,且排序
? ? <S extends T> Page<S> findAll(Example<S> example,Pageable pageable); ?//根據(jù)“實(shí)例”查找一批對象,且排序和分頁
? ? <S extends T> long count(Example<S> example); ?//根據(jù)“實(shí)例”查找,返回符合條件的對象個數(shù)
? ? <S extends T> boolean exists(Example<S> example); ?//根據(jù)“實(shí)例”判斷是否有符合條件的對象
}

因此,繼承 JpaRepository 的會自動擁有上述這些方法和排序、分頁功能。查看源碼我們發(fā)現(xiàn) PagingAndSortingRepository 又繼承了 CrudRepository。CrudRepository 的源碼如下:

@NoRepositoryBean
public interface CrudRepository<T,ID> extends Repository<T,ID> {
? ? <S extends T> S save(S entity);
? ? <S extends T> Iterable<S> saveAll(Iterable<S> entities);
? ? Optional<T> findById(ID id);
? ? boolean existsById(ID id);
? ? Iterable<T> findAll();
? ? Iterable<T> findAllById(Iterable<ID> ids);
? ? long count();
? ? void deleteById(ID id);
? ? void delete(T entity);
? ? void deleteAll(Iterable<? extends T> entities);
? ? void deleteAll();
}

從 CrudRepository 的源碼可以看出 CrudRepository 內(nèi)置了我們最常用的增、刪、改、查的方法,方便我們?nèi)ナ褂?,因?yàn)?JpaRepository 繼承了 PagingAndSortingRepository,PagingAndSortingRepository 繼承了 CrudRepository,所以繼承 JpaRepository 的類也默認(rèn)擁有了上述方法。

因此使用 JPA 操作數(shù)據(jù)庫時,只需要構(gòu)建的 Repository 繼承了 JpaRepository,就會擁有了很多常用的數(shù)據(jù)庫操作方法。

5.測試

創(chuàng)建好 UserRepository 之后,當(dāng)業(yè)務(wù)代碼中需要使用時直接將此接口注入到對應(yīng)的類中,在 Spring Boot 啟動時,會自動根據(jù)注解內(nèi)容創(chuàng)建實(shí)現(xiàn)類并注入到目標(biāo)類中。

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTests {
? ? @Resource
? ? private UserRepository userRepository;
? ? @Test
? ? public void test() ?{
? ? ? ? Date date = new Date();
? ? ? ? DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG,DateFormat.LONG); ? ? ? ?
? ? ? ? String formattedDate = dateFormat.format(date);
? ? ? ? userRepository.save(new User("aa","aa@126.com","aa","aa123456",formattedDate));
? ? ? ? userRepository.save(new User("bb","bb@126.com","bb","bb123456",formattedDate));
? ? ? ? userRepository.save(new User("cc","cc@126.com","cc","cc123456",formattedDate));
? ? ? ? Assert.assertEquals(9,userRepository.findAll().size());
? ? ? ? Assert.assertEquals("bb",userRepository.findByUserNameOrEmail("bb","cc@126.com").getNickName());
? ? ? ? userRepository.delete(userRepository.findByUserName("aa1"));
? ? }
}

6.基本查詢

我們可以將 Spring Data JPA 查詢分為兩種,一種是 Spring Data JPA 默認(rèn)實(shí)現(xiàn)的,另一種是需要根據(jù)查詢的情況來自行構(gòu)建。

預(yù)生成方法

預(yù)生成方法就是我們上面看到的那些方法,因?yàn)槔^承了 JpaRepository 而擁有了父類的這些內(nèi)容。

(1)繼承 JpaRepository

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

(2)使用默認(rèn)方法

//所有父類擁有的方法都可以直接調(diào)用,根據(jù)方法名也可以看出它的含義。
@Test
public void testBaseQuery() {
userRepository.findAll();
userRepository.findById(1l);
userRepository.save(user);
userRepository.delete(user);
userRepository.count();
userRepository.existsById(1l);
// ...
?? ?}

7.自定義查詢

Spring Data JPA 可以根據(jù)接口方法名來實(shí)現(xiàn)數(shù)據(jù)庫操作,主要的語法是 findXXBy、readAXXBy、queryXXBy、countXXBy、getXXBy 后面跟屬性名稱,利用這個功能僅需要在定義的 Repository 中添加對應(yīng)的方法名即可,使用時 Spring Boot 會自動幫我們實(shí)現(xiàn),示例如下。

根據(jù)用戶名查詢用戶:

User findByUserName(String userName);

也可以加一些關(guān)鍵字 And、or:

User findByUserNameOrEmail(String username,String email);

修改、刪除、統(tǒng)計也是類似語法:

Long deleteById(Long id);
Long countByUserName(String userName)

基本上 SQL 體系中的關(guān)鍵詞都可以使用,如 Like 、IgnoreCase、OrderBy:

List<User> findByEmailLike(String email);
User findByUserNameIgnoreCase(String userName);
List<User> findByUserNameOrderByEmailDesc(String email);

可以根據(jù)查詢的條件不斷地添加和拼接,Spring Boot 都可以正確解析和執(zhí)行,其他使用示例可以參考下表。

KeywordSampleJPQL snippet
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
Is,EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1(parameter bound with appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1(parameter bound with prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1(parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> age)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?1)
CountcountByFirstNameselect count(*) from ... where x.firstName = ?1
ExistsexistsByFirstNamelike the dao.exists(Example),judge by attribution of firstName:select keyindex0_.id as col_0_0_ from key_index keyindex0_ where keyindex0_.name=? limit ?

3、小結(jié)

第一部分講解了使用 JPA 大大解放了我們對數(shù)據(jù)庫的操作,經(jīng)常使用的 SQL 大部分都已經(jīng)被預(yù)生成,直接使用即可。另外 JPA 還有一個特點(diǎn),那就是再也不用關(guān)心數(shù)據(jù)庫的表結(jié)構(gòu)了,需要更改的時候只需要修改對應(yīng) Model 的屬性即可。在微服務(wù)架構(gòu)中,因?yàn)榉?wù)拆分得越來越小,微服務(wù)內(nèi)部只關(guān)心自己的業(yè)務(wù),需要復(fù)雜查詢的場景會越來越少,在微服務(wù)架構(gòu)中更推薦使用 JPA 技術(shù)。

二、Spring Data JPA高級用法

第一部分介紹了 Spring Data JPA 的使用方式和基本查詢,常用的增、刪、改、查需求 Spring Data JPA 已經(jīng)實(shí)現(xiàn)了。但對于復(fù)雜的數(shù)據(jù)庫場景,動態(tài)生成方法不能滿足,對此 Spring Data JPA 提供了其他的解決方案。

1、自定義 SQL 查詢

使用 Spring Data 大部分的 SQL 都可以根據(jù)方法名定義的方式來實(shí)現(xiàn),但是由于某些原因必須使用自定義的 SQL 來查詢,Spring Data 也可以完美支持。

在 SQL 的查詢方法上面使用 @Query 注解,在注解內(nèi)寫 Hql 來查詢內(nèi)容。

@Query("select u from User u")
Page<User> findALL(Pageable pageable);

當(dāng)然如果感覺使用原生 SQL 更習(xí)慣,它也是支持的,需要再添加一個參數(shù) nativeQuery = true。

@Query("select * from user u where u.nick_name = ?1", nativeQuery = true)
Page<User> findByNickName(String nickName, Pageable pageable);

@Query 上面的 1 代表的是方法參數(shù)里面的順序,如果有多個參數(shù)也可以按照這個方式添加 1、2、3....。除了按照這種方式傳參外,還可以使用 @Param 來支持。

@Query("select u from User u where u.nickName = :nickName")
Page<User> findByNickName(@Param("nickName") String nickName, Pageable pageable);

如涉及到刪除和修改需要加上 @Modifying,也可以根據(jù)需要添加 @Transactional 對事務(wù)的支持、操作超時設(shè)置等。

@Transactional(timeout = 10)
@Modifying
@Query("update User set userName = ?1 where id = ?2")
int modifyById(String ?userName, Long id);
@Transactional
@Modifying
@Query("delete from User where id = ?1")
void deleteById(Long id);

使用已命名的查詢

除了使用 @Query 注解外,還可以預(yù)先定義好一些查詢,并為其命名,然后再 Repository 中添加相同命名的方法,定義命名的 Query:

@Entity
@NamedQueries({
? ? ? ? @NamedQuery(name = "User.findByPassWord", query = "select u from User u where u.passWord = ?1"),
? ? ? ? @NamedQuery(name = "User.findByNickName", query = "select u from User u where u.nickName = ?1"),
})
public class User {
? ? ……
}

通過 @NamedQueries 注解可以定義多個命名 Query,@NamedQuery 的 name 屬性定義了 Query 的名稱,注意加上 Entity 名稱 . 作為前綴,query 屬性定義查詢語句,定義對應(yīng)的方法:

List<User> findByPassWord(String passWord);
List<User> findByNickName(String nickName);

Query 查找策略

到此,我們有了三種方法來定義 Query:(1)通過方法名自動創(chuàng)建 Query,(2)通過 @Query 注解實(shí)現(xiàn)自定義 Query,(3)通過 @NamedQuery 注解來定義 Query。那么,Spring Data JPA 如何來查找這些 Query 呢?

通過配置 @EnableJpaRepositories 的 queryLookupStrategy 屬性來配置 Query 查找策略,有如下定義。

  • CREATE:嘗試從查詢方法名構(gòu)造特定于存儲的查詢。一般的方法是從方法名中刪除一組已知的前綴,并解析方法的其余部分。
  • USE_DECLARED_QUERY:嘗試查找已聲明的查詢,如果找不到,則拋出異常。查詢可以通過某個地方的注釋定義,也可以通過其他方式聲明。
  • CREATE_IF_NOT_FOUND(默認(rèn)):CREATE 和 USE_DECLARED_QUERY 的組合,它首先查找一個已聲明的查詢,如果沒有找到已聲明的查詢,它將創(chuàng)建一個自定義方法基于名稱的查詢。它允許通過方法名進(jìn)行快速查詢定義,還可以根據(jù)需要引入聲明的查詢來定制這些查詢調(diào)優(yōu)。

2、分頁查詢Spring Data JPA 已經(jīng)幫我們內(nèi)置了分頁功能

在查詢的方法中,需要傳入?yún)?shù) Pageable,當(dāng)查詢中有多個參數(shù)的時候 Pageable 建議作為最后一個參數(shù)傳入。

@Query("select u from User u")
Page<User> findALL(Pageable pageable);
Page<User> findByNickName(String nickName, Pageable pageable);

Pageable 是 Spring 封裝的分頁實(shí)現(xiàn)類,使用的時候需要傳入頁數(shù)、每頁條數(shù)和排序規(guī)則,Page 是 Spring 封裝的分頁對象,封裝了總頁數(shù)、分頁數(shù)據(jù)等。返回對象除使用 Page 外,還可以使用 Slice 作為返回值。

Slice<User> findByNickNameAndEmail(String nickName, String email,Pageable pageable);

Page 和 Slice 的區(qū)別如下:

Page 接口繼承自 Slice 接口,而 Slice 繼承自 Iterable 接口。

Page 接口擴(kuò)展了 Slice 接口,添加了獲取總頁數(shù)和元素總數(shù)量的方法,因此,返回 Page 接口時,必須執(zhí)行兩條 SQL,一條復(fù)雜查詢分頁數(shù)據(jù),另一條負(fù)責(zé)統(tǒng)計數(shù)據(jù)數(shù)量。

返回 Slice 結(jié)果時,查詢的 SQL 只會有查詢分頁數(shù)據(jù)這一條,不統(tǒng)計數(shù)據(jù)數(shù)量。

用途不一樣:Slice 不需要知道總頁數(shù)、總數(shù)據(jù)量,只需要知道是否有下一頁、上一頁,是否是首頁、尾頁等,比如前端滑動加載一頁可用;而 Page 知道總頁數(shù)、總數(shù)據(jù)量,可以用于展示具體的頁數(shù)信息,比如后臺分頁查詢。

@Test
public void testPageQuery() ?{
int page=1,size=2;
Sort sort = new Sort(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(page, size, sort);
userRepository.findALL(pageable);
userRepository.findByNickName("aa", pageable);
?? ?}
  • Sort,控制分頁數(shù)據(jù)的排序,可以選擇升序和降序。
  • PageRequest,控制分頁的輔助類,可以設(shè)置頁碼、每頁的數(shù)據(jù)條數(shù)、排序等。

限制查詢:有時候我們只需要查詢前 N 個元素,或者只取前一個實(shí)體。

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);

3、復(fù)雜查詢

我們可以通過 AND 或者 OR 等連接詞來不斷拼接屬性來構(gòu)建多條件查詢,但如果參數(shù)大于 6 個時,方法名就會變得非常的長,并且還不能解決動態(tài)多條件查詢的場景。到這里就需要給大家介紹另外一個利器 JpaSpecificationExecutor 了。

JpaSpecificationExecutor 是 JPA 2.0 提供的 Criteria API 的使用封裝,可以用于動態(tài)生成 Query 來滿足我們業(yè)務(wù)中的各種復(fù)雜場景。Spring Data JPA 為我們提供了 JpaSpecificationExecutor 接口,只要簡單實(shí)現(xiàn) toPredicate 方法就可以實(shí)現(xiàn)復(fù)雜的查詢。

我們來看一下 JpaSpecificationExecutor 的源碼:

public interface JpaSpecificationExecutor<T> {
? ?//根據(jù) Specification 條件查詢單個對象,注意的是,如果條件能查出來多個會報錯
? ?T findOne(@Nullable Specification<T> spec);
? ?//根據(jù) Specification 條件查詢 List 結(jié)果
? ?List<T> findAll(@Nullable Specification<T> spec);
? ?//根據(jù) Specification 條件,分頁查詢
? ?Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
? ?//根據(jù) Specification 條件,帶排序的查詢結(jié)果
? ?List<T> findAll(@Nullable Specification<T> spec, Sort sort);
? ?//根據(jù) Specification 條件,查詢數(shù)量
? ?long count(@Nullable Specification<T> spec);
}

JpaSpecificationExecutor 的源碼很簡單,根據(jù) Specification 的查詢條件返回 List、Page 或者 count 數(shù)據(jù)。在使用 JpaSpecificationExecutor 構(gòu)建復(fù)雜查詢場景之前,我們需要了解幾個概念:

  • Root root,代表了可以查詢和操作的實(shí)體對象的根,開一個通過 get("屬性名") 來獲取對應(yīng)的值。
  • CriteriaQuery query,代表一個 specific 的頂層查詢對象,它包含著查詢的各個部分,比如 select 、from、where、group by、order by 等。
  • CriteriaBuilder cb,來構(gòu)建 CritiaQuery 的構(gòu)建器對象,其實(shí)就相當(dāng)于條件或者是條件組合,并以 Predicate 的形式返回。

使用案例: 首先定義一個 UserDetail 對象,作為演示的數(shù)據(jù)模型。

@Entity
public class UserDetail {
? ? @Id
? ? @GeneratedValue
? ? private Long id;
? ? @Column(nullable = false, unique = true)
? ? private Long userId;
? ? private Integer age;
? ? private String realName;
? ? private String status;
? ? private String hobby;
? ? private String introduction;
? ? private String lastLoginIp;
}

創(chuàng)建 UserDetail 對應(yīng)的 Repository:

public interface UserDetailRepository extends JpaSpecificationExecutor<UserDetail>,JpaRepository<UserDetail, Long> ?{
}

定義一個查詢 Page<UserDetail> 的接口:

public interface UserDetailService {
? ? public Page<UserDetail> findByCondition(UserDetailParam detailParam, Pageable pageable);
}

在 UserDetailServiceImpl 中,我們來演示 JpaSpecificationExecutor 的具體使用。

@Service
public class UserDetailServiceImpl implements ?UserDetailService{
? ? @Resource
? ? private UserDetailRepository userDetailRepository;
? ? @Override
? ? public Page<UserDetail> findByCondition(UserDetailParam detailParam, Pageable pageable){
? ? ? ? return userDetailRepository.findAll((root, query, cb) -> {
? ? ? ? ? ? List<Predicate> predicates = new ArrayList<Predicate>();
? ? ? ? ? ? //equal 示例
? ? ? ? ? ? if (!StringUtils.isNullOrEmpty(detailParam.getIntroduction())){
? ? ? ? ? ? ? ? predicates.add(cb.equal(root.get("introduction"),detailParam.getIntroduction()));
? ? ? ? ? ? }
? ? ? ? ? ? //like 示例
? ? ? ? ? ? if (!StringUtils.isNullOrEmpty(detailParam.getRealName())){
? ? ? ? ? ? ? ? predicates.add(cb.like(root.get("realName"),"%"+detailParam.getRealName()+"%"));
? ? ? ? ? ? }
? ? ? ? ? ? //between 示例
? ? ? ? ? ? if (detailParam.getMinAge()!=null && detailParam.getMaxAge()!=null) {
? ? ? ? ? ? ? ? Predicate agePredicate = cb.between(root.get("age"), detailParam.getMinAge(), detailParam.getMaxAge());
? ? ? ? ? ? ? ? predicates.add(agePredicate);
? ? ? ? ? ? }
? ? ? ? ? ? //greaterThan 大于等于示例
? ? ? ? ? ? if (detailParam.getMinAge()!=null){
? ? ? ? ? ? ? ? predicates.add(cb.greaterThan(root.get("age"),detailParam.getMinAge()));
? ? ? ? ? ? }
? ? ? ? ? ? return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
? ? ? ? }, pageable);
? ? }
}

上面的示例是根據(jù)不同條件來動態(tài)查詢 UserDetail 分頁數(shù)據(jù),UserDetailParam 是參數(shù)的封裝,示例中使用了常用的大于、like、等于等示例,根據(jù)這個思路我們可以不斷擴(kuò)展完成更復(fù)雜的動態(tài) SQL 查詢。

@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaSpecificationTests {
? ? @Resource
? ? private UserDetailService userDetailService;
? ? @Test
? ? public void testFindByCondition() ?{
? ? ? ? int page=0,size=10;
? ? ? ? Sort sort = new Sort(Sort.Direction.DESC, "id");
? ? ? ? Pageable pageable = PageRequest.of(page, size, sort);
? ? ? ? UserDetailParam param=new UserDetailParam();
? ? ? ? param.setIntroduction("程序員");
? ? ? ? param.setMinAge(10);
? ? ? ? param.setMaxAge(30);
? ? ? ? Page<UserDetail> page1=userDetailService.findByCondition(param,pageable);
? ? ? ? for (UserDetail userDetail:page1){
? ? ? ? ? ? System.out.println("userDetail: "+userDetail.toString());
? ? ? ? }
? ? }
}

4、多表查詢

多表查詢在 Spring Data JPA 中有兩種實(shí)現(xiàn)方式,第一種是利用 Hibernate 的級聯(lián)查詢來實(shí)現(xiàn),第二種是創(chuàng)建一個結(jié)果集的接口來接收連表查詢后的結(jié)果,這里主要介紹第二種方式。

我們還是使用上面的 UserDetail 作為數(shù)據(jù)模型來使用,定義一個結(jié)果集的接口類,接口類的內(nèi)容來自于用戶表和用戶詳情表。

public interface UserInfo {
? ? String getUserName();
? ? String getEmail();
? ? String getAddress();
? ? String getHobby();
}

在運(yùn)行中 Spring 會給接口(UserInfo)自動生產(chǎn)一個代理類來接收返回的結(jié)果,代碼中使用 getXX 的形式來獲取。

在 UserDetailRepository 中添加查詢的方法,返回類型設(shè)置為 UserInfo,特別注意這里的 SQL 是 HQL,需要寫類的名和屬性,這塊很容易出錯

@Query("select u.userName as userName, u.email as email, d.introduction as introduction , d.hobby as hobby from User u , UserDetail d " +
? ? ? ? ? ? "where u.id=d.userId ?and ?d.hobby = ?1 ")
List<UserInfo> findUserInfo(String hobby);

測試驗(yàn)證

@Test
public void testUserInfo() ?{
? ? List<UserInfo> userInfos=userDetailRepository.findUserInfo("釣魚");
? ? for (UserInfo userInfo:userInfos){
? ? ? ? System.out.println("userInfo: "+userInfo.getUserName()+"-"+userInfo.getEmail()+"-"+userInfo.getHobby()+"-"+userInfo.getIntroduction());
? ? }
}

測試后返回

userInfo: aa-aa@126.com-釣魚-程序員

5、小結(jié)

Spring Data JPA 使用動態(tài)注入的原理,根據(jù)方法名動態(tài)生成方法的實(shí)現(xiàn),因此根據(jù)方法名實(shí)現(xiàn)數(shù)據(jù)查詢,即可滿足日常絕大部分使用場景。除了這種查詢方式之外,Spring Data JPA 還支持多種自定義查詢來滿足更多復(fù)雜場景的使用,兩種方式相結(jié)合可以靈活滿足項(xiàng)目對 Orm 層的需求。

通過學(xué)習(xí) Spring Data JPA 也可以看出 Spring Boot 的設(shè)計思想,80% 的需求通過默認(rèn)、簡單的方式實(shí)現(xiàn),滿足大部分使用場景,對于另外 20% 復(fù)雜的場景,提供另外的技術(shù)手段來解決。Spring Data JPA 中根據(jù)方法名動態(tài)實(shí)現(xiàn) SQL,組件環(huán)境自動配置等細(xì)節(jié),都是將 Spring Boot 約定優(yōu)于配置的思想體現(xiàn)的淋淋盡致。

三、Spring Data JPA多數(shù)據(jù)源使用

1、前言

項(xiàng)目中使用多個數(shù)據(jù)源在以往工作中比較常見,微服務(wù)架構(gòu)中不建議一個項(xiàng)目使用多個數(shù)據(jù)源。在微服務(wù)架構(gòu)下,一個微服務(wù)擁有自己獨(dú)立的一個數(shù)據(jù)庫,如果此微服務(wù)要使用其他數(shù)據(jù)庫的數(shù)據(jù),需要調(diào)用對應(yīng)庫的微服務(wù)接口來調(diào)用,而不是在一個項(xiàng)目中連接使用多個數(shù)據(jù)庫,這樣微服務(wù)更獨(dú)立、更容易水平擴(kuò)展。

雖然在微服務(wù)架構(gòu)下,不提倡一個項(xiàng)目擁有多個數(shù)據(jù)源,但在 Spring Boot 體系中,項(xiàng)目實(shí)現(xiàn)多數(shù)據(jù)源調(diào)用卻是一件很容易的事情,本節(jié)課將介紹 Spring Data JPA 多數(shù)據(jù)源的使用。

Spring Data JPA 使用多數(shù)據(jù)源的整體思路是,配置不同的數(shù)據(jù)源,在啟動時分別加載多個數(shù)據(jù)源配置,并且注入到不同的 repository 中。這樣不同的 repository 包就有不同的數(shù)據(jù)源,使用時注入對應(yīng)包下的 repository,就會使用對應(yīng)數(shù)據(jù)源的操作。

項(xiàng)目結(jié)構(gòu)如下:

onfig 啟動時加載、配置多數(shù)據(jù)源;

model 存放數(shù)據(jù)操作的實(shí)體類;

repository 目錄下有兩個包路徑 test1 和 test2 ,分別代表兩個不同數(shù)據(jù)源下的倉庫,這兩個包下的 repository 可以相同也可以不同。

2、多數(shù)據(jù)源的支持

配置 Spring Data JPA 對多數(shù)據(jù)源的使用,一般分為以下幾步:

  • 創(chuàng)建數(shù)據(jù)庫 test1 和 test2
  • 配置多數(shù)據(jù)源
  • 不同源的 repository 放入不同包路徑
  • 聲明不同的包路徑下使用不同的數(shù)據(jù)源、事務(wù)支持
  • 不同的包路徑下創(chuàng)建對應(yīng)的 repository
  • 測試使用

上面的一些步驟我們在前面兩課中已經(jīng)講過了,這里只補(bǔ)充不同的內(nèi)容。

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.secondary.username=root
spring.datasource.secondary.password=root
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
# 設(shè)置將項(xiàng)目中的 SQL 格式化后打印出來,方便在開發(fā)過程中調(diào)試跟蹤。
#SQL 輸出
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.hbm2ddl.auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
#format 一下 SQL 進(jìn)行輸出
spring.jpa.properties.hibernate.format_sql=true

創(chuàng)建 DataSourceConfig 添加 @Configuration 注解,在項(xiàng)目啟動時運(yùn)行初始化數(shù)據(jù)庫資源。

@Configuration
public class DataSourceConfig {
}

在 DataSourceConfig 類中加載配置文件,利用 ConfigurationProperties 自動裝配的特性加載兩個數(shù)據(jù)源。

加載第一個數(shù)據(jù)源,數(shù)據(jù)源配置以 spring.datasource.primary 開頭,注意當(dāng)有多個數(shù)據(jù)源時,需要將其中一個標(biāo)注為 @Primary,作為默認(rèn)的數(shù)據(jù)源使用。

@Bean(name = "primaryDataSource")
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSource firstDataSource() {
? ? return DataSourceBuilder.create().build();
}

加載第二個數(shù)據(jù)源,數(shù)據(jù)源配置以 spring.datasource.secondary 為開頭。

@Bean(name = "secondaryDataSource")
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondDataSource() {
? ? return DataSourceBuilder.create().build();
}

加載 JPA 的相關(guān)配置信息,JpaProperties 是 JPA 的一些屬性配置信息,構(gòu)建 LocalEntityManagerFactoryBean 需要參數(shù)信息注入到方法中。

@Autowired
private JpaProperties jpaProperties;
@Autowired
private HibernateProperties hibernateProperties;
@Bean(name = "vendorProperties")
public Map<String, Object> getVendorProperties() {
? ? return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
}

第一個數(shù)據(jù)源的加載配置過程

首先來看第一個數(shù)據(jù)源的加載配置過程,創(chuàng)建 PrimaryConfig 類,將上面創(chuàng)建好的第一個數(shù)據(jù)源注入到類中,添加 @Configuration 和 @EnableTransactionManagement 注解,第一個代表啟動時加載,第二個注

解表示啟用事務(wù),同時將第一個數(shù)據(jù)源和 JPA 配置信息注入到類中。

@Configuration
@EnableTransactionManagement
public class PrimaryConfig {
? ? @Autowired
? ? @Qualifier("primaryDataSource")
? ? private DataSource primaryDataSource;
? ? @Autowired
? ? @Qualifier("vendorProperties")
? ? private Map<String, Object> vendorProperties;
}

LocalEntityManagerFactoryBean 負(fù)責(zé)創(chuàng)建一個適合于僅使用 JPA 進(jìn)行數(shù)據(jù)訪問的環(huán)境的 EntityManager,構(gòu)建的時候需要指明提示實(shí)體類的包路徑、數(shù)據(jù)源和 JPA 配置信息。

@Bean(name = "entityManagerFactoryPrimary")
@Primary
public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary (EntityManagerFactoryBuilder builder) {
? ? return builder
? ? ? ? ? ? .dataSource(primaryDataSource)
? ? ? ? ? ? .properties(vendorProperties)
? ? ? ? ? ? .packages("com.neo.model") //設(shè)置實(shí)體類所在位置
? ? ? ? ? ? .persistenceUnit("primaryPersistenceUnit")
? ? ? ? ? ? .build();
}

利用上面的 entityManagerFactoryPrimary() 方法構(gòu)建好最終的 EntityManager。

@Bean(name = "entityManagerPrimary")
@Primary
public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
? ? return entityManagerFactoryPrimary(builder).getObject().createEntityManager();
}

EntityManager 是 JPA 中用于增、刪、改、查的接口,它的作用相當(dāng)于一座橋梁,連接內(nèi)存中的 Java 對象和數(shù)據(jù)庫的數(shù)據(jù)存儲。使用 EntityManager 中的相關(guān)接口對數(shù)據(jù)庫實(shí)體進(jìn)行操作的時候, EntityManager

會跟蹤實(shí)體對象的狀態(tài),并決定在特定時刻將對實(shí)體的操作映射到數(shù)據(jù)庫操作上面,同時給數(shù)據(jù)源添加上 JPA 事務(wù)。

@Bean(name = "transactionManagerPrimary")
@Primary
PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
? ? return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());
}

最后一步最為關(guān)鍵,將我們在類中配置好的 EntityManager 和事務(wù)信息注入到對應(yīng)數(shù)據(jù)源的 repository 目錄下,這樣此目錄下的 repository 就會擁有對應(yīng)數(shù)據(jù)源和事務(wù)的信息。

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
? ? ? ? entityManagerFactoryRef="entityManagerFactoryPrimary",
? ? ? ? transactionManagerRef="transactionManagerPrimary",
? ? ? ? basePackages= { "com.neo.repository.test1" })//設(shè)置dao(repo)所在位置
public class PrimaryConfig {}

其中,basePackages 支持設(shè)置多個包路徑,例如,basePackages= { "com.neo.repository.test1","com.neo.repository.test3" }

第二個數(shù)據(jù)源的加載配置過程

第二個數(shù)據(jù)源配置和第一個數(shù)據(jù)源配置類似,只是方法上去掉了注解:@Primary,第二個數(shù)據(jù)源數(shù)據(jù)源加載配置類 SecondaryConfig 完整代碼如下: 

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
? ? ? ? entityManagerFactoryRef="entityManagerFactorySecondary",
? ? ? ? transactionManagerRef="transactionManagerSecondary",
? ? ? ? basePackages= { "com.neo.repository.test2" })
public class SecondaryConfig {
? ? @Autowired
? ? @Qualifier("secondaryDataSource")
? ? private DataSource secondaryDataSource;
? ? @Autowired
? ? @Qualifier("vendorProperties")
? ? private Map<String, Object> vendorProperties;
? ? @Bean(name = "entityManagerFactorySecondary")
? ? public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary (EntityManagerFactoryBuilder builder) {
? ? ? ? return builder
? ? ? ? ? ? ? ? .dataSource(secondaryDataSource)
? ? ? ? ? ? ? ? .properties(vendorProperties)
? ? ? ? ? ? ? ? .packages("com.neo.model")
? ? ? ? ? ? ? ? .persistenceUnit("secondaryPersistenceUnit")
? ? ? ? ? ? ? ? .build();
? ? }
? ? @Bean(name = "entityManagerSecondary")
? ? public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
? ? ? ? return entityManagerFactorySecondary(builder).getObject().createEntityManager();
? ? }
? ? @Bean(name = "transactionManagerSecondary")
? ? PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder) {
? ? ? ? return new JpaTransactionManager(entityManagerFactorySecondary(builder).getObject());
? ? }
}

到此多數(shù)據(jù)源的配置就完成了,項(xiàng)目中使用哪個數(shù)據(jù)源的操作,就注入對應(yīng)包下的 repository 進(jìn)行操作即可,接下來我們對上面配置好的數(shù)據(jù)源進(jìn)行測試。

創(chuàng)建 UserRepositoryTests 測試類,將兩個包下的 repository 都注入到測試類中:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTests {
? ? @Resource
? ? private UserTest1Repository userTest1Repository;
? ? @Resource
? ? private UserTest2Repository userTest2Repository;
}

首先測試兩個數(shù)據(jù)庫中都存入數(shù)據(jù),數(shù)據(jù)源1插入 2 條用戶信息,數(shù)據(jù)源2插入 1 條用戶信息。

@Test
public void testSave() throws Exception {
? ? Date date = new Date();
? ? DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
? ? String formattedDate = dateFormat.format(date);
? ? userTest1Repository.save(new User("aa", "aa123456","aa@126.com", "aa", ?formattedDate));
? ? userTest1Repository.save(new User("bb", "bb123456","bb@126.com", "bb", ?formattedDate));
? ? userTest2Repository.save(new User("cc", "cc123456","cc@126.com", "cc", ?formattedDate));
}

執(zhí)行完測試用例后查看數(shù)據(jù)庫,發(fā)現(xiàn) test1 庫有兩條數(shù)據(jù),test2 有一條,證明兩個數(shù)據(jù)源均保存數(shù)據(jù)正常。下面繼續(xù)測試刪除功能,使用兩個數(shù)據(jù)源的 repository 將用戶信息全部刪除。

@Test
public void testDelete() throws Exception {
? ? userTest1Repository.deleteAll();
? ? userTest2Repository.deleteAll();
}

執(zhí)行完測試用例后,發(fā)現(xiàn) test1 庫和 test2 庫用戶表的信息已經(jīng)被清空,證明多數(shù)據(jù)源刪除成功。

3、小結(jié)

Spring Data JPA 通過在啟動時加載不同的數(shù)據(jù)源,并將不同的數(shù)據(jù)源注入到不同的 repository 包下,從而實(shí)現(xiàn)項(xiàng)目多數(shù)據(jù)源操作,在項(xiàng)目中使用多數(shù)據(jù)源時,需要用到哪個數(shù)據(jù)源,只需要將對應(yīng)包下的 repository 注入操作即可。本課示例中以兩個數(shù)據(jù)源作為演示,但其實(shí)三個或者更多數(shù)據(jù)源配置、操作,都可以按照上面方法進(jìn)行配置使用。

以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

最新評論