Spring Boot 2.0 讀書筆記_10:Spring Data JPA 下

一、持久化Entity

首先我們創建一個UserRepository,繼承接口JpaRepository(該接口集成了所有常用接口方法),進行基礎CURD的操作測試。

注意:
<User, Integer>,分別代表:實體類以及實體類主鍵屬性封裝類型
UserRepository也就是對應常規開發模式下的DAO接口

public interface UserRepository extends JpaRepository<User, Integer> {
  
  // 保存(添加)和更新
  @Override
  <S extends User> S save(S s);

  // 根據Id刪除
  @Override
  void deleteById(Integer integer);

  // 根據Id查詢
  @Override
  Optional<User> findById(Integer integer);
}

Service層實現

  • save:保存(添加)、更新

      // 添加和更新實現
      public Integer addUser(User user){
        user.setName("張三");
        userDao.save(user);
        user.setName(user.getName() + "_");
        userDao.save(user);
        return user.getId();
      }
    

測試結果:

  Hibernate: insert into user (create_time, department_id, name) values (?, ?, ?)
  Hibernate: update user set create_time=?, department_id=?, name=? where id=?

數據庫返回結果:
在這裏插入圖片描述
分析:
在定義User Entity時,對id成員變量進行了@Id、@GeneratedValue(strategy=GenerationType.IDENTITY)標註,表名該成員變量對應數據庫表中的主鍵,並且採用了主鍵自增的策略。

通過addUser方法save User對象時,沒有setId(),主鍵爲空情況下,插入user對象到數據庫。看到的SQL語句打印也並沒有Id字段,是因爲主鍵自增策略的緣故,插入後主鍵Id賦值爲4。當第二個save方法執行的時候,User對象已經有主鍵了。因此執行了更新操作,Id是通過user.getId獲取的,因此最終結果name字段變爲了 '張三_'

  • findById:根據Id進行查詢

    public User findUser(int id) {
      Optional<User> user = userDao.findById(id);
      return user;
    }
    

    測試結果:

    Hibernate: select user0_.id as id1_1_0_, user0_.create_time as create_t2_1_0_, 
    user0_.department_id as departme4_1_0_, user0_.name as name3_1_0_, department1_.id 
    as id1_0_1_, department1_.name as name2_0_1_ from user user0_ left outer join 
    department department1_ on user0_.department_id=department1_.id where user0_.id=?
    
  • deleteById:根據Id進行刪除

    public boolean deleteUser(int id) {
      userDao.deleteById(id);
      return userDao.existsById(id);
    }
    

    測試結果:false

    Hibernate: delete from user where id=?
    Hibernate: select count(*) as col_0_0_ from user user0_ where user0_.id=?
    

總結下:通過簡單的CURD測試發現,基本的CURD功能可以實現,也無需使用XML配置SQL。但是打印的執行SQL着實有點複雜,難道這就是自動生成的SQL該有的樣子?當然針對非過多複雜查詢的業務場景下是可以的,牽扯複雜查詢之後,避免不了SQL語句的自定義。

二、Sort 排序

測試內置排序方法:

List findAll(Sort sort); 返回所有實體,按照指定排序(默認升序)返回。

Sort對象的構造方法:

方法 說明
public Sort(String… properties) 按照指定的屬性列表升序排序
public Sort(Direction direction, String… properties) 按照指定屬性列表排序,排序方式由Direction指定,Direction是枚舉類,有Direction.ASC和Direction.DESC兩類
public Sort(Order… orders) Order 可以通過Order靜態方法來創建
public static Order asc(String propertyName);
public static Order desc(String propertyName);
  • getAllUserList:返回用戶實體集合

    public List<User> getAllUserList() {
      // 按照Id降序查詢
      Sort sort = new Sort(Sort.Direction.DESC, "id");
      List<User> all = userDao.findAll(sort);
      return all;
    }
    

    測試結果:

    Hibernate: select user0_.id as id1_1_, user0_.create_time as 
    create_t2_1_, user0_.department_id as departme4_1_, user0_.name as 
    name3_1_ from user user0_ order by user0_.id desc
    

    返回截圖:
    在這裏插入圖片描述

三、Page和Pageable

Pageable接口用於構造翻頁查詢,PageRequest是其實現類

測試內置分頁方法(含排序):

Page findAll(Pageable pageable); 返回實體列表,實體列表的offset、limit通過方法參數 Pageable 控制。

這裏採用PageRequest中的如下構造方法實例化Pageable接口:

public PageRequest(int page, int size, 
    Direction direction, String... properties) {
    this(page, size, Sort.by(direction, properties));
}

注意:
page 總是從0開始,標識查詢頁,size標識每頁期望的行數。
Spring Data 分頁返回Page對象,Page對象提供了以下常用方法:

方法 說明
int getTotalPages() 總頁數
long getTotalElements() 返回元素總數
List getContent() 返回此次查詢的結果集
  • getAllUser(int page, int size):返回分頁實體列表

    public List<User> getAllUser(int page, int size) {
      PageRequest pageable = PageRequest.of(page, size, Sort.Direction.DESC, "id");
      Page<User> pageObject =  userDao.findAll(pageable);
      int totalPage = pageObject.getTotalPages();
      long count = pageObject.getTotalElements();
      return pageObject.getContent();
    }
    

    這裏按照size = 2處理,一共5條數據,理論分3頁。
    測試前數據:
    在這裏插入圖片描述
    測試page0&size=2時的請求:
    在這裏插入圖片描述

四、JPQL
Spring Data 可以通過查詢的方法名和參數名自動構造 JPA OQL 查詢。
JPA OQL 簡稱 JPQL:[Java Persistence Query Language] Java持久化查詢語言,旨在以面向對象表達式語言的表達式,將SQL語法簡單查詢語義綁定在一起。這種語言編寫的查詢是可移植的,可以被編譯成主流數據庫服務器上的SQL

其特徵與原生SQL語句類似,並且完全面向對象,通過類名屬性訪問,而不是表名表的屬性

  • 基於方法名字查詢

    public interface UserRepository extends JpaRepository<User, Integer> {
      public User findByName(String name);
    }
    

    注意:方法名和參數名需要遵循一定的規則,Spring Data JPA才能自動轉化爲JPQL

    • 方法名通常包含多個實體屬性(成員變量)用於查詢,屬性(成員變量)之間可以使用ANDOR連接,也支持BetweenLessThanGreaterThanLike;
    • 方法名可以以findBygetByqueryBy開頭;
    • 查詢結果可以排序,方法名需要包含OrderBy+屬性+Asc(Desc);
    • 可以通過TopFirst來限定查詢結果集;
    • 一些特殊的參數可以出現在參數列表(簽名)中,如:PageableSort

    舉堆栗子:

    • 根據名字查詢,結果升序,且分頁

      Page<User> findByLastnameOrderByFirstNameAsc(String lastname, Pageable pageable);
      
    • 查詢滿足條件的前10個用戶

      List<User> findFirst10ByLastname(String lastname, Sort sort);
      
    • 使用And聯合查詢

      List<User> findByLastnameAndFirstname(String lastname, String firstname);
      
    • 使用Or查詢

      List<User> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
      
    • 使用like查詢,name中必須包含like中的%或者?

      User findByNameLike(String name);
      
  • Spring Data支持的關鍵字

    關鍵字 例子 轉化的JPQL片段
    And findByLastnameAndFirstname …where x.lastname = ?1 and x.firstname = ?2
    Or findByLastnameOrFirstname …where x.lastname = ?1 or x.firstname = ?2
    Between findByStartDateBetween …where x.startDate between ?1 and ?2
    LessThan findByAgeLessThan
    findByAgeLessThanEqual
    …where x.age < ?1
    …where x.age <= ?1
    GreaterThan findByAgeGreaterThan
    findByAgeGreaterThanEqual
    …where x.age > ?1
    …where x.age >= ?1
    After(DateTime) findByStartDateAfter …where x.startDate > ?1
    Before(DateTime) findByStartDateBefore …where x.startDate < ?1
    IsNull findByAgeIsNull
    findByAge(Is)NotNull
    …where x.age is null
    …where x.age not null
    OrderBy findByAgeOrderByLastnameDesc …where x.age = ?1 order by x.lastname desc
    Not findByLastnameNot …where x.lastname <>?1
    In findByAgeIn(Collection ages)
    findByAgeNotIn(Collection ages)
    …where x.age in ?1
    …where x.age not in ?1
    True findByActiveTrue
    finByActiveFlase
    …where x.active = true
    …where x.active = false
    IgnoreCase findByFirstnameIgnoreCase …where UPPER(x.firstname) = UPPER(?1)
    Like findByFirstnameLike
    findByFirstnameNotLike
    …where x.firstname like ?1
    …where x.firstname not like ?1

    突然覺得這種JPA查詢好像很高大上的樣子,完全無SQL查詢。真是厲害了我的JPQL!

五、@Query查詢

@Query註解允許在方法使用JPQL

  • JPQL:面向對象和對象屬性

    @Query(value = "select u from User u where u.name = ?1 and u.department.id = ?2")
    public User findUser(String name, Integer departmentId);
    
  • SQL:面向數據表名和字段名 (nativeQuery = true)

    @Query(value = "select * from User where name = ?1 and department_id = ?2", 
            nativeQuery =  true)
    public User findUser(String name, Integer departmentId);
    
  • 無論是JPQL還是SQL,均支持命名參數。例如:

    @Query(value = "select * from User where name = :name and 
            department_id = :departmentId", nativeQuery =  true)
    public User findUser(String name, Integer departmentId);
    

    命名參數在上一章節JDBC Template 中也提到了,這樣有利於SQL語句參數的和方法參數的映射,常規的索引方式固然方便,但是方法參數簽名順序變更後,SQL語句中的?佔位符順序也要變更。各有優劣。

  • Object[]數組非Entity查詢返回結果的替代方案

    例如:分組統計每個部門的用戶數目

    @Query(value = "select department_id, count(*) from User group by department_id", 
          nativeQuery = true)
    public List<Object[]> queryUserCount();
    
  • @Modifying:JPQL更新、刪除操作時搭配使用

    @Modifying
    @Query(value = "update User u set u.name = ?1 where u.id = ?2")
    public int updateName(String name, Integer id);
    

六、Example 查詢
上邊提到了方法名構造JPQL@Query構造JPQL進行查詢,Example對象的查詢方案是指:根據實體(Entity)創建一個Example對象,Spring Data 通過 Example對象來構造JPQL

public List<User> getByExample(String name) {
  // 創建對象
  User user = new User();
  Department dept = new Department();
  // 屬性賦值
  user.setName(name);
  dept.setId(1);
  // 組合對象user賦值
  user.setDepartment(dept);
  // Exanple.of靜態方法構造example對象
  Example<User> example = Example.of(user);
  // 調用findAll進行查詢
  List<User> list = userDao.findAll(example);
  return list;
}

總結下:通過構造實體查詢條件user,結合Example.of(user)的靜態方法構造Example對象,最後將Example對象作爲參數傳入JpaRepository(UserRepository)實現方法中進行執行查詢。

非完全匹配查詢:ExampleMatcher

  // ExampleMatcher提供了條件指定。下述代碼條件爲:名稱Tom開頭的所有用戶
  ExampleMatcher matcher = ExampleMatcher.matching()
    .withMatcher("Tom", GenericPropertyMatchers.startsWith().ignoreCase());
  Example<User> example = Example.of(user, matcher);

七、EntityManager:JPA提供的底層數據庫訪問接口

書中這裏提到了一個例子:動態分頁查詢中使用EntityManager彌補Repository的不足。

這裏就直接上代碼說明了:

public Page<User> queryUser(Integer departmentId, Pageable page) {
  //構造基礎JPQL,這個JPQL將作爲求和以及分頁查詢的基礎JPQL
  StringBuilder baseJpql = new StringBuilder("from User u where 1 = 1 ");
  // 初始化查詢參數Map
  Map<String, Object> paras = new HashMap<String, Object>();
  if (departmentId != null) {
      // 添加附加添加參數,拼裝JPQL
      baseJpql.append("and u.department.id = :deptId");
      // 配置附加查詢參數 deptId
      paras.put("deptId", departmentId);
  }
  //功能1:查詢滿足條件的總數
  long count = getQueryCount(baseJpql, paras);
  if (count == 0) {
      return new PageImpl(Collections.emptyList(), page, 0);
  }
  //功能2:查詢滿足分頁條件結果集
  List list = getQueryResult(baseJpql, paras, page);
  //返回結果
  Page ret = new PageImpl(list, page, count);
  return ret;
}

通用:查詢參數配置方法:setQueryParameter()

private void setQueryParameter(Query query, Map<String, Object> paras) {
  for (Entry<String, Object> entry : paras.entrySet()) {
    query.setParameter(entry.getKey(), entry.getValue());
  }
}

功能1:求和查詢 getQueryCount()

private Long getQueryCount(StringBuilder baseJpql, 
                            Map<String, Object> paras) {
  // 創建Query 查詢對象 結合傳遞的JPQL
  Query query = em.createQuery("select count(1) " + baseJpql.toString());
  // 調用通用查詢參數配置方法(見上)
  setQueryParameter(query, paras);
  // 調用query對象的查詢方法:getSingleResult()得到滿足條件的記錄總數
  Number number = (Number) query.getSingleResult();
  return number.longValue();
}

功能2:分頁查詢 getQueryResult()

private List getQueryResult(StringBuilder baseJpql, Map<String, Object> paras, Pageable page) {
  // 創建Query 查詢對象 結合傳遞的JPQL
  Query query = em.createQuery("select u " + baseJpql.toString());
  // 調用通用查詢參數配置方法(見上上)
  setQueryParameter(query, paras);
  // 配置分頁相關信息
  query.setFirstResult((int) page.getOffset());
  query.setMaxResults(page.getPageNumber());
  // 調用查詢方法得到list
  List list = query.getResultList();
  return list;
}

queryUser() 的邏輯梳理爲:首先通過拼裝好的JPQL進行求和查詢,根據返回count是否爲 0,爲 0 則返回一個空頁(Collections.emptyList( )),非 0 便根據總數進行記錄分頁,分頁參數對象作爲參數傳入分頁查詢方法,最後返回分頁結果數據集。

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