Java : DSL : SQL Query
[id:lethevert:20060202:p4]の続き。
Javaでもある程度のDSLが書けるんじゃない?ということで、ちょっとやってみました。
JavaのDSLというと、JParsecを思い出すのだけど、それとはちょっと違う感じで。
サンプルコード
対象とした領域は、SQLの組み立てということにします。で、以下、テストコード。コメントに書いてあるのが実行結果です(見やすくなるように、改行とインデントを追加しています)。
"Expr where()"をオーバーライドして、条件を作成しています。比較演算子や論理演算子が前置になってしまうのは、2項演算子が定義できないJavaの制限です。これで2項演算子が定義できたら完璧なのにと思うけど、Lispも前置記法なことを考えれば、許容範囲かな。*1
import java.util.Date; public class Main { public static void main(String[] args) { String sql; sql = new Query("tableA").sql(); System.out.println(sql); // -> SELECT * FROM tableA sql = new Query("tableA", "tableB") { Expr where() { return and( isNull( col("colA")), eq( col("colB"), num(1)), ne( col("colC"), str("abc'de")), or( le( date("2000/01/01"), col("colD")), ge( date(new Date()), col("colE")))); } }.sql(); System.out.println(sql); /* SELECT * FROM tableA, tableB * WHERE (colA IS NULL AND colB = 1 AND colC != 'abc''de' * AND (TO_DATE('2000/01/01', 'YYYY/MM/DD') <= colD * OR TO_DATE('2006/02/03', 'YYYY/MM/DD') >= colE)) */ } }
実装
本体は、Queryクラスです。内部クラスとしてExprとTermが定義されています。その他、条件式構築のために必要な関数をQueryクラスのメソッドとして必要な数だけ定義しています。
[id:lethevert:20060122:p2]で作ったFoldクラスを拡張して使っているので、まず、その定義から。
public abstract class Fold<A, B> { abstract A each(A s0, B s1); public A $(A ret, B[] sa) { for (int i=0; i<sa.length; ++i) { ret = each(ret, sa[i]); } return ret; } public A snd(A ret, B[] sa) { for (int i=1; i<sa.length; ++i) { ret = each(ret, sa[i]); } return ret; } }
そして、本体のQuery.java。
import java.util.Date; import java.text.SimpleDateFormat; import java.text.ParseException; public class Query { abstract class Expr { abstract String sql(); public String toString() { return sql();} } abstract class Term { abstract String sql(); public String toString() { return sql();} } private static final JoinString commajoin = new JoinString(", "); private static final JoinString andjoin = new JoinString(" AND "); private static final JoinString orjoin = new JoinString(" OR "); private final String[] tables; public Query(String... tables) { assert tables.length > 0 : "error: short of tables"; this.tables = tables; } public String sql() { return "SELECT * FROM " + commajoin.$(tables) + (null == where?"":" WHERE "+where.sql()); } final Expr where = where(); Expr where() { return null;} protected Term col(final String colnm) { return new Term() { String sql() { return colnm;} }; } protected Term str(final String str) { return new Term() { String sql() { return "'" + str.replaceAll("'","''") + "'";} }; } protected Term num(final int num) { return new Term() { String sql() { return Integer.toString(num);} }; } protected Term num(final double num) { return new Term() { String sql() { return Double.toString(num);} }; } protected Term date(final Date date) { return new Term() { String sql() { return "TO_DATE('" + new SimpleDateFormat("yyyy/MM/dd").format(date) + "', 'YYYY/MM/DD')"; } }; } protected Term date(final String date) { try { return date(new SimpleDateFormat("yyyy/MM/dd").parse(date)); } catch(ParseException e) { assert false : e.toString(); return null; } } protected Expr isNull(final Term t1) { return new Expr() { String sql() { return t1.sql() + " IS NULL"; } }; } protected Expr eq(final Term t1, final Term t2) { return new Expr() { String sql() { return t1.sql() + " = " + t2.sql(); } }; } protected Expr ne(final Term t1, final Term t2) { return new Expr() { String sql() { return t1.sql() + " != " + t2.sql(); } }; } protected Expr lt(final Term t1, final Term t2) { return new Expr() { String sql() { return t1.sql() + " < " + t2.sql(); } }; } protected Expr le(final Term t1, final Term t2) { return new Expr() { String sql() { return t1.sql() + " <= " + t2.sql(); } }; } protected Expr gt(final Term t1, final Term t2) { return new Expr() { String sql() { return t1.sql() + " > " + t2.sql(); } }; } protected Expr ge(final Term t1, final Term t2) { return new Expr() { String sql() { return t1.sql() + " >= " + t2.sql(); } }; } protected Expr and(final Expr... ts) { return new Expr() { String sql() { return "(" + andjoin.$(ts) + ")"; } }; } protected Expr or(final Expr... ts) { return new Expr() { String sql() { return "(" + orjoin.$(ts) + ")"; } }; } static class JoinString { final String sep; JoinString(String sep) { this.sep = sep;} final Fold<StringBuffer, Object> joiner = new Fold<StringBuffer, Object>() { StringBuffer each(StringBuffer s1, Object s2) { return s1.append(sep).append(s2.toString()); } }; String $(Object[] strs) { return joiner.snd(new StringBuffer(strs[0].toString()), strs).toString(); } } }