Spring JDBC で取得結果をエンティティにマッピングする

# はじめに

最近Springについてチームで勉強会をしていて、Spring JDBCについて以下の記事で基本をまとめた。

https://qiita.com/atjjnnn/items/c0ea187cc5d8cc5519bb

ただ、この時書いたRowMapperについていまいち理解が曖昧だったためもう少し調べてみた。

# この記事で書かれていること

- Spring JDBCが提供している取得結果を変換するインターフェースは何があるか
- RowMapperとResultSetExtractorの使い方

# Sprig JDBCで取得結果を独自にエンティティに変換するには？

Spring JDBCでは取得結果を変換するインターフェースが3つ用意されている。

- RowMapper

JDBCのResultSetを**参照して**特定のPOJOにマッピングするためのインターフェース。

ResultSetの1行を特定の1インスタンスに変換することができる。

ResultSetExtractorと異なり、ResultSetの1行から1インスタンスへ変換されることが保証される。

カーソル処理等のResultSetに対する処理をSpringにゆだねることができる。

- ResultSetExtractor

JDBCのResultSetを**操作して**特定のPOJOにマッピングするためのインターフェース。

RowMapperとResultSetExtractorの違い

|  | RowMapper | ResultSetExtractor |
| --- | --- | --- |
| ResultSetの操作 | 許されていない | 許されている |
| POJOの1インスタンス生成 | ResultSetの1行からのみ変換 | ResultSetの複数行から変換が可能 |

- RowCallbackHandler

JDBCのResultSetを参照して何らかの処理を行うためのインターフェース。

戻り値を返さない。取得結果のファイル出力やデータのチェック等を行う場合に使用。

# **JOINで取得した結果を複数のEntityオブジェクトへマッピングしたい場合は？**

以下のテーブルを例に、1行のみレコードを変換する場合と複数レコードを変換する場合の両方確認してみる。

## 1行レコードのみ変換する場合

### **RowMapperインタフェースを実装する**

■テーブル例

customerテーブル

| customer_id(PK) | customer_no | first_name | last_name |
| --- | --- | --- | --- |

reservationテーブル

| reservation_id(PK) | customer_no | room_no |
| --- | --- | --- |

■JOIN SQL例

```java
SELECT
	c.customer_id,
	c.customer_no AS c_customer_no,
	c.first_name,
	c.last_name,
	r.reservation_id,
	r.customer_no AS r_customer_no,
	r.room_no
FROM
	customer c
	LEFT OUTER JOIN reservation r
	ON c.customer_no = r.customer_no
WHERE
		r.reservation_id = 1001
```

取得結果例：

| customer_id | c_customer_no | first_name | last_name | reservation_id | r_customer_no | room_no |
| --- | --- | --- | --- | --- | --- | --- |
| 23 | guest23 | Mike | Johonson | 1001 | guest23 | 503 |

Customerエンティティ

```java
public class Customer {
	private int customerId;
	private String customerNo;
	private String firstName;
	private String lastName;
	private Reservation reservation;
	...Getter, Setterメソッド
}
```

Reservationエンティティ

```java
public class Reservation{
	private int reservationId;
	private String customerNo;
	private int RoomNo;
	...Getter, Setterメソッド
} 
```

Customerリポジトリ

```java
public Customer selectById(int id) {
	String sql = "SELECT..." // 上に記載したJOIN SQL
	return jdbcTemplate.queryForObject(sql, new CustomerRowMapper(), id);
}
```

CustomerRowMapperクラス

```java
static class CustomerRowMapper implements RowMapper<Customer> {
	@Override
	public Customer mapRow(ResultSet rs, int rowNum) throws SQLException {
		Customer customer = new Customer();
		customer.setCustomerId(rs.getInt("customer_id"));
		customer.setCustomerNo(rs.getString("c_customer_no"));
		customer.setFirstName(rs.getString("first_name"));
		customer.setLastName(rs.getString("last_name"));
		Reservation reservation = new Reservation();
		reservation.setReservationId(rs.getInt("reservation_id"));
		reservation.setCustomerNo(rs.getString("r_customer_no"));
		reservation.setRoomNo(rs.getInt("room_no"));
		customer.setReservation(reservation);
		return customer;
	}
}
```

※以下の記事では1つのEntityオブジェクトへの変換にDataClassRowMapperを利用したが、上記のような複数オブジェクトへの変換はできないため利用できない。

[https://qiita.com/atjjnnn/items/c0ea187cc5d8cc5519bb#レコードをentityオブジェクトに変換して取得する場合](https://qiita.com/atjjnnn/items/c0ea187cc5d8cc5519bb#%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92entity%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%97%E3%81%A6%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88)

（応用）ラムダ式を利用した場合の書き方

Customerリポジトリ

```java
public List<Customer> selectById(int id) {
	String sql = "SELECT..." // 上に記載したSQL
	return jdbcTemplate.query(sql, (rs, rowNum -> {
		Customer customer = new Customer();
		customer.setCustomerId(rs.getInt("customer_id"));
		customer.setCustomerNo(rs.getString("c_customer_no"));
		customer.setFirstName(rs.getString("first_name"));
		customer.setLastName(rs.getString("last_name"));
		Reservation reservation = new Reservation();
		reservation.setReservationId(rs.getInt("reservation_id"));
		reservation.setCustomerNo(rs.getString("r_customer_no"));
		reservation.setRoomNo(rs.getInt("room_no"));
		customer.setReservation(reservation);
		return customer;
	});
}
```

ラムダ式を利用すると、RowMapperで行う処理をDAOクラスに直接記述するため、RowMapperインターフェースの実装クラスが不要となる。

## 複数レコードを変換する場合

### **ResultSetExtractorインタフェースを実装する**

■テーブル例

customerテーブル

| customer_id(PK) | customer_no | first_name | last_name |
| --- | --- | --- | --- |

reservationテーブル

| reservation_id(PK) | customer_no | room_no |
| --- | --- | --- |

■JOIN SQL例

```java
SELECT
	c.customer_id,
	c.customer_no AS c_customer_no,
	c.first_name,
	c.last_name,
	r.reservation_id,
	r.customer_no AS r_customer_no,
	r.room_no
FROM
	customer c
	LEFT OUTER JOIN reservation r
	ON c.customer_no = r.customer_no
WHERE
		c.customer_id = 23
```

取得結果例：

| customer_id | c_customer_no | first_name | last_name | reservation_id | r_customer_no | room_no |
| --- | --- | --- | --- | --- | --- | --- |
| 23 | guest23 | Mike | Johonson | 1001 | guest23 | 503 |
| 23 | guest24 | Mike | Johonson | 1002 | guest24 | 504 |
| 23 | guest25 | Mike | Johonson | 1003 | guest25 | 505 |

※同じゲストが複数部屋を予約している想定

Customerエンティティ

```java
public class Customer {
	private int customerId;
	private String customerNo;
	private String firstName;
	private String lastName;
	private Reservation reservation;
	private List<Reservation> reservations; // 複数のReservationオブジェクトのリスト
	...Getter, Setterメソッド
}
```

Customerリポジトリ

```java
public Customer selectById(int id) {
	String sql = "SELECT..." // 上に記載したJOIN SQL
	return jdbcTemplate.query(sql, new CustomerResultSetExtractor(), id);
}
```

CustomerResultSetExtractorクラス

```java
static class CustomerResultSetExtractor implements ResultSetExtractor<Customer> {
	@Override
	public Customer extractData(ResultSet rs) throws SQLException, DataAccessException {
		Customer customer = null;
		while(rs.next()) {
			if (customer == null) { // 1対多の1に該当するCustomerオブジェクトは1つ生成
				Customer customer = new Customer();
				customer.setCustomerId(rs.getInt("customer_id"));
				customer.setCustomerNo(rs.getString("c_customer_no"));
				customer.setFirstName(rs.getString("first_name"));
				customer.setLastName(rs.getString("last_name"));
			}
			// 1対多の多に該当するReservationオブジェクトはレコードの数だけ生成
			Reservation reservation = new Reservation();
			reservation.setReservationId(rs.getInt("reservation_id"));
			reservation.setCustomerNo(rs.getString("r_customer_no"));
			reservation.setRoomNo(rs.getInt("room_no"));
			customer.getReservations().add(reservation); // 取得したレコードの数だけ追加
		}
		return customer;
	}
}
```

# おわりに

何か間違えてたら教えてください。

# 参考

[プロになるためのSpring入門](https://gihyo.jp/book/2023/978-4-297-13613-0)

[Spring徹底入門](https://www.shoeisha.co.jp/book/detail/9784798142470)

https://olafnosuke.hatenablog.com/entry/2022/06/05/181058

https://qiita.com/YJ2222/items/e814d1b4e0aaafeb54c0

https://qiita.com/AIN/items/ff3b48fe9a61f5cd297f