JDBCのPreparedStatementで名前付きパラメタを使えるようにするクラスNamedParameterPreparedStatementです。
GitHubにも置いてあります。リポジトリは「JDBCのResultSetからクラスにマッピング」と共有しています。
https://github.com/kmaebashi/dbutil
JDBCでPreparedStatementを使う時は、プレースホルダには「?」しか使えず、何番目の「?」なのかを数えないといけませんが、 「:PARAM_NAME」のようにコロンで始まる名前付きパラメタを使えるようにします。 C#あたりだと昔から使えますし、Spring FrameworkのNamedParameterJdbcTemplateと同様の機能です。
こんな感じで使います。
// 名前付きパラメタを含むSQLを書く。 String sql = """ INSERT INTO USERS ( SERIALID, NAME, ADDRESS, TEL ) VALUES ( :SERIALID, :NAME, :ADDRESS, :TEL ) """; // NamedParameterPreparedStatementのインスタンス生成。 // 第1引数はjava.sql.Connection, 第2引数が名前付きパラメタを含むSQLの文字列。 NamedParameterPreparedStatement npps = NamedParameterPreparedStatement.newInstance(conn, sql); // 各パラメタの名前と値の組をMapに詰め込む。 var params = new HashMap<String, Object>(); params.put("SERIALID", serialId); params.put("NAME", name); params.put("ADDRESS", address); params.put("TEL", tel); // NamedParameterPreparedStatementに対してパラメタを設定する。 npps.setParameters(params); // NamedParameterPreparedStatementからPreparedStatementを取得し、 // 以後は好きにする。 int result = npps.getPreparedStatement().executeUpdate();
上の例にあるとおり、パラメタの値はMapで渡しますが、これで値として指定できるデータ型は以下の通りです(プリミティブ型は当然auto boxingで拡張されるとして)。
これ以外の型を設定した場合、UnsupportedTypeExceptionがスローされます。必要に応じて書き足してください。
com.kmaebashi以下 ├─dbutil │ NamedParameterPreparedStatement.java … 利用者に公開するインタフェース │ ParameterValueNotFoundException.java … パラメタの設定不足エラー │ SqlParseException.java … SQLのパースエラー │ UnsupportedTypeException.java … サポート対象外の型を使ったエラー │ └─dbutilimpl NamedParameterPreparedStatementImpl.java … 実装の本体 SqlAndParams.java … 内部で使用するクラス
CSVパーサと同様、利用者に公開するクラスと公開しないクラスを分けています。
利用者に公開するインタフェースNamedParameterPreparedStatement.javaは以下の通り。上の使い方のサンプルで使ったメソッドがそのまま並んでいます。
NamedParameterPreparedStatement.java
package com.kmaebashi.dbutil; import java.sql.*; import java.util.Map; import com.kmaebashi.dbutilimpl.NamedParameterPreparedStatementImpl; public interface NamedParameterPreparedStatement { public static NamedParameterPreparedStatement newInstance(Connection conn, String sql) throws SQLException, SqlParseException { return new NamedParameterPreparedStatementImpl(conn, sql); } public void setParameters(Map<String, Object> params) throws SQLException, UnsupportedTypeException, ParameterValueNotFoundException; public PreparedStatement getPreparedStatement(); }
以下は例外です。まあ特記することはありません。
ParameterValueNotFoundException.java
package com.kmaebashi.dbutil; public class ParameterValueNotFoundException extends Exception { public ParameterValueNotFoundException(String message) { super(message); } }
SqlParseException.java
package com.kmaebashi.dbutil; public class SqlParseException extends Exception { public SqlParseException(String message) { super(message); } }
UnsupportedTypeExceptionは、NamedParameterPreparedStatementと共用しています。
UnsupportedTypeException.java
package com.kmaebashi.dbutil; public class UnsupportedTypeException extends Exception { public UnsupportedTypeException(String message) { super(message); } }
以下NamedParameterPreparedStatementImplが実装であり、本体です。
NamedParameterPreparedStatementImpl.java
package com.kmaebashi.dbutilimpl; import java.sql.*; import com.kmaebashi.dbutil.NamedParameterPreparedStatement; import com.kmaebashi.dbutil.ParameterValueNotFoundException; import com.kmaebashi.dbutil.SqlParseException; import com.kmaebashi.dbutil.UnsupportedTypeException; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Map; public class NamedParameterPreparedStatementImpl implements NamedParameterPreparedStatement { private PreparedStatement preparedStatement; private String[] paramNames; /* 基本動作: * コンストラクタでSQLをパースし、中に出てくる名前付きパラメタの一覧をparamNamesにセットする。 * paramNamesにはSQL中の名前付きパラメタが登場順に格納されている。複数回登場したら、 * 重複して格納されている。 * setParameters()で、paramNamesを頭からループし、引数のMapから取得した値を * ps.setXXX()で設定する。 */ public NamedParameterPreparedStatementImpl(Connection conn, String sql) throws SQLException, SqlParseException { SqlAndParams sqlAndParams = parseSql(sql); this.paramNames = sqlAndParams.paramNames; this.preparedStatement = conn.prepareStatement(sqlAndParams.sql); } @Override public void setParameters(Map<String, Object> params) throws SQLException, UnsupportedTypeException, ParameterValueNotFoundException { setParametersImpl(this.preparedStatement, this.paramNames, params); } @Override public PreparedStatement getPreparedStatement() { return this.preparedStatement; } private enum State { INITIAL, COLON, IN_PARAMETER, COMMENT_START, IN_COMMENT, C_STYLE_COMMENT_START, IN_C_STYLE_COMMENT, C_STYLE_COMMENT_END, IN_STRING, STRING_END } static SqlAndParams parseSql(String srcSql) throws SqlParseException { State state = State.INITIAL; StringBuilder sqlSB = new StringBuilder(); StringBuilder param = null; ArrayList<String> paramList = new ArrayList<String>(); for (int i = 0; i < srcSql.length(); i++) { char ch = srcSql.charAt(i); switch (state) { case INITIAL: if (ch == ':') { state = State.COLON; } else if (ch == '-') { state = State.COMMENT_START; sqlSB.append(ch); } else if (ch == '/') { state = State.C_STYLE_COMMENT_START; sqlSB.append(ch); } else if (ch == '\'') { state = State.IN_STRING; sqlSB.append(ch); } else { sqlSB.append(ch); } break; case COLON: if (Character.isJavaIdentifierStart(ch)) { param = new StringBuilder(); param.append(ch); state = State.IN_PARAMETER; } else { throw new SqlParseException(":の後ろに識別子がありません。"); } break; case IN_PARAMETER: if (Character.isJavaIdentifierPart(ch)) { param.append(ch); } else { paramList.add(param.toString()); sqlSB.append('?'); sqlSB.append(ch); state = State.INITIAL; } break; case COMMENT_START: if (ch == '-') { state = State.IN_COMMENT; } else { state = State.INITIAL; } sqlSB.append(ch); break; case IN_COMMENT: if (ch == '\n') { state = State.INITIAL; } sqlSB.append(ch); break; case C_STYLE_COMMENT_START: if (ch == '*') { state = State.IN_C_STYLE_COMMENT; } else { state = State.INITIAL; } sqlSB.append(ch); break; case IN_C_STYLE_COMMENT: if (ch == '*') { state = State.C_STYLE_COMMENT_END; } sqlSB.append(ch); break; case C_STYLE_COMMENT_END: if (ch == '/') { state = State.INITIAL; } else { state = State.IN_COMMENT; } sqlSB.append(ch); break; case IN_STRING: if (ch == '\'') { state = State.STRING_END; } sqlSB.append(ch); break; case STRING_END: if (ch == '\'') { state = State.IN_STRING; sqlSB.append(ch); } else { if (ch == ':') { state = State.COLON; } else if (ch == '-') { state = State.COMMENT_START; sqlSB.append(ch); } else if (ch == '/') { state = State.C_STYLE_COMMENT_START; sqlSB.append(ch); } else { sqlSB.append(ch); } } } } return new SqlAndParams(sqlSB.toString(), paramList.toArray(new String[0])); } static void setParametersImpl(PreparedStatement ps, String[] paramNames, Map<String, Object> paramValues) throws SQLException, UnsupportedTypeException, ParameterValueNotFoundException { for (int i = 0; i < paramNames.length; i++) { if (!paramValues.containsKey(paramNames[i])) { throw new ParameterValueNotFoundException("パラメタ" + paramNames[i] + "の値が見つかりません。"); } Object value = paramValues.get(paramNames[i]); if (value instanceof Integer intValue) { ps.setInt(i + 1, intValue.intValue()); } else if (value instanceof Double doubleValue) { ps.setDouble(i + 1, doubleValue); } else if (value instanceof Boolean boolValue) { ps.setBoolean(i + 1, boolValue); } else if (value instanceof String strValue) { ps.setString(i + 1, strValue); } else if (value instanceof java.sql.Date dateValue) { ps.setDate(i + 1, dateValue); } else if (value instanceof LocalDate dateValue) { ps.setDate(i + 1, java.sql.Date.valueOf(dateValue)); } else if (value instanceof java.sql.Timestamp timestampValue) { ps.setTimestamp(i + 1, timestampValue); } else if (value instanceof LocalDateTime dateTimeValue) { ps.setTimestamp(i + 1, java.sql.Timestamp.valueOf(dateTimeValue)); } else if (value == null) { ps.setNull(i + 1, Types.NULL); } else { throw new UnsupportedTypeException("型"+ value.getClass().getName() + "はサポートしていません。" + "必要に応じて書き足してください。"); } } } }
大筋の挙動は18行目からのコメントに記載しています。
56行目からのparseSql()メソッドで、受け取ったSQLを解析しています。この程度のものなら、正規表現か何かで「:[a-zA-Z_][a-zA-Z0-9_」を変換すればいいかとも思いましたが、結局コメントの中や文字列リテラルの中まで変換しては困るので、1文字ずつ読み込んで有限状態マシンで解析しています。有限状態マシンって何?という人は、CSVパーサの説明を参照してください。
setParameters()が呼ばれたら、setParametersImpl()(165行目~)が動きます(わざわざメソッドを分けたのは、テストも考慮しstaticメソッドにしたかったからです)。
コメント(22行目)にあるように、ここでパース時に設定したparamNamesを頭からループし、引数のMapから取得した値をps.setXXX()で設定しています。
最後のSqlAndParams.javaは、parseSql()からふたつの戻り値を戻すためだけのクラスです。Javaにもout引数欲しい……
SqlAndParams.java
package com.kmaebashi.dbutilimpl; class SqlAndParams { String sql; String[] paramNames; SqlAndParams(String sql, String[] paramNames) { this.sql = sql; this.paramNames = paramNames; } }
公開日: 2024/01/06
不具合等ありましたら、掲示板にご連絡願います。