一. 概述
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);
}