2012 11 21

フレームワークを作るの3

XMLからSQLの定義を取得する処理について。 パラメーターをSQLの中に定義しているので、必要な処理は次の3つ。

  1. 指定の id のSQL定義を取得する。
  2. パラメーターを抜き出して 「?」 に置き換える。
  3. 取り出した順のパラメーターのリストを作る。

まずは取り出した結果を格納するためのクラス SqlDef を定義する。

package framework.dao; import java.util.List; public class SqlDef { private final String text; private final List<String> prms; public SqlDef(final String text, final List<String> prms) { this.text = text; this.prms = prms; } public String getText() { return text; } public List<String> getPrms() { return prms; } }

SQLのidを指定すると、このインスタンスに結果が設定されて返ってくるのだな。 text はパラメーターをプレースホルダに置き換えたSQLの文字列で、 prms はパラメーターのリスト。 一度取得してしまえば変更する必要は無いからgetterだけ。 初期値設定はコンストラクタでやることにしている。

次に、XMLを読んでSQLのデータを取得する処理について。

とあるフレームワークの設定で数十メガというサイズのXMLを見たことがある。 ちょっと古いPCでeclipseで読み込んだら、メモリ不足で終了してしまった。 そんな大量のXMLだと、読み込みのスピードや効率を考えるのは必須だが、SQLだけならそう大きくはならないだろうし、またちょっとぐらい大きくなったところで今の環境なら大して問題にはならないだろう。 と、希望的観測の元に、XMLからのデータ取り出しが最も楽にできる(と俺が感じている)XPathを使うことにする。

XMLの定義ファイル名は内部で固定で持っても良いのだが、切り替えられるようにしておくとテストで便利そうなので、コンストラクタで指定することにする。 それ以外の外部インターフェースは、idを指定してSQLの情報を取得するだけでいいだろう。

という方針で SqlDefLoader を実装する。

package framework.dao; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import framework.exception.SystemException; public class SqlDefLoader { private static final String SQL_XPATH_FMT = "//sql[@id='%s']/text()"; private static final Pattern PRM_PTN = Pattern.compile(":([A-Za-z0-9_]+)"); private static XPath xpath = null; private Document doc = null; public SqlDefLoader(final String sqlDefFileName) { xpath = XPathFactory.newInstance().newXPath(); DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); domFactory.setNamespaceAware(true); try { doc = domFactory.newDocumentBuilder().parse(sqlDefFileName); } catch (SAXException | ParserConfigurationException | IOException e) { throw new SystemException(e); } } public SqlDef getSql(final String id) { return getPrms(new SqlDef(getText(id), new ArrayList<String>())); } private String getText(final String id) { return getNodeList(SQL_XPATH_FMT, id).item(0).getNodeValue(); } private static SqlDef getPrms(final SqlDef def) { Matcher m = PRM_PTN.matcher(def.getText()); if (!m.find()) { return def; } String text = def.getText().replaceFirst(m.group(0), "?"); List<String> prms = new ArrayList<>(def.getPrms()); prms.add(m.group(1)); return getPrms(new SqlDef(text, prms)); } private NodeList getNodeList(final String xpathFormat, final String id) { try { XPathExpression expr = xpath.compile(String.format(xpathFormat, id)); return (NodeList) expr.evaluate(doc, XPathConstants.NODESET); } catch (XPathExpressionException e) { throw new SystemException(e); } } }

微妙に面倒なのが、XMLから取得したSQLを処理する部分。

メソッド getPrms では、SQLの文字列から :prm という形式のパラメータの最初の一つを抜き出してリストに積むとともにプレースホルダーの 「?」 に置き換える。 これを再帰的にパラメータが見つからなくなるまで実行している。

これ、置き換えに replaceFirst を使うべきところを、最初はうっかり replace を使ってしまったために、同じパラメーターが複数ある場合は最初に見つかったタイミングで全てプレースホルダーに置き換えてしまっていた。 しかし該当するパラメーターは一つしかリストに積まないため、実際の値設定で不一致になってしまうのだな。

サンプルで定義したSQLでは同じパラメーターが無かったために、最初はこの恥ずかしい問題に気付かず、テストしてようやく気付いたのだった。 やっぱりテストは大事だね。

発生する例外は、全部 SystemException にして投げっ放し。 ここで例外が発生するのは、つまりは設定の根幹に問題がある訳で、だからもうその先の処理を続ける意味が無いという判断。

最後になったけど、この2つのクラスはどちらも framework.dao に置く。

今日はここまで。 次は、これを使うようにDAOを変更する予定。