【Spring MVC】Spring JDBCを使ったDBアクセス(ResultSetExtractorの使い方)

概要

データアクセスフレームワークであるSpring JDBCを使用する方法についてまとめた。
当記事では、JdbcTemplateのqueryメソッドと共に使用するResultSetExtractorについて紹介する。

ResultSetExtractorは、複数のテーブルを検索してORマッピングする際に使用する。

 

前提

以下の記事で紹介したJdbcTemplate.queryメソッドを使用する。

queryメソッドの使い方

概要 データアクセスフレームワークであるSpring JDBCを使用する方法についてまとめた。 当記事では、JdbcTemplateのSELECT処理であるqueryメソッドの使い方について紹介する。   前提 以下[…]

 

特徴

ResultSetExtractorは、複数テーブルに対するSQLクエリの結果をオブジェクトにマッピングするために使用する。

RowMapperは1つのテーブルとオブジェクトをマッピングすることを得意とするが、複数テーブルとマッピングする場合だと煩雑になる。
そのため、複数テーブルに対してORマッピングを行うにはResultSetExtractorが推奨される。

 

使い方

JdbcTemplate.queryと組み合わせて以下のように使用する。

JdbcTemplate.query(
SQL文字列,
パラメータ ※省略可,
ResultSetExtractorの実装
)

 

①JdbcTemplateによるSQLクエリの実行により、結果セット(ResultSet)を取得
②ResultSetを引数に、ResultSetExtractorのextractDataメソッドを呼び出す
③extractDataメソッドの中で定義した処理(rsからJavaオブジェクトへの変換)が呼ばれる
④extractDataメソッドから返却されたJavaオブジェクトが、最終的にqueryメソッドの戻り値として返却される

 

使用例

ResultSetExtractorを実装した使用例について紹介する。

全レコード検索

すべての生徒情報を取得する。

インターフェース

StudentDao.java


/** 全生徒情報を取得 */
List<Student> findAllStudens();

 

実装

ResultSetExtractorの定義

同じDao内で再利用できるように、フィールドにResultSetExtractorを匿名クラスで定義している。
尚、別のDaoで利用する場合、ResultSetExtractorを実装したクラスを用意したほうがいい。

 

JdbcStudentDao.java


/** Studentリストに変換するResultSetExtractor */
private final ResultSetExtractor<List<Student>> studentListWithTestsExtractor = new ResultSetExtractor<List<Student>>() {
	@Override
	public List<Student> extractData(ResultSet rs) throws SQLException {
		Map<Integer, Student> studentMap = new LinkedHashMap<>();
		while (rs.next()) {
			// 生徒情報をマップから取得
			var student = studentMap.get(rs.getInt("id"));
			if (student == null) {
				// 生徒情報がNULLの場合、作成する
				student = new Student(rs.getInt("id"), rs.getInt("grade"), rs.getString("name"),
						rs.getString("class_name"), rs.getString("memo"), new ArrayList<>());
				studentMap.put(rs.getInt("id"), student);
			}
			
			var testId = rs.getInt("test_id");
			if (testId > 0) {
				// テスト情報が存在する場合、リストに格納
				var test = new TestPoint(testId, rs.getInt("japanese"), rs.getInt("mathematics"),
						rs.getString("evaluation"), rs.getDate("test_date"), rs.getInt("student_id"));
				student.getTests().add(test);
			}
		}
		
		return new ArrayList<Student>(studentMap.values());
	}
};

 

private final ResultSetExtractor<List<Student>> studentListWithTestsExtractor = new ResultSetExtractor<List<Student>>() {

ResultSetExtractorのジェネリクス内に、返却する型を設定する
上記はStudentリストを返却するという意味。

 

@Override
public List<Student> extractData(ResultSet rs) throws SQLException {

ResultSetExtractor.extractDataを実装する。
このメソッドは、JdbcTemplate.queryが実行された後に取得する結果セット(ResultSet)を引数に、ORマッピングすることが役割。

メソッド内にてSQLExceptionがスローされた場合、JdbcTemplateがDataAccessExceptionにラップする。

 

JdbcTemplateの例外ハンドリング

概要 データアクセスフレームワークであるSpring JDBCを使用する方法についてまとめた。 当記事ではDAOの作成と、DBアクセスの動確を行う方法について紹介する。 ※尚、DAOパターンとは何かについての説明は割愛する &n[…]

 

while (rs.next()) {

SQLクエリの結果として得られた結果セットを行単位でループする。
生徒IDをキーに生徒情報を保持するマップ(Map<生徒ID, 生徒情報>)を、以下の流れで作成する。

・マップから生徒情報を取得
・生徒情報が存在しなければ、生徒情報を生成して生徒IDをキーにマップに格納
・テスト情報を取得
・テスト情報が存在する場合、テスト情報を生成して生徒情報のテストリストに格納

 

return new ArrayList<Student>(studentMap.values());

生成した生徒情報マップをもとに、生徒情報リストを生成して返却する。

 

queryの実装

作成したResultSetExtractorをJdbcTemplate.queryメソッドに渡すことで、ResultSetExtractor.extractDataで定義した戻り値(Studentリスト)を取得できる

JdbcStudentDao.java


@Override
public List<Student> findAllStudens() {
	var sql = "SELECT s.id, s.grade, s.name, s.class_name, s.memo, "
			+ "t.id as test_id, t.japanese, t.mathematics, t.evaluation, t.test_date, t.student_id "
			+ "FROM student s LEFT JOIN test_point t ON s.id = t.student_id";

	return jdbcTemplate.query(sql, studentListWithTestsExtractor);
}

 

return jdbcTemplate.query(sql, studentListWithTestsExtractor);

RowMapperを渡したように、ResultSetExtractorの実装をqueryメソッドに渡すだけ。
尚、リストを取得できない場合でも、例外は発生せず空のリストを返却する

 

使用結果

FooMain.java


// 全生徒情報を取得
List<Student> studentList = studentDao.findAllStudens();
studentList.forEach(s -> System.out.println(s));
// ------------------------
// コンソール
// ------------------------
// Student(id=1, grade=1, name=山田太郎, className=1-A, memo=良好な成績, tests=[TestPoint(id=1, japanese=80, mathematics=70, evaluation=良好, testDate=2023-05-22, studentId=1), TestPoint(id=4, japanese=85, mathematics=90, evaluation=非常に良い, testDate=2023-09-30, studentId=1)])
// Student(id=2, grade=2, name=佐藤花子, className=2-B, memo=スポーツ万能, tests=[TestPoint(id=2, japanese=90, mathematics=65, evaluation=優秀, testDate=2023-05-22, studentId=2), TestPoint(id=5, japanese=60, mathematics=80, evaluation=普通, testDate=2023-09-30, studentId=2)])
// Student(id=3, grade=3, name=鈴木一郎, className=3-C, memo=芸術に秀でる, tests=[TestPoint(id=3, japanese=75, mathematics=85, evaluation=可, testDate=2023-05-22, studentId=3), TestPoint(id=6, japanese=85, mathematics=90, evaluation=非常に良い, testDate=2023-09-30, studentId=3)])

 

1件レコード検索

1件の生徒情報を取得する。

インターフェース

StudentDao.java


/** 生徒情報を取得 */
Student findStudentById(int id);

 

実装

JdbcStudentDao.java


@Override
public Student findStudentById(int id) {
	var sql = "SELECT s.id, s.grade, s.name, s.class_name, s.memo, "
            + "t.id as test_id, t.japanese, t.mathematics, t.evaluation, t.test_date, t.student_id "
            + "FROM student s LEFT JOIN test_point t ON s.id = t.student_id WHERE s.id = ?";

    List<Student> results = jdbcTemplate.query(sql, new Object[]{id}, studentListWithTestsExtractor);
	
    if (results.isEmpty()) {
    	// 生徒情報の取得に失敗
        throw new EmptyResultDataAccessException(1);
    }

    return results.get(0);
}

 

List<Student> results = jdbcTemplate.query(sql, new Object[]{id}, studentListWithTestsExtractor);

1件だけ取得するためにResultSetExtractorの実装を行いたくないため、全レコード取得時に使用した匿名クラスを利用する。
また、パラメータが存在する場合はnew Object[]{ パラメータ1, パラメータ2… }と設定する。

 

// 生徒情報の取得に失敗
throw new EmptyResultDataAccessException(1);

1件のデータのみ期待しているため、生徒情報を取得できない場合はqueryForObjectと同様の例外をスローしている。
EmptyResultDataAccessExceptionの引数に設定している「1」は、1件のレコード取得を期待するという意味。

IncorrectResultSizeDataAccessExceptionについてはテーブルの構造上、同じIDに対して複数の生徒情報を保持できないため考慮する必要はない。

 

使用結果

FooMain.java


// 生徒情報の取得
var student = studentDao.findStudentById(2);
System.out.println(student);
// ------------------------
// コンソール
// ------------------------
// Student(id=2, grade=2, name=佐藤花子, className=2-B, memo=スポーツ万能, tests=[TestPoint(id=2, japanese=90, mathematics=65, evaluation=優秀, testDate=2023-05-22, studentId=2), TestPoint(id=5, japanese=60, mathematics=80, evaluation=普通, testDate=2023-09-30, studentId=2)])

 

スポンサーリンク