昨日の続き。
SQLとプレースホルダーの内容とが取得できるようになったので、後DBアクセスに必要なのは、実際の値をプレースホルダーに設定する処理になる。
これは、検索条件を与えるクラスが、SQL内のパラメーター
:prm
に対して
getPrm
というgetterを持つという制約を課しておけば、リフレクションで簡単に処理できるだろう。
と考えて、さてDAOについて。 DBアクセスの指定がidを指定するだけ、結果の変換先も外部から指定するとなったので、DAOの処理はもう処理内容に依存しないで書けるようになっているのだな。 ということで、共通基盤扱いの CmnDao を作る。
package framework.dao;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import framework.exception.SystemException;
public class CmnDao {
private final Connection conn;
private final SqlDefLoader loader;
public CmnDao(final Connection conn, final SqlDefLoader loader) {
this.conn = conn;
this.loader = loader;
}
private static <E> void setPrms(PreparedStatement ps, final List<String> prms, final E criteria) throws SQLException {
try {
int i = 1;
for (String prm : prms) {
ps.setString(i++, (String) criteria.getClass().getMethod(toGetterName(prm)).invoke(criteria));
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
throw new SystemException(e);
}
}
private static String toGetterName(final String fieldName) {
return "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
}
public <T, E> List<T> select(final String sqlId, final E criteria, Class<T> dtoClass) throws SQLException {
List<T> list = new ArrayList<>();
SqlDef sql = loader.getSql(sqlId);
try (PreparedStatement ps = conn.prepareStatement(sql.getText())) {
setPrms(ps, sql.getPrms(), criteria);
ResultSet rs = ps.executeQuery();
DtoConverter<T> cv = new DtoConverter<>(rs, dtoClass);
while (rs.next()) {
list.add(cv.toDTO(rs, dtoClass.newInstance()));
}
} catch (InstantiationException | IllegalAccessException e) {
throw new SystemException(e);
}
return list;
}
public <T> List<T> select(final String sqlId, Class<T> dtoClass) throws SQLException {
return select(sqlId, null, dtoClass);
}
public <E> Integer update(final String sqlId, final E criteria) throws SQLException {
SqlDef sql = loader.getSql(sqlId);
try (PreparedStatement ps = conn.prepareStatement(sql.getText())) {
setPrms(ps, sql.getPrms(), criteria);
return ps.executeUpdate();
}
}
public Integer update(final String sqlId) throws SQLException {
return update(sqlId, null);
}
}
DB接続とSqlDefLoaderをコンストラクタで渡す。
検索系は、SQLのidと検索条件と結果を格納するクラスを引数に指定する。 検索条件が不要な場合のために、これを省略した呼び出しも受け付ける。 戻値は全てDTOのリスト。 検索結果が2件以上だったら例外を投げるような、DTOを1件だけ返す専用のメソッドを作ろうかと一瞬考えたのだが、一瞬でやめた。 面倒だし、どうせ呼び出し側でその辺りの面倒を見ることになることになるだろうし。 検索結果が1件しか無いはずなのかどうかは、呼び出し側にしか分からないからね。
更新系は、SQLのidと更新条件を引数に指定する。 更新条件を指定しなくてもいいのは、検索の場合と同じ。 戻値は、更新または削除した件数。
検索系では、大量データを扱う可能性がある場合は、結果をリストに詰めて返すとメモリ不足になる可能性があるので、これとは別のResultSetを返すような処理が必要になるのだろうが、その辺りの検討と実装は後回し。
ところでジェネリクスだが、これまでの印象は、安全だし便利ではあるけど面倒だって程度だった。 でもメタな定義をしようとすると、これが無いなんて考えられないぐらいに便利なんだね。 ジェネリクス以前なら、Object型にしておいて使う先々でキャストするとか、一々専用にインターフェースを作るとかしてたのが、ジェネリクス以降だと <T> なんて簡単な記述で、特定の型に縛られない定義が出来るステキ仕様。 誰が考えたのか知らないが、世の中には気の利いた奴がいるもんだ。
さて、処理内容に依存しない CmnDao が出来た訳だが、これをサービス層のオブジェクトで直接操作するのは気が引ける。 というのも、直接使おうとするとサービス層のオブジェクトでSQLのidを指定することになって、つまりはサービス層のオブジェクトがデータアクセス層の詳細を知る必要があるということ。 これでは層を分けた意味が無い。
ということで、SQLの呼び出しを隠蔽するために元の BooksDao の中で CmnDao を使うことにする。
package dao;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import dao.BooksDao;
import dto.BookDto;
import framework.dao.CmnDao;
import framework.dao.SqlDefLoader;
public class BooksDao {
private final CmnDao dao;
public BooksDao(Connection conn) {
this.dao = new CmnDao(conn, new SqlDefLoader("sqlDef.xml"));
}
public Integer insert(final BookDto book) throws SQLException {
return dao.update("insert", book);
}
public BookDto selectByISBN(final String isbn) throws SQLException {
BookDto criteria = new BookDto();
criteria.setIsbn(isbn);
List<BookDto> list = dao.select("selectByIsbn", criteria, BookDto.class);
return list.size() == 1 ? list.get(0) : new BookDto();
}
public List<BookDto> selectAll() throws SQLException {
return dao.select("selectAll", BookDto.class);
}
public Integer updateByISBN(final BookDto book) throws SQLException {
return dao.update("updateByIsbn", book);
}
public Integer deleteByISBN(final BookDto book) throws SQLException {
return dao.update("deleteByIsbn", book);
}
}
SqlDefLoader に渡す定義ファイル名をここで設定しているのは、定義ファイルの肥大化対策でDAO毎にファイルを切り替えることがあるかもしれないと思ったから。 あと、こうしたファイル名とかidの文字列とかは、頭の方でまとめて定数として宣言した方がいいのかもしれないけど、今はメソッドとほぼ1対1だしどうでもいい感じ。
CmnDao の select が常にリストを返すようになっているため、 BooksDao の selectByISBN では、戻り値のリストの要素数を調べて、1のときのみリストの先頭要素を返すようにしている。 要素数は1かどうかだけを判断しているのは、ISBNが主キーだから。 2以上は有り得ない。 そうアサーションを入れた方がいいのかな。
無かった場合にnullではなく空のDTOを返しているのは、こうした方が呼び出し元で扱いやすい気がするから。 一般に、オブジェクトがnullではないかと判定するよりは、オブジェクトのgetterなりで空だと判断する方が、記述が楽で読み易い場合が多いような気がするんだよね。
さて、データアクセス層については、ここまでで一応使える最低限をクリアしたと思う。
現在の構成は、ファイルまで含めると以下の通り。
apl
Main.java
service
BooksService.java
dao
BooksDao.java
dto
BookDto.java
util
DbUtil.java
framework
service
dao
CmnDao.java
DtoConverter.java
SqlDefLoader.java
SqlDef.java
exception
BusinessException.java
SystemException.java
sqlDef.xml
データアクセス層で言えば、フレームワークを使う時に自分で書く必要があるのは sqlDef.xml と BooksDao.java の2つ。
SQLは、別途変数の宣言が必要であっても sqlplus で動作確認したSQLをそのままコピーすればいいので、定義ファイルを作るのは楽になるんじゃないかと思う。 パラメータがSQLの中に埋め込まれているので、性能対策でSQLの組み替えをした場合でも、呼び出し側は何も変えなくて済む可能性も結構高いのではないか。 性能対策と言えば、SQLをそのまま書けるのでヒントも書き易い。
欠点は、特定のDB製品に依存する書き方になりがちなこと。 まあ、途中で別のDBに変更とか、そうそうない話ではあるが。 あとはSQLが苦手だとどうしようもないことか。
今のところ放置しているのは、検索結果として大量データを扱う場合について。 オブジェクトをシングルトンにすべきかどうか、定義ファイルを毎回読むのかキャッシュするのか、この辺りもあまり考えてない。 まあ、こっちは後でどうにでもなりそうだし、困ったら考えればいいか。
今日はここまで。 次からはサービス層を考える。