Spring
項目中,咱們使用JPA
進行查詢,只需簡單地繼承SpringData
提供的接口便可實現強大的數據查詢功能。java
以前的強檢器具統計管理用的僅僅是單表查詢,理解不深,此次開發的考評員綜合查詢涉及到了多個實體間的查詢,值得學習,特此記錄。架構
如下是ER
圖。ide
管理部門選擇本部門或其管轄的部門中的某人員做爲該部門的考評人員。根據區域、部門、學科類別查詢出符合條件的考評人員。學習
複雜查詢,咱們就須要SpringData
提供的JpaSpecificationExecutor
接口,這是該接口的源代碼。ui
public interface JpaSpecificationExecutor<T> { /** * Returns a single entity matching the given {@link Specification}. * * @param spec * @return */ T findOne(Specification<T> spec); /** * Returns all entities matching the given {@link Specification}. * * @param spec * @return */ List<T> findAll(Specification<T> spec); /** * Returns a {@link Page} of entities matching the given {@link Specification}. * * @param spec * @param pageable * @return */ Page<T> findAll(Specification<T> spec, Pageable pageable); /** * Returns all entities matching the given {@link Specification} and {@link Sort}. * * @param spec * @param sort * @return */ List<T> findAll(Specification<T> spec, Sort sort); /** * Returns the number of instances that the given {@link Specification} will return. * * @param spec the {@link Specification} to count instances for * @return the number of instances */ long count(Specification<T> spec); }
該接口中聲明的方法都有一個共同的參數:Specification
,翻譯過來就是規範的意思。this
這是Specification
接口源代碼。spa
public interface Specification<T> { /** * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given * {@link Root} and {@link CriteriaQuery}. * * @param root * @param query * @return a {@link Predicate}, must not be {@literal null}. */ Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb); }
Specification
接口中有一個toPredicate
方法,看不懂代碼咱們能夠看註釋。翻譯
Creates a WHERE clause for a query of the referenced entity in form of a {Predicate}
用謂詞的形式爲引用的實體建立一個WHERE語句。debug
到這裏,咱們應該明白了這個查詢的原理:咱們先生成咱們須要的謂語,而後用toPredicate
將謂語生成爲where
語句,最後再使用咱們Specification
類型的where
語句做爲參數用JpaSpecificationExecutor
接口中定義的方法進行查詢。日誌
JpaSpecificationExecutor
接口public interface ExaminerRepository extends CrudRepository<Examiner, Long>, JpaSpecificationExecutor { }
咱們的考評員倉庫原繼承CrudRepository
,爲了實現能夠複雜查詢,繼承JpaSpecificationExecutor
接口。
爲了規範,咱們建了一個spec
的查詢規格的包,將全部生成查詢規格的類放入該包中。
/** * @author zhangxishuo on 2018/5/26 * 考評人員查詢規格 */ public class ExaminerSpecs { // 日誌 private static final Logger logger = Logger.getLogger(ExaminerSpecs.class); public static Specification<Examiner> base(Department userDepartment, final Map<String, Object> map) { return new Specification<Examiner>() { @Override public Predicate toPredicate(Root<Examiner> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { return null; } }; } }
咱們在這個類中寫了一個base
方法,用於根據傳入的參數生成一個查詢規格。
return
語句中,咱們new
了一個接口,而後在new
這個接口的同時對這個接口進行實現,實現接口中的toPredicate
方法。
toPredicate
中的三個參數意義。Root
:查詢的模型;CriteriaQuery
:標準查詢,用於查詢的;CriteriaBuilder
:標準構建,用戶構建查詢語句的。
你們不要去糾結是直接new
接口而後在花括號中實現仍是寫一個類實現接口而後再去new
?
這裏說一下我本身的理解。其實這主要是看實際須要,若是這個接口的實現類須要被好多個其餘類調用,爲了代碼的複用,咱們就會寫一個類去實現接口,就像咱們後臺MVC
架構的業務邏輯層,由於同一個方法可能會被好多個控制器進行調用,因此咱們建一個Service
接口,再創建一個Service
接口實現類,而後其餘類須要時進行Autowire
。
若是咱們的方法不須要複用,那Android
代碼就是咱們最好的例子(Android
中大量使用了new
接口而後在new
時去實現的寫法)。
public class MyActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.content_layout_id); final Button button = findViewById(R.id.button_id); button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Code here executes on main thread after user presses button } }); } }
這是Android
官方文檔中對按鈕添加點擊事件的示例代碼,咱們看到這裏的View.OnClickListener()
就是在new
的時候再去實現的。其實這也不難理解,打開咱們天天用的APP
,幾乎每一個按鈕,每一個視圖的功能都是不同的,因此不須要進行代碼複用。
謂語是什麼呢?按個人理解,一個謂語就是一個條件。
logger.debug("構建謂語,人員的部門的區域的id等於map中區域的id"); Predicate districtPredicate = criteriaBuilder .equal(root.join("qualifier") .join("department") .join("district") .get("id") .as(Long.class), ((District) map.get("district")) .getId());
咱們使用criteriaBuilder
的equal
方法構建了一個等於的條件(或謂語),該條件要求考評員(root
)的人員的部門的區域的id
轉化爲Long
類型的結果與傳入的區域的id
相等。
相似的寫法,咱們能夠構建出部門和學科的謂語。
謂語建立完了,咱們還須要將這些謂語進行鏈接。
這裏咱們須要用到and
條件,即區域符合且部門符合且學科類別符合。
爲了方便構建,這裏參考以前的項目代碼構建了一個用and
拼接謂語的方法。
private Predicate predicate = null; private CriteriaBuilder criteriaBuilder; // 設置and謂語.注意,這裏只能設置and關係的謂語,若是謂語爲OR,則須要手動設置 private void andPredicate(Predicate predicate) { // 若是傳入的謂語不爲空 if (null != predicate) { if (null == this.predicate) { // 若是該方法以前沒有謂語,則直接賦值 this.predicate = predicate; } else { // 若是以前有謂語。則使用criteriaBuilder的與將已有謂語和新謂語用and鏈接 this.predicate = this.criteriaBuilder.and(this.predicate, predicate); } } }
這幾行代碼就體現了一名優秀軟件工程師的素養,優秀的人在寫代碼以前就能碰見哪些功能是重複的,不需實現以後再將相同代碼分離重構。
logger.debug("用and鏈接該謂語"); this.andPredicate(districtPredicate);
CriteriaQuery
若是咱們只是單純的查詢的話,沒有什麼特殊要求的話,那咱們直接就能夠把咱們的謂語返回。
return this.predicate;
沒錯,直接將謂語返回,debug
的時候發現實際查詢時會獲取咱們的謂語,並執行query.where
語句。
若是須要複雜功能的話,可使用CriteriaQuery
。
// 把Predicate應用到CriteriaQuery中去,能夠實現更豐富的查詢功能,能夠分組,排序等 criteriaQuery.where(this.predicate); return criteriaQuery.getRestriction();
基礎工做完成了,咱們終於可使用咱們的謂語進行查詢啦。
/** * 查詢符合條件的考評員信息 * @param district 區域 * @param department 部門 * @param discipline 學科類別 * @return 符合條件的考評員信息 */ @Override public List<Examiner> getAllExaminerInfo(District district, Department department, Discipline discipline) { logger.debug("新建Map並設置屬性"); Map<String, Object> map = new HashMap<>(); map.put("department", department); map.put("district", district); map.put("discipline", discipline); logger.debug("調用根據Map的查詢方法"); return this.getExaminerInfoByMap(map); } /** * 根據Map查詢考評員的信息 * @param map 考評員查詢Map * @return 考評員列表 */ private List<Examiner> getExaminerInfoByMap(Map<String, Object> map) { logger.debug("獲取當前登陸用戶"); User currentLoginUser = userService.getCurrentLoginUser(); logger.debug("獲取查詢謂語"); Specification<Examiner> specification = ExaminerSpecs.base(currentLoginUser.getDepartment(), map); logger.debug("查詢並返回"); return (List<Examiner>) examinerRepository.findAll(specification); }
怎麼樣,謂語拼接完以後是否是很簡單呢?