在五年前,同人曾經發表過〈永續物件查詢之設計〉。當年我分別以其一和其二兩篇分享應用在 Hibernate 框架的動態物件條件查詢的實作,而最近同人在工作上應用 Hibernate-Jpa 又實作了另一套動態條件查詢的元件。這兩者不一樣的是,不像五年前需要定義查詢條件表示式的語法結構,現在我應用的設計樣式更簡單,捨棄了較複雜的 interpreter pattern,而單單利用延遲運算的概念來結合 builder pattern 與 strategy pattern。
我們先來看看使用方式的差異吧!先前的使用案例是這樣的:
context.setValue("genderParm", "male"); List boList = dao.find( QueryCriteria.from(BizObject.class). where(Relation.and() .add(ComparisonExpression.like( new PropertyExpression("name"), ConstantExpression.getString("abc%"))) .add(ComparisonExpression.eq( new PropertyExpression.property("gender"), new ParameterExpression("genderParm"))), context));
這一段程式碼,現在我們想要改用更簡單的寫法,也就是:
List<BizObject> boList = Biz.forJpql( "SELECT bo FROM BizObject bo") .add(QueryComparsion.like( "bo.name", JpaQueryParam.NAME), "abc%") .add(QueryComparsion.equals( "bo.gender", JpaQueryParam.GENDER), "male") .getResultList(entityManager, BizObject.class, AllQueryComparsionJoint.getInstance());
以上看起來更簡單了,不需要定義條件查詢的語境 Context,也不用埋首在複雜的查詢語法的結構當中。其中 QueryComparsion 定義了我們查詢條件會使用的運算式,就像下面這一段程式所示:
public class QueryComparsion { private ComparsionOperator operator; private String field; private JpaQueryParam param; private QueryComparsion(ComparsionOperator operator, String field, JpaQueryParam param) { this.operator = operator; this.field = field; this.param = param; } public static QueryComparsion like(String field, JpaQueryParam param) { return new QueryComparsion( ComparsionOperator.LIKE, field, param); } public static QueryComparsion equals(String field, JpaQueryParam param) { return new QueryComparsion( ComparsionOperator.EQ, field, param); } public ComparsionOperator getOperator() { return operator; } public String getField() { return field; } public JpaQueryParam getParam() { return param; } } public enum ComparsionOperator { EQ(" = "), LIKE(" LIKE "); private String sign; ComparsionOperator(String sign) { this.sign = sign; } public String sign() { return sign; } }
然後,利用延遲運算的概念,實作 QueryCriteriaBuilder 就變得非常自然,絲毫沒有複雜或困難的地方。
public class QueryCriteriaBuilder { private List<QueryComparsion> comparsions = new ArrayList<QueryComparsion>(); private Map<JpaQueryParam, Object> values = new HashMap<JpaQueryParam, Object>(); private String ql; public QueryCriteriaBuilder(String ql) { this.ql = ql; } public QueryCriteriaBuilder add( QueryComparsion comparsion, Object value) { if (null != value) { comparsions.add(comparsion); values.put(comparsion.getParam(), value); } return this; } public static QueryCriteriaBuilder forJpql(String ql) { return new QueryCriteriaBuilder(ql); } public <T> List<T> getResultList( EntityManager entityManager, Class<T> clazz, QueryComparsionJoint comparsionJoint) { try { Query q = entityManager.createQuery( comparsionJoint.getJpql(ql, comparsions)); for (QueryComparsion comparsion: comparsions) { q.setParameter(comparsion.getParam().name(), values.get(comparsion.getParam())); } @SuppressWarnings("unchecked") List<T> resultList = (List<T>) q.getResultList(); return resultList; } catch (Exception e) { return null; } } } public interface QueryComparsionJoint { public String getJpql(String ql, List<QueryComparsion> comparsions); }
最後,我們需要實作 AllQueryComparsionJoint,它指的是用 AND 來連接每一個的條件比較式。當然,實際的運用可能也會需要 AnyQueryComparsionJoint,或是其它特定的關係運算邏輯。把它們封裝起來成為策略物件正是遵循「把會變動的地方封裝起來」的設計原則,這樣就可以不受查詢條件表示的語法結構的框限。
public class AllQueryComparsionJoint implements QueryComparsionJoint { private static final char CRITERIA_CONCATENATOR = ' '; private static final String WHERE = "WHERE "; private static final Object AND = " AND "; private static QueryComparsionJoint instance = null; private AllQueryComparsionJoint() { super(); } @Override public String getJpql(String ql, List<QueryComparsion> comparsions) { StringBuilder qlBuilder = new StringBuilder(ql); String criteria = getCriteria(comparsions); if (StringUtils.hasText(criteria)) { qlBuilder .append(CRITERIA_CONCATENATOR) .append(criteria); } return qlBuilder.toString(); } private String getCriteria( List<QueryComparsion> comparsions) { Iterator<QueryComparsion> comparsionIterator = comparsions.iterator(); StringBuilder criteriaBuilder = new StringBuilder(); while (comparsionIterator.hasNext()) { QueryComparsion comparsion = comparsionIterator.next(); if (criteriaBuilder.length() == 0) { criteriaBuilder.append(WHERE); } else { criteriaBuilder.append(AND); } criteriaBuilder .append(comparsion.getField()) .append(comparsion.getOperator().sign()) .append(comparsion.getParam().getExpression()); } return criteriaBuilder.toString(); } public static QueryComparsionJoint getInstance() { if (null == instance) { instance = new AllQueryComparsionJoint(); } return instance; } }
這個實作和同人之前分享很多有關延遲運算的實作一樣,都有去除一連串或巢狀 if 的效果。同人在臉書上,曾經和光正兄討論過這些議題,剛好可以藉這篇文章分享延遲運算應用到的各種設計樣式。
一般來說,程式可以寫成一連串或是巢狀的 if 敍述,那就代表程式流程可以用狀態變數來呈現,也就是可以表示成有限狀態機的形式,可以用 interpreter 或是 state pattern 來解決甚至是用表格檢索的方式來表現。但我們一般都不會遇到需要那麼複雜的情況,主要是因為那是框架需要解決的問題,比如說程式語言的 Compiler 或是如 Hibernate 的 HQL、JPA 的 JPQL。
對其它我們會碰到沒那麼複雜的情況,同人的經驗顯示有二種不同解決方式:第一種就是上一篇流程元件化提到的應用 builder pattern 或是更早提到訊息拆解組合應用 visitor pattern,建立一個解決問題過程的 context 脈絡,把答案組合出來,有時候問題比較簡單時,也可能只需要像本篇文章提到只需要應用 strategy pattern 就可以了、另一種方式則是利用泛函編程的高階函式,建構出解決問題的表示式,然後再讓函式一層層套疊的方式來求解,同人不久前分享的語言整合查詢就是這種解法的代表。
第二種解決方式無疑是比較複雜的,同人有想過用高階函式來實作流程元件化,但顯然在有時間壓力的情況下,這樣做是找自己麻煩,還不如用 builder pattern 就可以達到目的。至於何時會用到 visitor 呢?當你處理問題的結構元素是固定而行為是變化的,那就會傾向用 visitor pattern 來設計解決方案。例如拆解組合訊息的欄位就那幾種,而它們都有拆解或組合兩種實作,所以很適合應用 visitor 來尋訪結構。
不過,最後同人還是要強調一點,千萬別為了設計樣式而應用設計樣式。同人開發這些實作都是先從使用案例的陳述開始,都是在架構大致底定之時,讓設計樣式自己來敲門報到的。以上經驗分享只是提供一個參考,樣式的關鍵真的是要我們去思考,我們想要解決什麼問題、以及為什麼要解決這個問題,切忌把樣式硬套在需求上呀。