更簡單的條件查詢設計

在五年前,同人曾經發表過〈永續物件查詢之設計〉。當年我分別以其一其二兩篇分享應用在 Hibernate 框架的動態物件條件查詢的實作,而最近同人在工作上應用 Hibernate-Jpa 又實作了另一套動態條件查詢的元件。這兩者不一樣的是,不像五年前需要定義查詢條件表示式的語法結構,現在我應用的設計樣式更簡單,捨棄了較複雜的 interpreter pattern,而單單利用延遲運算的概念來結合 builder patternstrategy 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 或是如 HibernateHQLJPAJPQL

對其它我們會碰到沒那麼複雜的情況,同人的經驗顯示有二種不同解決方式:第一種就是上一篇流程元件化提到的應用 builder pattern 或是更早提到訊息拆解組合應用 visitor pattern,建立一個解決問題過程的 context 脈絡,把答案組合出來,有時候問題比較簡單時,也可能只需要像本篇文章提到只需要應用 strategy pattern 就可以了、另一種方式則是利用泛函編程高階函式,建構出解決問題的表示式,然後再讓函式一層層套疊的方式來求解,同人不久前分享的語言整合查詢就是這種解法的代表。

第二種解決方式無疑是比較複雜的,同人有想過用高階函式來實作流程元件化,但顯然在有時間壓力的情況下,這樣做是找自己麻煩,還不如用 builder pattern 就可以達到目的。至於何時會用到 visitor 呢?當你處理問題的結構元素是固定而行為是變化的,那就會傾向用 visitor pattern 來設計解決方案。例如拆解組合訊息的欄位就那幾種,而它們都有拆解或組合兩種實作,所以很適合應用 visitor 來尋訪結構。

不過,最後同人還是要強調一點,千萬別為了設計樣式而應用設計樣式。同人開發這些實作都是先從使用案例的陳述開始,都是在架構大致底定之時,讓設計樣式自己來敲門報到的。以上經驗分享只是提供一個參考,樣式的關鍵真的是要我們去思考,我們想要解決什麼問題、以及為什麼要解決這個問題,切忌把樣式硬套在需求上呀。

Please follow and like us:
分類: 分析設計建模, 問題解決, 生活感觸, 編程技巧, 職場, 設計原則。這篇內容的永久連結

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *