Spring Data JPA 基礎(一)

一. 概述

Spring Data JPA 是 Java Persistence API (JPA) 規範的實現,底層是對Hibernate 5.x 操作數據庫的封裝,它簡化了在java開發中使用 JPA 訪問數據庫的操作。

二. 使用 Spring Data Repositories

Spring Data repository 抽象的目的就是顯著減少各種數據訪問層實現技術的樣板代碼:

// Spring Data repository 是最基礎的接口,用於獲取repository管理的domain對象的類型以及id的類型
// 注意泛型的使用
public interface Repository<T, ID extends Serializable> {

}

CrudRepository接口繼承了Repository接口,並提供了它所管理的domain對象的基本的 CRUD 方法,新版本中還添加了很多新的api,具體信息可以查詢api:

// domain對象CRUD操作的泛型接口
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
    // 保存一個實體對象 insert into...
    <S extends T> S save(S entity);
    // 根據id獲取domain對象,select * from xxx where id = ?
    Optional<T> findById(ID primaryKey);
    // 查詢表中所有記錄,將返回的結果集封裝到一個集合對象中(List),select * from xxx
    Iterable<T> findAll();
    // 統計表中記錄數
    long count();
    // 刪除記錄
    void delete(T entity);
    
    // ... lookup api for more details
}

PagingAndSortingRepository接口繼承了CrudRepository接口並添加了分頁查詢的方法:

public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
    // Pageable對象封裝了分頁查詢的參數,用於SQL中limit子句的查詢參數 page & size
    Page<T> findAll(Pageable pageable);
}

JpaRepository接口又繼承了PagingAndSortingRepository,開發中用戶自定義repository可以直接繼承該接口,同時JpaRepository還繼承了一個QueryByExampleExecutor接口,該接口提供對QBE查詢方法的支持,比較非主流。

根據 JPA 規範,項目中的領域對象要添加註解,用於建立domain和數據庫表之間的靜態映射關係:

@Entity
@Table(name = "tb_user")
public class User implements Serializable {
	// id可以設置生成策略,這裏選擇通過id生成器手動設置
	@Id
	private String id;
	@Column	// 屬性名稱如果和表中字段名稱一致可以不加註解
	private String username;
	@Column
	private String password;
	@Column
	private String email
	@Column
	private Date birthday;

	// ... constructor & methods
}

三. JPA Repositories

在使用 JPA 時Query對象的獲取有兩種方法,一種是手寫SQL,另一種是從方法名稱中派生(神奇)。從方法名稱中派生的方式適用於查詢條件不太複雜的情況,否者方法名稱會變得很長很難看,還有在涉及關聯查詢的時候手寫SQL更方便。

從查詢方法名稱中派生Query對象

public interface UserRepository extends JpaRepository<User, Long> {
    // 創建query對象操作JPA criteria api,該對象對應如下的JPQL查詢語句
    // select from User u where u.emailAddress = ?1 and u.lastname = ?2
    List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

JPA使用“關鍵字”機制分割方法名稱,建立多條件查詢對象Query,該對象對應一條JQPL查詢語句片段,關鍵字用於組合各種查詢條件,JPA中關鍵字一共有二十多個,以下爲常用的:

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 findByFirstname , findByFirstnameEquals … where x.firstname = ?1
LessThan findByAgeLessThan … where x.age < ?1
GrateThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
Like findByFirstnameLike … where x.firstname like ?1

方法名稱中可以使用關鍵字top或者first來過濾出查詢結果的前n條記錄:

List<User> findFirst10ByLastname(String lastname);

@Query 的使用方法

Spring Data JPA 中可以在查詢方法上添加@Query註解來定義一條 JPQL 查詢語句:

public interface UserRepository extends JpaRepository<User, Long> {
    // 注意模糊查詢的使用
    @Query("select from User u where u.firstname like %?1")
    List<User> findByFirstnameEndsWith(String firstname);
}

@Query 註解允許創建原生的SQL查詢語句而不是JPQL,使用原生的SQL查詢語句的好處是可以在SQL圖形化界面中調試好了之後再copy到代碼中,防止出錯。返回的結果集被自動映射封裝爲方法返回值聲明類型的對象中。

public interface UserRepository extends JpaRepository<User, Long> {
    // 使用 nativeQuery = true 開啓原生SQL語句查詢
    @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
    User findByEmailAddress(String emailAddress);
    
    @Modifying
    @Query("update User u set u.firstname = ?1 where u.lastname = ?2")
    int setFixedFirstnameFor(String firstname, String lastname);
}

@Modifying 註解

在@Query註解中編寫 JPQL 實現 UPDATE 和 DELETE 的時候必須在方法上加上@Modifying 註解,通知 Spring Data 這是刪除或者更新操作,同時在Service層需要添加事務的支持@Transactional,注意JPQL是不支持INSERT操作的,代碼如上。

四. Specifications

JPA 2 規範中引入了一個條件查詢的api,這個 criteria api 可以使用編程的方式手動設置查詢的條件(where子句)。在 Spring Data JPA 中使用 criteria api 只需要繼承接口JpaSpecificationExecutor,該接口中的查詢方法接收一個Specification 類型的對象,這個對象中封裝了where子句中的查詢條件:

public interface CustomerRepository extends JpaRepository<Customer, Long>, JpaSpecificationExecutor {
    // 繼承JpaSpecificationExecutor接口,實現條件查詢,這個方法特別使用於頁面多參數的
    // 條件查詢,需要在運行中動態構建SQL語句,類似於mybatis中動態SQL語句
}

構建動態查詢條件對象:

/**
 * 動態條件構建
 * @param searchMap
 * @return
 */
private Specification<Problem> createSpecification(Map searchMap) {

    return new Specification<Problem>() {

        @Nullable
        @Override
        public Predicate toPredicate(Root<Problem> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            List<Predicate> predicateList = new ArrayList<Predicate>();
            // ID
            if (searchMap.get("id")!=null && !"".equals(searchMap.get("id"))) {
                predicateList.add(cb.like(root.get("id").as(String.class), "%"+(String)searchMap.get("id")+"%"));
            }
            // 標題
            if (searchMap.get("title")!=null && !"".equals(searchMap.get("title"))) {
                predicateList.add(cb.like(root.get("title").as(String.class), "%"+(String)searchMap.get("title")+"%"));
            }
            // 內容
            if (searchMap.get("content")!=null && !"".equals(searchMap.get("content"))) {
                predicateList.add(cb.like(root.get("content").as(String.class), "%"+(String)searchMap.get("content")+"%"));
            }
            // 用戶ID
            if (searchMap.get("userid")!=null && !"".equals(searchMap.get("userid"))) {
                predicateList.add(cb.like(root.get("userid").as(String.class), "%"+(String)searchMap.get("userid")+"%"));
            }
            // 暱稱
            if (searchMap.get("nickname")!=null && !"".equals(searchMap.get("nickname"))) {
                predicateList.add(cb.like(root.get("nickname").as(String.class), "%"+(String)searchMap.get("nickname")+"%"));
            }
            // 是否解決
            if (searchMap.get("solve")!=null && !"".equals(searchMap.get("solve"))) {
                predicateList.add(cb.like(root.get("solve").as(String.class), "%"+(String)searchMap.get("solve")+"%"));
            }
            // 回覆人暱稱
            if (searchMap.get("replyname")!=null && !"".equals(searchMap.get("replyname"))) {
                predicateList.add(cb.like(root.get("replyname").as(String.class), "%"+(String)searchMap.get("replyname")+"%"));
            }

            return cb.and( predicateList.toArray(new Predicate[predicateList.size()]));
        }
    };
}

然後調用條件查詢:

/**
 * 條件查詢+分頁
 *
 * */
public Page<Problem> pageQuery(Map searchMap, int page, int size){
    Specification<Problem> specification = createSpecification(searchMap);
    Pageable pageable = PageRequest.of(page - 1, size);

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