.NET + SQL Server の組み合わせなら、この blog でも何回か取り上げている ScriptDom があります。
NuGet Gallery | Microsoft.SqlServer.TransactSql.ScriptDom 12.0.1
Microsoft.SqlServer.TransactSql.ScriptDom 名前空間 ()
が Java + Oracle でそういうのが無いか調べてみました。
やりたい事:SELECT の WHERE に定型的な条件を追加する。
select id , name from table_1 where age <= ? and age >= ? order by name desc
みたいなクエリを
select id , name from table_1 where age <= ? and age >= ? and delete_flg = 0 order by name desc
な風に変更したいなーという感じです。
とりあえず使い慣れた Doma で出来るか試してみました。
Doma も内部的には Parser と Generator をもっているようなので、一応出来そうです。
package sample; import java.util.Optional; import javax.sql.DataSource; import org.junit.Assert; import org.junit.Test; import org.seasar.doma.internal.jdbc.sql.NodePreparedSqlBuilder; import org.seasar.doma.internal.jdbc.sql.PreparedSql; import org.seasar.doma.internal.jdbc.sql.SimpleSqlNodeVisitor; import org.seasar.doma.internal.jdbc.sql.SqlParser; import org.seasar.doma.internal.jdbc.sql.node.EolNode; import org.seasar.doma.internal.jdbc.sql.node.FromClauseNode; import org.seasar.doma.internal.jdbc.sql.node.LogicalOperatorNode; import org.seasar.doma.internal.jdbc.sql.node.OtherNode; import org.seasar.doma.internal.jdbc.sql.node.SelectStatementNode; import org.seasar.doma.internal.jdbc.sql.node.WhereClauseNode; import org.seasar.doma.internal.jdbc.sql.node.WhitespaceNode; import org.seasar.doma.internal.jdbc.sql.node.WordNode; import org.seasar.doma.jdbc.Config; import org.seasar.doma.jdbc.SqlKind; import org.seasar.doma.jdbc.SqlNode; import org.seasar.doma.jdbc.dialect.Dialect; import org.seasar.doma.jdbc.dialect.Mssql2008Dialect; public class AppendWhereTransformerTest { private String buildSql(String sql) { SqlParser parser = new SqlParser(sql); SqlNode root = parser.parse(); root.accept(new AppendWhereTransformer(), null); NodePreparedSqlBuilder builder = new NodePreparedSqlBuilder(new TestConfig(), SqlKind.SELECT, null); PreparedSql result = builder.build(root, s -> s); return result.getRawSql(); } @Test public void シンプル() { String sql = "select\r\n" + " id\r\n" + " , name\r\n" + "from\r\n" + " table_1"; String actual = buildSql(sql); Assert.assertEquals("select\r\n" + " id\r\n" + " , name\r\n" + "from\r\n" + " table_1\r\n" + "where\r\n" + " appendColumn = ?", actual); } @Test public void 条件とソート有() { String sql = "select\r\n" + " id\r\n" + " , name\r\n" + "from\r\n" + " table_1\r\n" + "where\r\n" + " age <= ?\r\n" + " and age >=?\r\n" + "order by\r\n" + " name desc"; String actual = buildSql(sql); Assert.assertEquals("select\r\n" + " id\r\n" + " , name\r\n" + "from\r\n" + " table_1\r\n" + "where\r\n" + " age <= ?\r\n" + " and age >=?\r\n" + " and appendColumn = ?\r\n" + "order by\r\n" + " name desc", actual); } @Test public void 集計() { String sql = "select\r\n" + " id\r\n" + " , sum(amount)\r\n" + "from\r\n" + " table_1\r\n" + "where\r\n" + " age <= ?\r\n" + " and age >=?\r\n" + "group by\r\n" + " id\r\n" + "order by\r\n" + " id desc"; String actual = buildSql(sql); Assert.assertEquals("select\r\n" + " id\r\n" + " , sum(amount)\r\n" + "from\r\n" + " table_1\r\n" + "where\r\n" + " age <= ?\r\n" + " and age >=?\r\n" + " and appendColumn = ?\r\n" + "group by\r\n" + " id\r\n" + "order by\r\n" + " id desc", actual); } @Test public void サブクエリ() { String sql = "select\r\n" + " id\r\n" + " , name\r\n" + " , (select max(table_2.age) from table_2 where table_2.parentId = nest_.id) as MaxAge\r\n" + "from\r\n" + " (\r\n" + " select\r\n" + " id\r\n" + " , name\r\n" + " , appendColumn\r\n" + " from\r\n" + " table_1\r\n" + " where" + " age <= ?\r\n" + " and age >= ?\r\n" + " ) nest_\r\n" + "order by\r\n" + " name desc"; String actual = buildSql(sql); Assert.assertEquals("select\r\n" + " id\r\n" + " , name\r\n" + " , (select max(table_2.age) from table_2 where table_2.parentId = nest_.id) as MaxAge\r\n" + "from\r\n" + " (\r\n" + " select\r\n" + " id\r\n" + " , name\r\n" + " , appendColumn\r\n" + " from\r\n" + " table_1\r\n" + " where" + " age <= ?\r\n" + " and age >= ?\r\n" + " ) nest_\r\n" + "\r\n" + "where\r\n" + " appendColumn = ?\r\n" + "order by\r\n" + " name desc", actual); } } class TestConfig implements Config { @Override public DataSource getDataSource() { return null; } @Override public Dialect getDialect() { return new Mssql2008Dialect(); } } class AppendWhereTransformer extends SimpleSqlNodeVisitor<SqlNode, Void> { @Override protected SqlNode defaultAction(SqlNode node, Void p) { node.getChildren().forEach(n -> n.accept(this, p)); return node; } @Override public SqlNode visitSelectStatementNode(SelectStatementNode node, Void p) { WhereClauseNode whereNode = Optional.ofNullable(node.getWhereClauseNode()) .orElseGet(() -> { WhereClauseNode n = new WhereClauseNode("where"); n.appendNode(new EolNode("\r\n")); // setWhereClauseNode にすると、sql の一番下に where が追加されてしまう。order by や group by があると困るー。 FromClauseNode from = node.getFromClauseNode(); from.appendNode(new EolNode("\r\n")); from.appendNode(n); return n; }); whereNode.appendNode(WhitespaceNode.of(" ")); whereNode.appendNode(WhitespaceNode.of(" ")); whereNode.appendNode(new LogicalOperatorNode("and")); whereNode.appendNode(WhitespaceNode.of(" ")); whereNode.appendNode(new WordNode("appendColumn")); whereNode.appendNode(WhitespaceNode.of(" ")); whereNode.appendNode(OtherNode.of("=")); whereNode.appendNode(WhitespaceNode.of(" ")); whereNode.appendNode(new WordNode("?")); whereNode.appendNode(new EolNode("\r\n")); return node; } }
Java で見やすいクエリ書くの面倒!Xtend か Groovy なら見やすく書けるのに!
とりあえず出来ましたが、若干怖い感じ。
Where 句が無い時に、SelectStatementNode#setWhereClauseNode で Where 句を追加してしまうと、間違ったクエリになる事があります。
SelectStatementNode の set~ClauseNode の実装は、呼ばれたたびに後ろに追加していく様になっているので、Order By 等があると Order By の後に Where が追加されてしまいます。
なので、致し方無く From の最後に Where 追加してる無理やり感。。
もちろん Doma はこういう使い方するものではないので全然構わないのですが。
というわけで、次は別のライブラリで試してみます。
一番良いのは、ANTLR なんでしょうが難しそうだしなぁ。。
2015/02/12 追記:
Doma の作者の @nakamura_to さんに教えてもらいました。
@shinsukeoda SelectStatementNodeを新規に作成して中身を全部再セットするとうまくいくかもしれません。こんな感じです。 URL
こんな感じで SelectStatementNode を差換えると、問題無さそうですね!
SelectStatementNode result = new SelectStatementNode(); result.setSelectClauseNode(node.getSelectClauseNode()); result.setFromClauseNode(node.getFromClauseNode()); result.setWhereClauseNode(node.getWhereClauseNode()); result.setGroupByClauseNode(node.getGroupByClauseNode()); result.setHavingClauseNode(node.getHavingClauseNode()); result.setOrderByClauseNode(node.getOrderByClauseNode()); result.setForUpdateClauseNode(forUpdate); return result;