S2Dao の IN 句で 1000 件以上のリストを渡すにはどうするか
DBMS に Oracle を使っている場合、Oracle の制限により IN 句には 1000 件までしかリストを記述できません。そのため、S2Dao のバインド変数で 1000 件を超えるサイズの配列や List を渡すと、それがそのまま変数展開されてしまい SQL 構文エラーとなってしまいます。S2JDBC ではすでに同種の問題に対策されている方がいらっしゃる*1ようですが、S2Dao でこの問題を回避するにはどうしたらよいかを考えてみました。
- そのような設計をしない
- 設計レベルでの問題回避。これが理想ではあると思いますが、状況によってはそうもいかないこともあるかと思います。
- Dao に渡すリストを 1000 件単位に分割して複数回クエリを実行
- 分割してSQLを実行し、SELECT文の場合は後で結果セットをマージします。ただし、ORDER BY があるような場合にどうするのか、という課題が残ります。また S2Pager との相性も悪いです。
- 自力で IN 句を OR 条件で結合した WHERE 句を作成し、埋め込み変数コメントを使用して SQL に埋め込む
- 実際に現場で行われているのは、これかなぁと。エスケープは自力できちんとやる必要があるので注意。
- S2Dao を改造する
- これが可能なら良いのですが、実際にやろうとすると、それほど簡単な話ではなく・・・。いろいろと実現に難がありそうです。
SELECT 文で pagingSqlRewriter#rewrite が実行される直前に、IN 句を分割する処理を入れてみました。Oracle 決め打ちです。
とりあえずの実装をしてみたのですが、parse のやり方が荒っぽいので、実際に使い物になるレベルではありません。真面目に対応しようとするとかなり大変なので、あきらめて素直に S2JDBC を使うか、埋め込み変数コメントで対応するほうがいいと思います(SqlTokenizer に手を入れて、きちんと IN 句用の Node が切り出されるようにしないと扱いづらい)。
役にはたたないと思いますが、参考までに晒しておきます。
diff -rc before/src/main/java/org/seasar/dao/CommandContext.java after/src/main/java/org/seasar/dao/CommandContext.java *** before/src/main/java/org/seasar/dao/CommandContext.java 2009-01-05 21:32:16.000000000 +0900 --- after/src/main/java/org/seasar/dao/CommandContext.java 2009-04-27 20:27:38.046875000 +0900 *************** *** 44,47 **** --- 44,50 ---- public boolean isEnabled(); public void setEnabled(boolean enabled); + + public boolean needsParenNodeSplit(); + public void setNeedsParenNodeSplit(boolean needsSplit); } diff -rc before/src/main/java/org/seasar/dao/context/CommandContextImpl.java after/src/main/java/org/seasar/dao/context/CommandContextImpl.java *** before/src/main/java/org/seasar/dao/context/CommandContextImpl.java 2009-01-05 21:32:16.000000000 +0900 --- after/src/main/java/org/seasar/dao/context/CommandContextImpl.java 2009-04-27 20:28:09.296875000 +0900 *************** *** 44,49 **** --- 44,51 ---- private boolean enabled = true; + private boolean needsParenNodeSplit = false; + private CommandContext parent; static { *************** *** 169,172 **** --- 171,183 ---- public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public boolean needsParenNodeSplit() { + return this.needsParenNodeSplit; + } + + public void setNeedsParenNodeSplit(boolean needsSplit) { + this.needsParenNodeSplit = needsSplit; + } + } \ No newline at end of file diff -rc before/src/main/java/org/seasar/dao/impl/SelectDynamicCommand.java after/src/main/java/org/seasar/dao/impl/SelectDynamicCommand.java *** before/src/main/java/org/seasar/dao/impl/SelectDynamicCommand.java 2009-03-10 23:38:18.000000000 +0900 --- after/src/main/java/org/seasar/dao/impl/SelectDynamicCommand.java 2009-04-27 20:10:52.984375000 +0900 *************** *** 57,62 **** --- 57,70 ---- Object[] bindVariables = ctx.getBindVariables(); Class[] bindVariableTypes = ctx.getBindVariableTypes(); String sql = ctx.getSql(); + + if (ctx.needsParenNodeSplit()) { + ExecutingSqlTokenizer tokenizer = new ExecutingSqlTokenizer(sql); + String[] tokens = tokenizer.parse(); + InClauseSplitter splitter = new InClauseSplitter(tokens); + sql = splitter.splitAndToString(); + } + String executingSql = pagingSqlRewriter.rewrite(sql, bindVariables, bindVariableTypes); BasicSelectHandler selectHandler = new BasicSelectHandler( *************** *** 75,78 **** --- 83,195 ---- return ret; } + + private static class ExecutingSqlTokenizer { + private String sql; + public ExecutingSqlTokenizer(String sql) { + this.sql = sql; + } + public String[] parse() { // FIXME: buggy! + String[] splitted = sql.split("\\s"); + int arraySize = 0; + for (int i = 0; i < splitted.length; i++) { + if (splitted[i] != null && splitted[i].length() > 0) { + arraySize++; + } + } + String[] parsed = new String[arraySize]; + int index = 0; + for (int i = 0; i < splitted.length; i++) { + if (splitted[i] != null && splitted[i].length() > 0) { + parsed[index] = splitted[i]; + index++; + } + } + return parsed; + } + } + + private static class InClauseSplitter { + private String[] tokens; + public InClauseSplitter(String[] tokens) { + this.tokens = tokens; + } + public String[] split() { + final int inClauseIndex = searchInClauseIndex(); + if (inClauseIndex <= 0) { + return tokens; + } + final int inClauseBracketBeginIndex = searchBracketBegin(inClauseIndex); + final int inClauseBracketEndIndex = searchBracketEnd(inClauseIndex); + if (inClauseBracketBeginIndex == -1 || inClauseBracketEndIndex == -1) { + return tokens; + } + if (inClauseBracketEndIndex - inClauseBracketBeginIndex + 1 <= 1000) { + return tokens; + } + + final String inClauseRightOperand = tokens[inClauseIndex - 1]; + // foo in (?, ?, ?, ..., ?) + // --> (foo in (?, ..., ?) or foo in (?, ..., ?) or foo in (?, ..., ?)) + String[] rewrited = new String[tokens.length]; + final int splitSize = 1000; + int splitCount = 0; + for (int i = 0; i < tokens.length; i++) { + if (i < inClauseIndex - 2) { + rewrited[i] = tokens[i]; + + } else if (i == inClauseIndex - 1) { + rewrited[i] = "(" + tokens[i]; + + } else if (i == inClauseIndex) { + rewrited[i] = tokens[i]; + + } else if (i > inClauseIndex && i < inClauseBracketEndIndex) { + if (splitCount == splitSize) { + rewrited[i] = tokens[i].substring(0, tokens[i].length() - 1) + + ") or " + inClauseRightOperand + " in ("; + splitCount = 0; + } else { + rewrited[i] = tokens[i]; + splitCount++; + } + } else if (i == inClauseBracketEndIndex) { + rewrited[i] = tokens[i] + ")"; + } else { + rewrited[i] = tokens[i]; + } + } + return rewrited; + } + public String splitAndToString() { + String[] rewrited = split(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < rewrited.length; i++) { + sb.append(rewrited[i]).append(" "); + } + return new String(sb).trim(); + } + private int searchInClauseIndex() { + for (int i = 0; i < tokens.length; i++) { + if (tokens[i].toLowerCase().equals("in")) { + return i; + } + } + return -1; + } + private int searchBracketBegin(int beginIndex) { + return searchBracketInternal(beginIndex, "("); + } + private int searchBracketEnd(int beginIndex) { + return searchBracketInternal(beginIndex, ")"); + } + private int searchBracketInternal(int beginIndex, String searchType) { + for (int i = beginIndex; i < tokens.length; i++) { + if (tokens[i].contains(searchType)) { + return i; + } + } + return -1; + } + } } diff -rc before/src/main/java/org/seasar/dao/node/ParenBindVariableNode.java after/src/main/java/org/seasar/dao/node/ParenBindVariableNode.java *** before/src/main/java/org/seasar/dao/node/ParenBindVariableNode.java 2009-01-05 21:32:30.000000000 +0900 --- after/src/main/java/org/seasar/dao/node/ParenBindVariableNode.java 2009-04-27 20:28:22.625000000 +0900 *************** *** 62,67 **** --- 62,70 ---- if (length == 0) { return; } + if (length > 1000) { + ctx.setNeedsParenNodeSplit(true); + } Class clazz = null; for (int i = 0; i < length; ++i) { Object o = Array.get(array, i);