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
不具合等ありましたら、掲示板にご連絡願います。