Spring Boot整理——Spring Data JPA
一、基本介紹
JPA誕生的緣由是為了整合第三方ORM框架,建立一種標準的方式,百度百科說是JDK為了實現ORM的天下歸一,目前也是在按照這個方向發展,但是還沒能完全實現。在ORM框架中,Hibernate是一支很大的部隊,使用很廣泛,也很方便,能力也很強,同時Hibernate也是和JPA整合的比較良好,我們可以認為JPA是標準,事實上也是,JPA幾乎都是介面,實現都是Hibernate在做,巨集觀上面看,在JPA的統一之下Hibernate很良好的執行。
我們都知道,在使用持久化工具的時候,一般都有一個物件來操作資料庫,在原生的Hibernate中叫做Session,在JPA中叫做EntityManager,在MyBatis中叫做SqlSession,通過這個物件來操作資料庫。我們一般按照三層結構來看的話,Service層做業務邏輯處理,Dao層和資料庫打交道,在Dao中,就存在著上面的物件。那麼ORM框架本身提供的功能有什麼呢?答案是基本的CRUD,所有的基礎CRUD框架都提供,我們使用起來感覺很方便,很給力,業務邏輯層面的處理ORM是沒有提供的,如果使用原生的框架,業務邏輯程式碼我們一般會自定義,會自己去寫SQL語句,然後執行。在這個時候,Spring-data-jpa的威力就體現出來了,ORM提供的能力他都提供,ORM框架沒有提供的業務邏輯功能Spring-data-jpa也提供,全方位的解決使用者的需求。使用Spring-data-jpa進行開發的過程中,常用的功能,我們幾乎不需要寫一條sql語句。
二、環境配置
1、maven配置
首先需要spring相關架包,其實spring-data-jpa裡面已經依賴了,如果你想用自己的版本則需要額外引入spring相關包.JPA實現還都是hibernate去實現的,所以還需要hibernate相關包.mysql就更不用說了.
<!--JPA start--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>1.10.4.RELEASE</version> </dependency> <!--JPA end--> <!--hibernate start--> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.version}</version> </dependency> <!--hibernate end--> <!--mysql start--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--mysql end-->
2、整合Spring
整合Spring主要有以下幾點要注意:
- 資料來源配置
- JPA提供者,JPA屬性配置
- 事務配置
- jpa:repositories 配置
具體如下程式碼:
<!-- 載入資料庫配置檔案 --> <context:property-placeholder location="classpath:config.properties"/> <!--配置資料庫連線池Druid--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 資料庫基本資訊配置 --> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="driverClassName" value="${jdbc.driver}" /> <property name="filters" value="${filters}" /> <!-- 最大併發連線數 --> <property name="maxActive" value="${maxActive}" /> <!-- 初始化連線數量 --> <property name="initialSize" value="${initialSize}" /> <!-- 配置獲取連線等待超時的時間 --> <property name="maxWait" value="${maxWait}" /> <!-- 最小空閒連線數 --> <property name="minIdle" value="${minIdle}" /> <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" /> <!-- 配置一個連線在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" /> <property name="validationQuery" value="SELECT 1" /> <property name="testWhileIdle" value="${testWhileIdle}" /> <property name="testOnBorrow" value="${testOnBorrow}" /> <property name="testOnReturn" value="${testOnReturn}" /> <property name="maxOpenPreparedStatements" value="${maxOpenPreparedStatements}" /> <!-- 開啟removeAbandoned功能 --> <property name="removeAbandoned" value="${removeAbandoned}" /> <!-- 1800秒,也就是30分鐘 --> <property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}" /> <!-- 關閉abanded連線時輸出錯誤日誌 --> <property name="logAbandoned" value="${logAbandoned}" /> </bean> <!--第二步--> <!--定義實體的工廠bean--> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource"/> <!--實體類位置--> <property name="packagesToScan" value="cn.mrdear.entity"/> <!--持久化單元名--> <property name="persistenceUnitName" value="TestJPA" /> <!--JPA提供者--> <property name="persistenceProviderClass" value="org.hibernate.ejb.HibernatePersistence"/> <!--JPA屬性--> <property name="jpaProperties"> <props> <!--配置方言--> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <!--啟用查詢日誌功能--> <prop key="hibernate.show_sql">false</prop> <!--優雅地輸出Sql--> <prop key="hibernate.format_sql">false</prop> <!--新增一條解釋型標註--> <prop key="hibernate.use_sql_comments">false</prop> <!--配置如何根據java模型生成資料庫表結構,常用update,validate--> <prop key="hibernate.hbm2ddl.auto">none</prop> </props> </property> </bean> <!--第三步--> <!--定義事務管理器--> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="dataSource" ref="dataSource"/> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <!--第四步--> <!--定義repository介面的存放目錄--> <!--定義介面實現的字尾,通常用Impl--> <!--定義實體工廠的引用--> <!--定義事務管理器的引用--> <jpa:repositories base-package="cn.mrdear.repository" repository-impl-postfix="Impl" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/> <!--第五步--> <!--宣告採用註解的方式申明事務--> <tx:annotation-driven transaction-manager="transactionManager"/>
3、建立實體類
實體類中常用註解:
- @Entity :宣告這個類是一個實體類
- @Table:指定對映到資料庫的表格
- @Id :對映到資料庫表的主鍵屬性,一個實體只能有一個屬性被對映為主鍵
- @GeneratedValue:主鍵的生成策略
- @Column配置單列屬性
@Entity//標識該為一個實體
@Table(name = "user")//關聯資料庫中的user表
public class User {
@Id//標識該屬性為主鍵
private Integer id;
private String name;
private String address;
private String phone;
//省略get和set
}
4、Repository介面
- Repository: 最頂層的介面,是一個空介面,目的是為了統一所有的Repository的型別,且能讓元件掃描時自動識別
- CrudRepository: Repository的子介面,提供CRUD 的功能。
- PagingAndSortingRepository:CrudRepository的子介面, 新增分頁排序。
- JpaRepository: PagingAndSortingRepository的子介面,增加批量操作等。
- JpaSpecificationExecutor: 用來做複雜查詢的介面。
由圖來看,一般使用JpaRepository這個介面做查詢即可.這個介面擁有如下方法:
- delete刪除或批量刪除
- findOne查詢單個
- findAll查詢所有
- save儲存單個或批量儲存
- saveAndFlush儲存並重新整理到資料庫
知道這些我們就可以建立repository 了:
//User表示該Repository與實體User關聯,主鍵型別為Integer
public interface UserRepository extends JpaRepository<User,Integer> {
}
這樣就完成了一個基本Repository的建立,可以直接使用其中的方法,而不需要去寫實現類。
5、測試
關於測試這裡,我把測試案例寫到test資料夾的話,總是報實體類未被JPA管理,所以改寫到java資料夾,具體原因未知.
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
UserRepository userRepository = (UserRepository) applicationContext.getBean("userRepository");
System.out.println(userRepository.findAll());
System.out.println(userRepository.findOne(1));
System.out.println(userRepository.findAll(new Sort(new Sort.Order(Sort.Direction.ASC,"id"))));
}
三、CRUD操作
1、增加
增加可以使用JpaRepository接口裡面的save方法.檢視原始碼可以發現實際上是使用了em.persist(entity)來使物件進入持久化狀態,最後提交事務的時候再一起更新到資料庫:
User user = new User();
user.setId(99);
user.setAddress("上海");
user.setName("張三");
user.setPhone("110");
//儲存單個
userRepository.save(user);
//儲存或更新
userRepository.saveAndFlush(user);
List<User> users = new ArrayList<>();
users.add(user);
//儲存多個
userRepository.save(users);
這裡還可以批量插入,對於mysql支援INSERT user VALUES (20,'王二','111','111'),(21,'王二','111','111');類似這樣的sql語句,具體實現就需要自己去寫實現了,這樣可以一次插入多條記錄,效率很高.至於一次插入多少條就可以根據你的業務量來自己制定。
2、刪除
刪除都是根據主鍵來刪除的,區別就是多條sql和單條sql :
User user = new User();
user.setId(21);
user.setName("王二");
/**
* 刪除都是根據主鍵刪除
*/
//刪除單條,根據主鍵值
userRepository.delete(20);
//刪除全部,先findALL查找出來,再一條一條刪除,最後提交事務
userRepository.deleteAll();
//刪除全部,一條sql
userRepository.deleteAllInBatch();
List<User> users = new ArrayList<>();
users.add(user);
//刪除集合,一條一條刪除
userRepository.delete(users);
//刪除集合,一條sql,拼接or語句 如 id=1 or id=2
userRepository.deleteInBatch(users);
3、修改
修改也是根據主鍵來更新的 :
User user = new User();
user.setId(1);
user.setName("張三");
/**
* 更新也是根據主鍵來更新 update XX xx where id=1
*/
userRepository.saveAndFlush(user);
批量更新的話,就呼叫entityManager的merge函式來更新.
//首先在service層獲取持久化管理器:
@PersistenceContext
private EntityManager em;
//批量更新方法,同理插入,刪除也都可以如此做.
@Transactional
public void batchUpateCustom(List<User> users) {
// TODO Auto-generated method stub
for(int i = 0; i < users.size(); i++) {
em.merge(users.get(i));
if(i % 30== 0) {
em.flush();
em.clear();
}
}
}
4、簡單查詢
單表查詢,大部分都可以使用下面的方法解決,多表聯合查詢的話,下面方法就不是很實用,下一節分析多表查詢。
1.使用JpaRepository方法
//查詢全部
userRepository.findAll();
//分頁查詢全部,返回封裝了分頁資訊
Page<User> pageInfo = userRepository.findAll(new PageRequest(1, 3, Sort.Direction.ASC,"id"));
//查詢全部,並排序
userRepository.findAll(new Sort(new Sort.Order(Sort.Direction.ASC,"id")));
User user = new User();
user.setName("小紅");
//條件查詢,可以聯合分頁,排序
userRepository.findAll(Example.of(user));
//查詢單個
userRepository.findOne(1);
2.解析方法名建立查詢
規則:find+全域性修飾+By+實體的屬性名稱+限定詞+連線詞+ …(其它實體屬性)+OrderBy+排序屬性+排序方向。例如:
//分頁查詢出符合姓名的記錄,同理Sort也可以直接加上
public List<User> findByName(String name, Pageable pageable);
全域性修飾: Distinct, Top, First 關鍵詞: IsNull, IsNotNull, Like, NotLike, Containing, In, NotIn, IgnoreCase, Between, Equals, LessThan, GreaterThan, After, Before… 排序方向: Asc, Desc 連線詞: And, Or And — 等價於 SQL 中的 and 關鍵字,比如 findByUsernameAndPassword(String user, Striang pwd); Or — 等價於 SQL 中的 or 關鍵字,比如 findByUsernameOrAddress(String user, String addr); Between — 等價於 SQL 中的 between 關鍵字,比如 findBySalaryBetween(int max, int min); LessThan — 等價於 SQL 中的 “<”,比如 findBySalaryLessThan(int max); GreaterThan — 等價於 SQL 中的”>”,比如 findBySalaryGreaterThan(int min); IsNull — 等價於 SQL 中的 “is null”,比如 findByUsernameIsNull(); IsNotNull — 等價於 SQL 中的 “is not null”,比如 findByUsernameIsNotNull(); NotNull — 與 IsNotNull 等價; Like — 等價於 SQL 中的 “like”,比如 findByUsernameLike(String user); NotLike — 等價於 SQL 中的 “not like”,比如 findByUsernameNotLike(String user); OrderBy — 等價於 SQL 中的 “order by”,比如 findByUsernameOrderBySalaryAsc(String user); Not — 等價於 SQL 中的 “! =”,比如 findByUsernameNot(String user); In — 等價於 SQL 中的 “in”,比如 findByUsernameIn(Collection userList) ,方法的引數可以是 Collection 型別,也可以是陣列或者不定長引數; NotIn — 等價於 SQL 中的 “not in”,比如 findByUsernameNotIn(Collection userList) ,方法的引數可以是 Collection 型別,也可以是陣列或者不定長引數; |
巢狀實體:
主實體中子實體的名稱+ _ +子實體的屬性名稱 List findByAddress_ZipCode(ZipCode zipCode) 表示查詢所有 Address(地址)的zipCode(郵編)為指定值的所有Person(人員) |
3.JPQL查詢
一個類似HQL的語法,在介面上使用@Query標識:
@Query("select a from user a where a.id = ?1")
public User findById(Long id);
使用@Modifying標識修改
@Modifying
@Query("update User a set a.name = ?1 where a.id < ?2")
public int updateName(String name, Long id);
攜帶分頁資訊:
@Query("select u from User u where u.name=?1")
public List<User> findByName(String name, Pageable pageable);
除此之外也可以使用原生sql,只需要@Query(nativeQuery=true)標識即可.
建立查詢順序:
Spring Data JPA 在為介面建立代理物件時,如果發現同時存在多種上述情況可用,它該優先採用哪種策略呢?為此, 提供了 query-lookup-strategy 屬性,用以指定查詢的順序。它有如下三個取值:
|
4.計數
計數就直接使用JpaRepository的count方法:
//查詢總數量
userRepository.count();
User user = new User();
user.setName("小紅");
//條件計數
userRepository.count(Example.of(user));
5.判斷是否存在
計數就直接使用JpaRepository的exists方法:
//根據主鍵判斷是否存在
userRepository.exists(1);
User user = new User();
user.setName("小紅");
//根據條件判斷是否存在
userRepository.exists(Example.of(user));
6.自定義查詢
首先自定義一個介面,用於定義自定義方法,如UserRepositoryCustom,然後讓UserRepository實現該介面,這樣的話就可以使用其中的方法。然後寫UserRepositoryImpl實現UserRepositoryCustom介面。
最後設定jpa:repositories的repository-impl-postfix="Impl",這樣的話JPA會查詢自定義實現類命名規則,這樣的話JPA在相應UserRepository包下面查詢實現類,找到則會使用其中的實現方法,而不去自己實現。具體可以看專案demo,或者下一節的複雜查詢。
5、複雜查詢
1.使用CriteriaBuilder構建JPQL
在UserRepositoryImpl中使用CriteriaBuilder實現根據id查詢,下面是程式碼:
public void findById(Integer id){
//select u from User u where u.id = 1
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class); //from User
cq.select(root); //select * from User
javax.persistence.criteria.Predicate pre = cb.equal(root.get("id").as(Integer.class),id);//id=1
cq.where(pre);//where id=1
Query query = entityManager.createQuery(cq);//select u from User u where u.id = 1
System.out.println(query.getResultList());
}
缺點:
- 程式碼量多
- 不易維護
- 條件複雜的話,則會顯得很混亂.
2.使用JpaSpecificationExecutor查詢
該介面有如下方法,裡面傳入條件都是Specification,該介面會返回一個Predicate條件集合,因此就可以在這裡封裝:
public interface JpaSpecificationExecutor<T> {
T findOne(Specification<T> spec);
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
long count(Specification<T> spec);
}
1.構造過濾條件集合
Operator列舉類裡面的operator屬性為了構建原生sql使用:
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.io.Serializable;
/**
* 篩選
*/
public class Filter implements Serializable {
private static final long serialVersionUID = -8712382358441065075L;
/**
* 運算子
*/
public enum Operator {
/** 等於 */
eq(" = "),
/** 不等於 */
ne(" != "),
/** 大於 */
gt(" > "),
/** 小於 */
lt(" < "),
/** 大於等於 */
ge(" >= "),
/** 小於等於 */
le(" <= "),
/** 類似 */
like(" like "),
/** 包含 */
in(" in "),
/** 為Null */
isNull(" is NULL "),
/** 不為Null */
isNotNull(" is not NULL ");
Operator(String operator) {
this.operator = operator;
}
private String operator;
public String getOperator() {
return operator;
}
public void setOperator(String operator) {
this.operator = operator;
}
}
/** 預設是否忽略大小寫 */
private static final boolean DEFAULT_IGNORE_CASE = false;
/** 屬性 */
private String property;
/** 運算子 */
private Filter.Operator operator;
/** 值 */
private Object value;
/** 是否忽略大小寫 */
private Boolean ignoreCase = DEFAULT_IGNORE_CASE;
/**
* 構造方法
*/
public Filter() {
}
/**
* 構造方法
*
* @param property
* 屬性
* @param operator
* 運算子
* @param value
* 值
*/
public Filter(String property, Filter.Operator operator, Object value) {
this.property = property;
this.operator = operator;
this.value = value;
}
/**
* 構造方法
*
* @param property
* 屬性
* @param operator
* 運算子
* @param value
* 值
* @param ignoreCase
* 忽略大小寫
*/
public Filter(String property, Filter.Operator operator, Object value, boolean ignoreCase) {
this.property = property;
this.operator = operator;
this.value = value;
this.ignoreCase = ignoreCase;
}
/**
* 返回等於篩選
*
* @param property
* 屬性
* @param value
* 值
* @return 等於篩選
*/
public static Filter eq(String property, Object value) {
return new Filter(property, Filter.Operator.eq, value);
}
/**
* 返回等於篩選
*
* @param property
* 屬性
* @param value
* 值
* @param ignoreCase
* 忽略大小寫
* @return 等於篩選
*/
public static Filter eq(String property, Object value, boolean ignoreCase) {
return new Filter(property, Filter.Operator.eq, value, ignoreCase);
}
/**
* 返回不等於篩選
*
* @param property
* 屬性
* @param value
* 值
* @return 不等於篩選
*/
public static Filter ne(String property, Object value) {
return new Filter(property, Filter.Operator.ne, value);
}
/**
* 返回不等於篩選
*
* @param property
* 屬性
* @param value
* 值
* @param ignoreCase
* 忽略大小寫
* @return 不等於篩選
*/
public static Filter ne(String property, Object value, boolean ignoreCase) {
return new Filter(property, Filter.Operator.ne, value, ignoreCase);
}
/**
* 返回大於篩選
*
* @param property
* 屬性
* @param value
* 值
* @return 大於篩選
*/
public static Filter gt(String property, Object value) {
return new Filter(property, Filter.Operator.gt, value);
}
/**
* 返回小於篩選
*
* @param property
* 屬性
* @param value
* 值
* @return 小於篩選
*/
public static Filter lt(String property, Object value) {
return new Filter(property, Filter.Operator.lt, value);
}
/**
* 返回大於等於篩選
*
* @param property
* 屬性
* @param value
* 值
* @return 大於等於篩選
*/
public static Filter ge(String property, Object value) {
return new Filter(property, Filter.Operator.ge, value);
}
/**
* 返回小於等於篩選
*
* @param property
* 屬性
* @param value
* 值
* @return 小於等於篩選
*/
public static Filter le(String property, Object value) {
return new Filter(property, Filter.Operator.le, value);
}
/**
* 返回相似篩選
*
* @param property
* 屬性
* @param value
* 值
* @return 相似篩選
*/
public static Filter like(String property, Object value) {
return new Filter(property, Filter.Operator.like, value);
}
/**
* 返回包含篩選
*
* @param property
* 屬性
* @param value
* 值
* @return 包含篩選
*/
public static Filter in(String property, Object value) {
return new Filter(property, Filter.Operator.in, value);
}
/**
* 返回為Null篩選
*
* @param property
* 屬性
* @return 為Null篩選
*/
public static Filter isNull(String property) {
return new Filter(property, Filter.Operator.isNull, null);
}
/**
* 返回不為Null篩選
*
* @param property
* 屬性
* @return 不為Null篩選
*/
public static Filter isNotNull(String property) {
return new Filter(property, Filter.Operator.isNotNull, null);
}
/**
* 返回忽略大小寫篩選
*
* @return 忽略大小寫篩選
*/
public Filter ignoreCase() {
this.ignoreCase = true;
return this;
}
/**
* 獲取屬性
*
* @return 屬性
*/
public String getProperty() {
return property;
}
/**
* 設定屬性
*
* @param property
* 屬性
*/
public void setProperty(String property) {
this.property = property;
}
/**
* 獲取運算子
*
* @return 運算子
*/
public Filter.Operator getOperator() {
return operator;
}
/**
* 設定運算子
*
* @param operator
* 運算子
*/
public void setOperator(Filter.Operator operator) {
this.operator = operator;
}
/**
* 獲取值
*
* @return 值
*/
public Object getValue() {
return value;
}
/**
* 設定值
*
* @param value
* 值
*/
public void setValue(Object value) {
this.value = value;
}
/**
* 獲取是否忽略大小寫
*
* @return 是否忽略大小寫
*/
public Boolean getIgnoreCase() {
return ignoreCase;
}
/**
* 設定是否忽略大小寫
*
* @param ignoreCase
* 是否忽略大小寫
*/
public void setIgnoreCase(Boolean ignoreCase) {
this.ignoreCase = ignoreCase;
}
/**
* 重寫equals方法
*
* @param obj
* 物件
* @return 是否相等
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
if (this == obj) {
return true;
}
Filter other = (Filter) obj;
return new EqualsBuilder().append(getProperty(), other.getProperty()).append(getOperator(), other.getOperator()).append(getValue(), other.getValue()).isEquals();
}
/**
* 重寫hashCode方法
*
* @return HashCode
*/
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(getProperty()).append(getOperator()).append(getValue()).toHashCode();
}
}
2.構造排序欄位
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.io.Serializable;
/**
* 排序
*
* @author copy from shopxx
*/
public class Order implements Serializable {
private static final long serialVersionUID = -3078342809727773232L;
/**
* 方向
*/
public enum Direction {
/** 遞增 */
asc,
/** 遞減 */
desc
}
/** 預設方向 */
private static final Order.Direction DEFAULT_DIRECTION = Order.Direction.desc;
/** 屬性 */
private String property;
/** 方向 */
private Order.Direction direction = DEFAULT_DIRECTION;
@Override
public String toString() {
return property+" " + direction.name();
}
/**
* 構造方法
*/
public Order() {
}
/**
* 構造方法
*
* @param property
* 屬性
* @param direction
* 方向
*/
public Order(String property, Order.Direction direction) {
this.property = property;
this.direction = direction;
}
/**
* 返回遞增排序
*
* @param property
* 屬性
* @return 遞增排序
*/
public static Order asc(String property) {
return new Order(property, Order.Direction.asc);
}
/**
* 返回遞減排序
*
* @param property
* 屬性
* @return 遞減排序
*/
public static Order desc(String property) {
return new Order(property, Order.Direction.desc);
}
/**
* 獲取屬性
*
* @return 屬性
*/
public String getProperty() {
return property;
}
/**
* 設定屬性
*
* @param property
* 屬性
*/
public void setProperty(String property) {
this.property = property;
}
/**
* 獲取方向
*
* @return 方向
*/
public Order.Direction getDirection() {
return direction;
}
/**
* 設定方向
*
* @param direction
* 方向
*/
public void setDirection(Order.Direction direction) {
this.direction = direction;
}
/**
* 重寫equals方法
*
* @param obj
* 物件
* @return 是否相等
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
if (this == obj) {
return true;
}
Order other = (Order) obj;
return new EqualsBuilder().append(getProperty(), other.getProperty()).append(getDirection(), other.getDirection()).isEquals();
}
/**
* 重寫hashCode方法
*
* @return HashCode
*/
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(getProperty()).append(getDirection()).toHashCode();
}
}
3.查詢語句生成
1.基本框架
/**
* 封裝查詢條件的實體
*/
public class QueryParams<T> implements Specification<T> {
/** 屬性分隔符 */
private static final String PROPERTY_SEPARATOR = ".";
/**
* and條件
*/
private List<Filter> andFilters = new ArrayList<>();
/**
* or條件
*/
private List<Filter> orFilters = new ArrayList<>();
/**
* 排序屬性
*/
private List<Order> orders = new ArrayList<>();
/**
* 獲取Path
*
* @param path
* Path
* @param propertyPath
* 屬性路徑
* @return Path
*/
@SuppressWarnings("unchecked")
private <X> Path<X> getPath(Path<?> path, String propertyPath) {
if (path == null || StringUtils.isEmpty(propertyPath)) {
return (Path<X>) path;
}
String property = StringUtils.substringBefore(propertyPath, PROPERTY_SEPARATOR);
return getPath(path.get(property), StringUtils.substringAfter(propertyPath, PROPERTY_SEPARATOR));
}
}
2.分析and條件
/**
* 轉換為Predicate
*/
@SuppressWarnings("unchecked")
private Predicate toAndPredicate(Root<T> root,CriteriaBuilder criteriaBuilder) {
Predicate restrictions = criteriaBuilder.conjunction();
if (root == null || CollectionUtils.isEmpty(andFilters)) {
return restrictions;
}
for (Filter filter : andFilters) {
if (filter == null) {
continue;
}
String property = filter.getProperty();
Filter.Operator operator = filter.getOperator();
Object value = filter.getValue();
Boolean ignoreCase = filter.getIgnoreCase();
Path<?> path = getPath(root, property);
if (path == null) {
continue;
}
//根據運算子生成相應條件
switch (operator) {
case eq:
if (value != null) {
if (BooleanUtils.isTrue(ignoreCase) && String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.equal(criteriaBuilder.lower((Path<String>) path), ((String) value).toLowerCase()));
} else {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.equal(path, value));
}
} else {
restrictions = criteriaBuilder.and(restrictions, path.isNull());
}
break;
case ne:
if (value != null) {
if (BooleanUtils.isTrue(ignoreCase) && String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.notEqual(criteriaBuilder.lower((Path<String>) path), ((String) value).toLowerCase()));
} else {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.notEqual(path, value));
}
} else {
restrictions = criteriaBuilder.and(restrictions, path.isNotNull());
}
break;
case gt:
if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.gt((Path<Number>) path, (Number) value));
}
break;
case lt:
if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.lt((Path<Number>) path, (Number) value));
}
break;
case ge:
if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.ge((Path<Number>) path, (Number) value));
}
break;
case le:
if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.le((Path<Number>) path, (Number) value));
}
break;
case like:
if (String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
if (BooleanUtils.isTrue(ignoreCase)) {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.like(criteriaBuilder.lower((Path<String>) path), ((String) value).toLowerCase()));
} else {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.like((Path<String>) path, (String) value));
}
}
break;
case in:
restrictions = criteriaBuilder.and(restrictions, path.in(value));
break;
case isNull:
restrictions = criteriaBuilder.and(restrictions, path.isNull());
break;
case isNotNull:
restrictions = criteriaBuilder.and(restrictions, path.isNotNull());
break;
}
}
return restrictions;
}
3.分析or條件
把and中的and改為or即可:
/**
* 轉換為Predicate
*/
@SuppressWarnings("unchecked")
private Predicate toOrPredicate(Root<T> root,CriteriaBuilder criteriaBuilder) {
Predicate restrictions = criteriaBuilder.disjunction();
if (root == null || CollectionUtils.isEmpty(andFilters)) {
return restrictions;
}
for (Filter filter : orFilters) {
if (filter == null) {
continue;
}
String property = filter.getProperty();
Filter.Operator operator = filter.getOperator();
Object value = filter.getValue();
Boolean ignoreCase = filter.getIgnoreCase();
Path<?> path = getPath(root, property);
if (path == null) {
continue;
}
switch (operator) {
case eq:
if (value != null) {
if (BooleanUtils.isTrue(ignoreCase) && String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.equal(criteriaBuilder.lower((Path<String>) path), ((String) value).toLowerCase()));
} else {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.equal(path, value));
}
} else {
restrictions = criteriaBuilder.or(restrictions, path.isNull());
}
break;
case ne:
if (value != null) {
if (BooleanUtils.isTrue(ignoreCase) && String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.notEqual(criteriaBuilder.lower((Path<String>) path), ((String) value).toLowerCase()));
} else {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.notEqual(path, value));
}
} else {
restrictions = criteriaBuilder.or(restrictions, path.isNotNull());
}
break;
case gt:
if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.gt((Path<Number>) path, (Number) value));
}
break;
case lt:
if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.lt((Path<Number>) path, (Number) value));
}
break;
case ge:
if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.ge((Path<Number>) path, (Number) value));
}
break;
case le:
if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.le((Path<Number>) path, (Number) value));
}
break;
case like:
if (String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
if (BooleanUtils.isTrue(ignoreCase)) {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.like(criteriaBuilder.lower((Path<String>) path), ((String) value).toLowerCase()));
} else {
restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.like((Path<String>) path, (String) value));
}
}
break;
case in:
restrictions = criteriaBuilder.or(restrictions, path.in(value));
break;
case isNull:
restrictions = criteriaBuilder.or(restrictions, path.isNull());
break;
case isNotNull:
restrictions = criteriaBuilder.or(restrictions, path.isNotNull());
break;
}
}
return restrictions;
}
4.分析排序條件
/**
* 轉換為Order
*/
private List<javax.persistence.criteria.Order> toOrders(Root<T> root,CriteriaBuilder criteriaBuilder) {
List<javax.persistence.criteria.Order> orderList = new ArrayList<javax.persistence.criteria.Order>();
if (root == null || CollectionUtils.isEmpty(orders)) {
return orderList;
}
for (Order order : orders) {
if (order == null) {
continue;
}
String property = order.getProperty();
Order.Direction direction = order.getDirection();
Path<?> path = getPath(root, property);
if (path == null || direction == null) {
continue;
}
switch (direction) {
case asc:
orderList.add(criteriaBuilder.asc(path));
break;
case desc:
orderList.add(criteriaBuilder.desc(path));
break;
}
}
return orderList;
}
最後在toPredicate方法中構造最終條件:
/**
* 生成條件的
* @param root 該物件的封裝
* @param query 查詢構建器
* @param cb 構建器
* @return 條件集合
*/
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate restrictions = cb.and(toAndPredicate(root,cb));
restrictions = cb.and(restrictions,toOrPredicate(root,cb));
query.orderBy(toOrders(root,cb));
return restrictions;
}
加上方便的鏈式呼叫方法:
/**
* 新增一個and條件
* @param filter 該條件
* @return 鏈式呼叫
*/
public QueryParams and(Filter filter){
this.andFilters.add(filter);
return this;
}
/**
* 新增多個and條件
* @param filter 該條件
* @return 鏈式呼叫
*/
public QueryParams and(Filter ...filter){
this.andFilters.addAll(Arrays.asList(filter));
return this;
}
/**
* 新增一個or條件
* @param filter 該條件
* @return 鏈式呼叫
*/
public QueryParams or(Filter filter){
this.orFilters.add(filter);
return this;
}
/**
* 新增多個or條件
* @param filter 該條件
* @return 鏈式呼叫
*/
public QueryParams or(Filter ...filter){
this.orFilters.addAll(Arrays.asList(filter));
return this;
}
/**
* 升序欄位
* @param property 該欄位對應變數名
* @return 鏈式呼叫
*/
public QueryParams orderASC(String property){
this.orders.add(Order.asc(property));
return this;
}
/**
* 降序欄位
* @param property 該欄位對應變數名
* @return 鏈式呼叫
*/
public QueryParams orderDESC(String property){
this.orders.add(Order.desc(property));
return this;
}
/**
* 清除所有條件
* @return 該例項
*/
public QueryParams clearAll(){
if (!this.andFilters.isEmpty()) this.andFilters.clear();
if (!this.orFilters.isEmpty()) this.orFilters.clear();
if (!this.orders.isEmpty()) this.orders.clear();
return this;
}
/**
* 清除and條件
* @return 該例項
*/
public QueryParams clearAnd(){
if (!this.andFilters.isEmpty()) this.andFilters.clear();
return this;
}
/**
* 清除or條件
* @return 該例項
*/
public QueryParams clearOr(){
if (!this.orFilters.isEmpty()) this.andFilters.clear();
return this;
}
/**
* 清除order條件
* @return 該例項
*/
public QueryParams clearOrder(){
if (!this.orders.isEmpty()) this.orders.clear();
return this;
}
//省略get和set方法
4.測試
首先讓PcardOrderRepository介面繼承加上JpaSpecificationExecutor:
public interface PcardOrderRepository extends JpaRepository<PcardOrder,String>,PcardOrderRepositoryCustom,JpaSpecificationExecutor<PcardOrder> {
}
編寫測試程式碼,這個使用的是CriteriaBuilder構建查詢的,所以查詢欄位都是JPQL欄位,並不是原生sql:
QueryParams<PcardOrder> queryParams = new QueryParams<>();
//使用Specification條件查詢,使用JPQL欄位查詢
queryParams
.and(Filter.eq("acctId","0014779934917371041"),Filter.ne("orderAmt",0L),
Filter.eq("orderRespCd","00"))
.or(Filter.eq("orderTypeId","A003"),Filter.eq("orderTypeId","A007"),
Filter.eq("orderTypeId","A021"),Filter.eq("orderTypeId","A018"))
.orderDESC("createTime");
Page<PcardOrder> JPQLlist = pcardOrderRepository.findAll(queryParams,new PageRequest(0,2));
//構造出來的條件
where
1=1
and pcardorder0_.acct_id=?
and pcardorder0_.order_amt<>0
and pcardorder0_.order_resp_cd=?
and (
0=1
or pcardorder0_.order_type_id=?
or pcardorder0_.order_type_id=?
or pcardorder0_.order_type_id=?
or pcardorder0_.order_type_id=?
)
order by
pcardorder0_.create_time desc limit ?
3.原生sql查詢
還是利用上面的Filter,具體還是遍歷+拼接,示例中我解除安裝了公共方法中,需要使用的Impl直接extends即可。
1.解析條件
解析條件實際上就是拼接sql,程式碼很簡單:
/**
* 公共方法的repository
*/
@NoRepositoryBean
public class BaseRepository {
private static Logger logger = LoggerFactory.getLogger(BaseRepository.class);
/**
* 分析查詢引數,並且合併到sql語句中
* @param sql JPQL查詢語句
* @param params 查詢引數
* @return 引數對應的value
*/
@SuppressWarnings("Unchecked")
protected List<Object> analysisQueryParams(StringBuilder sql, QueryParams<?> params){
List<String> strList = new ArrayList<>();
List<Object> valueList = new ArrayList<>();
int i = 1;
//分析or條件
for (Filter filter : params.getOrFilters()) {
if (filter.getValue() != null){
strList.add(filter.getProperty()+" " + filter.getOperator().getOperator()+" ?" + (i++));
valueList.add(filter.getValue());
}else {
strList.add(filter.getProperty()+" " + filter.getOperator().getOperator()+" ");
}
}
if (!strList.isEmpty()){
sql.append(" and ").append("( ").append(StringUtils.join(strList," or ")).append(" )");
}
strList.clear();
//分析and條件
for (Filter filter : params.getAndFilters()) {
if (filter.getValue() != null){
strList.add(filter.getProperty()+" " + filter.getOperator().getOperator()+" ?" + (i++));
valueList.add(filter.getValue());
}else {
strList.add(filter.getProperty()+" " + filter.getOperator().getOperator()+" ");
}
}
sql.append(" and ").append(StringUtils.join(strList," and "));
//分析排序欄位
if (!params.getOrders().isEmpty()){
sql.append(" order by ");
sql.append(StringUtils.join(params.getOrders(),","));
}
logger.debug("解析後的sql:"+sql.toString());
logger.debug("對應的值為:"+valueList);
return valueList;
}
}
2.測試
在自定義介面中加入方法:
public interface PcardOrderRepositoryCustom {
List findByQueryParam(QueryParams queryParams, Pageable pageable);
}
然後實現類中需要寫部分sql
@NoRepositoryBean
public class PcardOrderRepositoryImpl extends BaseRepository implements PcardOrderRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List findByQueryParam(QueryParams queryParams, Pageable pageable) {
StringBuilder sql = new StringBuilder("select * from tbl_pcard_order where 1=1 ");
List values = analysisQueryParams(sql,queryParams);
Query query = entityManager.createNativeQuery(sql.toString());
for (int i = 0; i < values.size(); i++) {
query.setParameter(i+1,values.get(i));
}
query.setFirstResult(pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
return query.getResultList();
}
}
測試程式碼:
//使用原生sql查詢,注意這裡使用原生sql欄位,並非JPQL欄位,
//本質是根據BaseRepository.analysisQueryParams 來拼接條件,可以根據自己的需求更改
queryParams.clearAll()
.and(Filter.eq("acct_id","0014779934917371041"),Filter.ne("order_amt",0L),
Filter.eq("order_resp_cd","00"))
.or(Filter.eq("order_type_id","A003"),Filter.eq("order_type_id","A007"),
Filter.eq("order_type_id","A021"),Filter.eq("order_type_id","A018"))
.orderDESC("create_time");
List nativeSqlList = pcardOrderRepository.findByQueryParam(queryParams,new PageRequest(0,2));
//構造出來的sql
where
1=1
and (
order_type_id = ?
or order_type_id = ?
or order_type_id = ?
or order_type_id = ?
)
and acct_id = ?
and order_amt != ?
and order_resp_cd = ?
order by
create_time desc limit ?
4.使用原生sql查詢出Map集合
使用原生sql進行表關聯查詢,返回值一般都用List:
public List<Object[]> findById(int id) {
String sql = "select u.*,c.* from user u,commont c where u.id = c.id and id=?1";
Query query = entityManager.createNativeQuery(sql);
query.setParameter(1,id);
return query.getResultList();
}
那麼就要改進,使其返回Map:
public List findById (int id) {
String sql = "select u.*,c.* from user u,commont c where u.id = c.id and id=?1";
Query query = entityManager.createNativeQuery(sql);
query.setParameter(1,id);
//轉換為Map集合
query.unwrap(org.hibernate.SQLQuery.class).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
return query.getResultList();
}
//實際上返回的是一個List<Map>集合
這樣的返回值是一個List型別,取出的時候直接根據鍵值取即可。
四、專案實戰
1、基本配置
// buildscript 程式碼塊中指令碼優先執行
buildscript {
// ext 用於定義動態屬性
ext {
springBootVersion = '1.5.2.RELEASE'
}
// 自定義 Thymeleaf 和 Thymeleaf Layout Dialect 的版本
ext['thymeleaf.version'] = '3.0.3.RELEASE'
ext['thymeleaf-layout-dialect.version'] = '2.2.0'
// 自定義 Hibernate 的版本
ext['hibernate.version'] = '5.2.8.Final'
// 使用了 Maven 的中央倉庫(你也可以指定其他倉庫)
repositories {
//mavenCentral()
maven {
url 'http://maven.aliyun.com/nexus/content/groups/public/'
}
}
// 依賴關係
dependencies {
// classpath 宣告說明了在執行其餘的指令碼時,ClassLoader 可以使用這些依賴項
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// 使用外掛
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
// 打包的型別為 jar,並指定了生成的打包的檔名稱和版本
jar {
baseName = 'jpa-in-action'
version = '1.0.0'
}
// 指定編譯 .java 檔案的 JDK 版本
sourceCompatibility = 1.8
// 預設使用了 Maven 的中央倉庫。這裡改用自定義的映象庫
repositories {
//mavenCentral()
maven {
url 'http://maven.aliyun.com/nexus/content/groups/public/'
}
}
// 依賴關係
dependencies {
// 該依賴對於編譯發行是必須的
compile('org.springframework.boot:spring-boot-starter-web')
// 新增 Thymeleaf 的依賴
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
// 新增 Spring Data JPA 的依賴
compile('org.springframework.boot:spring-boot-starter-data-jpa')
// 新增 MySQL連線驅動 的依賴
compile('mysql:mysql-connector-java:6.0.5')
// 該依賴對於編譯測試是必須的,預設包含編譯產品依賴和編譯時依
testCompile('org.springframework.boot:spring-boot-starter-test')
//新增H2的依賴
runtime('com.h2database:h2:1.4.193')
}
然後application.properties為:
# THYMELEAF
spring.thymeleaf.encoding=UTF-8
# 熱部署靜態檔案
spring.thymeleaf.cache=false
# 使用HTML5標準
spring.thymeleaf.mode=HTML5
#使用H2 控制檯 訪問頁面http://localhost:8080/h2-console/,
spring.h2.console.enabled=true
# DataSource 測試時可以先不用mysql資料庫
#spring.datasource.url=jdbc:mysql://localhost/blog?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
#spring.datasource.username=root
#spring.datasource.password=123456
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=create-drop
注意:在測試階段可以使用H2資料庫,其管理頁面如下(http://localhost:8080/h2-console/ ),預設的資料庫為jdbc:h2:mem:testdb,如果啟用mysql,把mysql註釋放開即可。
2、實體改造
@Entity // 實體
public class User implements Serializable{
private static final long serialVersionUID = 1L;
@Id // 主鍵
@GeneratedValue(strategy=GenerationType.IDENTITY) // 自增長策略
private Long id; // 使用者的唯一標識
@Column(nullable = false) // 對映為欄位,值不能為空
private String name;
@Column(nullable = false)
private Integer age;
@Override
public String toString() {
return String.format(
"User[id=%d, name='%s', age='%d']",
id, name, age);
}
}
3、持久化層改造
介面改成如下內容,實現類刪除,用spring 提供的
/**
* 使用者倉庫.
*/
public interface UserRepository extends CrudRepository<User, Long>{
}
4、控制層改造
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserRepository userRepository;
/**
* 從 使用者儲存庫 獲取使用者列表
* @return
*/
private List<User> getUserlist() {
List<User> users = new ArrayList<>();
for (User user : userRepository.findAll()) {
users.add(user);
}
return users;
}
/**
* 查詢所用使用者
* @return
*/
@GetMapping
public ModelAndView list(Model model) {
model.addAttribute("userList", getUserlist());
model.addAttribute("title", "使用者管理");
return new ModelAndView("users/list", "userModel", model);
}
/**
* 根據id查詢使用者
* @param message
* @return
*/
@GetMapping("{id}")
public ModelAndView view(@PathVariable("id") Long id, Model model) {
User user = userRepository.findOne(id);
model.addAttribute("user", user);
model.addAttribute("title", "檢視使用者");
return new ModelAndView("users/view", "userModel", model);
}
/**
* 獲取 form 表單頁面
* @param user
* @return
*/
@GetMapping("/form")
public ModelAndView createForm(Model model) {
model.addAttribute("user", new User(null, null));
model.addAttribute("title", "建立使用者");
return new ModelAndView("users/form", "userModel", model);
}
/**
* 新建使用者
* @param user
* @param result
* @param redirect
* @return
*/
@PostMapping
public ModelAndView create(User user) {
userRepository.save(user);
return new ModelAndView("redirect:/users");
}
/**
* 刪除使用者
* @param id
* @return
*/
@GetMapping(value = "delete/{id}")
public ModelAndView delete(@PathVariable("id") Long id, Model model) {
userRepository.delete(id);
model.addAttribute("userList", getUserlist());
model.addAttribute("title", "刪除使用者");
return new ModelAndView("users/list", "userModel", model);
}
/**
* 修改使用者
* @param user
* @return
*/
@GetMapping(value = "modify/{id}")
public ModelAndView modifyForm(@PathVariable("id") Long id, Model model) {
User user = user