在五年前,同人曾經發表過〈永續物件查詢之設計〉。當年我分別以其一和其二兩篇分享應用在 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 來尋訪結構。
不過,最後同人還是要強調一點,千萬別為了設計樣式而應用設計樣式。同人開發這些實作都是先從使用案例的陳述開始,都是在架構大致底定之時,讓設計樣式自己來敲門報到的。以上經驗分享只是提供一個參考,樣式的關鍵真的是要我們去思考,我們想要解決什麼問題、以及為什麼要解決這個問題,切忌把樣式硬套在需求上呀。

