S2Dao の IN 句で 1000 件以上のリストを渡すにはどうするか

DBMSOracle を使っている場合、Oracle の制限により IN 句には 1000 件までしかリストを記述できません。そのため、S2Dao のバインド変数で 1000 件を超えるサイズの配列や List を渡すと、それがそのまま変数展開されてしまい SQL 構文エラーとなってしまいます。S2JDBC ではすでに同種の問題に対策されている方がいらっしゃる*1ようですが、S2Dao でこの問題を回避するにはどうしたらよいかを考えてみました。

  1. そのような設計をしない
    • 設計レベルでの問題回避。これが理想ではあると思いますが、状況によってはそうもいかないこともあるかと思います。
  2. Dao に渡すリストを 1000 件単位に分割して複数回クエリを実行
    • 分割してSQLを実行し、SELECT文の場合は後で結果セットをマージします。ただし、ORDER BY があるような場合にどうするのか、という課題が残ります。また S2Pager との相性も悪いです。
  3. 自力で IN 句を OR 条件で結合した WHERE 句を作成し、埋め込み変数コメントを使用して SQL に埋め込む
    • 実際に現場で行われているのは、これかなぁと。エスケープは自力できちんとやる必要があるので注意。
  4. 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);