JDBCにおいてDBからの検索結果を保持するResultSetの内容を、DTO(Data Transfer Object)となる普通のクラスにマッピングするクラスResultSetMapperです。
GitHubにも置いてあります。リポジトリは「JDBCのPreparedStatementに名前付きパラメタ」と共有しています。
https://github.com/kmaebashi/dbutil
この機能はDBの検索結果からクラスへの一方通行のマッピングであり、(JPAとかのような)クラスからDBに書き込む機能はありません。
これを使うには、まずDTOとするクラスに、テーブルの列名と対応付けるためにアノテーションを付けます(フィールドの名前がDBの列名と同じであっても、このアノテーションは省略できません)。
import com.kmaebashi.dbutil.TableColumn;
public class Person {
@TableColumn("SERIALID")
public int serialId;
@TableColumn("NAME")
public String name;
@TableColumn("ADDRESS")
public String address;
@TableColumn("TEL")
public String tel;
}
こうしておいて、データが複数件の場合はResultSetMapper.toDtoList()メソッドを、データが1件とわかっている場合にはResultSetMapper.toDto()メソッドを使うことで、ResultSetからDTOへの変換ができます。
// ResultSetからList<Person>に変換する。 // 第1引数はResultSet, 第2引数はDTOのクラス。 List<Person> personList = ResultSetMapper.toDtoList(rs, Person.class); // ResultSetからList<Person>に変換する。 // 第1引数はResultSet, 第2引数はDTOのクラス。 // 1件も取得できなければ結果はnull、2件以上取得出来たらMultipleMatchExceptionを投げる。 Person person = ResultSetMapper.toDto(rs, Person.class);
JDBCの型(java.sql.Types)とJavaの型との対応付けは以下の通り。
| java.sql.Types | Javaの型 |
|---|---|
| Types.INTEGER | intまたはInteger |
| Types.REAL | doubleまたはDouble |
| Types.BITまたはTypes.BOOLEAN | booleanまたはBoolean |
| Types.CHAR | String |
| Types.VARCHARまたはTypes.NVARCHAR | String |
| Types.DATE | java.util.DateまたはLocalDate |
| Types.TIMESTAMP | java.util.DateまたはLocalDateTime |
| 上記以外 | UnsupportedTypeExceptionを投げる |
DBの型がこの表の左側である場合、DTOのフィールドの型はこの表の右側の型のいずれかでなければいけません(異なる場合、UnsupportedTypeExceptionを投げます)。
DBの値がNULLの時、intやdoubleやbooleanといったプリミティブ型で受けると、0や0.0や
Types.CHARの場合に限り、以下のようにtrim=trueを付けることで、取得時に末尾の空白をトリムすることができます。これはJavaのString.stripTrailing()メソッドを使っているので、全角の空白も除去されます。
@TableColumn(value="CHAR_COLUMN", trim=true) public String charColumn;
└─dbutil MultipleMatchException.java ResultSetMapper.java UnsupportedTypeException.java
ResultSetMapperについては、すべてstaticメソッドなので、interfaceによる公開分と非公開分の分離はしていません。すべてdbutil側にいます。
いきなり本体のResultSetMapper.javaです。
ResultSetMapper.java
package com.kmaebashi.dbutil;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class ResultSetMapper {
private ResultSetMapper() {
}
public static <T> T toDto(ResultSet rs, Class<T> dtoClass)
throws SQLException, InstantiationException, IllegalAccessException, UnsupportedTypeException,
MultipleMatchException, NoSuchMethodException, InvocationTargetException {
List<T> list = toDtoList(rs, dtoClass);
if (list.isEmpty()) {
return null;
} else if (list.size() > 1) {
throw new MultipleMatchException("" + list.size() + "件検索されました。");
} else {
return list.get(0);
}
}
public static <T> List<T> toDtoList(ResultSet rs, Class<T> dtoClass)
throws SQLException, InstantiationException, IllegalAccessException, UnsupportedTypeException,
NoSuchMethodException, InvocationTargetException {
ResultSetMetaData rsmd = rs.getMetaData();
int colCount = rsmd.getColumnCount();
HashMap<String, Integer> nameToIndex = new HashMap<>();
Field[] fieldArray = dtoClass.getDeclaredFields();
for (int i = 0; i < fieldArray.length; i++) {
TableColumn tc = fieldArray[i].getAnnotation(TableColumn.class);
if (tc != null)
{
nameToIndex.put(tc.value().toUpperCase(), i);
}
}
List<T> list = new ArrayList<>();
while (rs.next()) {
T dto = dtoClass.getDeclaredConstructor().newInstance();
for (int i = 0; i < colCount; i++) {
final int rsIdx = i + 1;
String colName = rsmd.getColumnName(rsIdx).toUpperCase();
if (!nameToIndex.containsKey(colName))
continue;
int fieldIndex = nameToIndex.get(colName);
int colType = rsmd.getColumnType(rsIdx);
switch (colType) {
case Types.INTEGER:
if (fieldArray[fieldIndex].getType() == Integer.TYPE) {
fieldArray[fieldIndex].setInt(dto, rs.getInt(rsIdx));
} else if (fieldArray[fieldIndex].getType() == Integer.class) {
int intValue = rs.getInt(rsIdx);
if (rs.wasNull()) {
fieldArray[fieldIndex].set(dto, null);
} else {
fieldArray[fieldIndex].set(dto, intValue);
}
} else {
throw new UnsupportedTypeException("整数を型"
+ fieldArray[fieldIndex].getType().getTypeName()
+ "に変換できません(列:" + colName + ")。");
}
break;
case Types.REAL:
if (fieldArray[fieldIndex].getType() == Double.TYPE) {
fieldArray[fieldIndex].setDouble(dto, rs.getDouble(rsIdx));
} else if (fieldArray[fieldIndex].getType() == Double.class) {
double doubleValue = rs.getDouble(rsIdx);
if (rs.wasNull()) {
fieldArray[fieldIndex].set(dto, null);
} else {
fieldArray[fieldIndex].set(dto, doubleValue);
}
} else {
throw new UnsupportedTypeException("実数を型"
+ fieldArray[fieldIndex].getType().getTypeName()
+ "に変換できません(列:" + colName + ")。");
}
break;
case Types.BIT:
case Types.BOOLEAN:
if (fieldArray[fieldIndex].getType() == Boolean.TYPE) {
fieldArray[fieldIndex].setBoolean(dto, rs.getBoolean(rsIdx));
} else if (fieldArray[fieldIndex].getType() == Boolean.class) {
boolean boolValue = rs.getBoolean(rsIdx);
if (rs.wasNull()) {
fieldArray[fieldIndex].set(dto, null);
} else {
fieldArray[fieldIndex].set(dto, boolValue);
}
} else {
throw new UnsupportedTypeException("ブーリアンを型"
+ fieldArray[fieldIndex].getType().getTypeName()
+ "に変換できません(列:" + colName + ")。");
}
break;
case Types.CHAR:
if (fieldArray[fieldIndex].getAnnotation(TableColumn.class).trim()) {
if (rs.getString(rsIdx) == null) {
fieldArray[fieldIndex].set(dto, null);
} else {
fieldArray[fieldIndex].set(dto, rs.getString(rsIdx).stripTrailing());
}
} else {
fieldArray[fieldIndex].set(dto, rs.getString(rsIdx));
}
break;
case Types.VARCHAR:
case Types.NVARCHAR:
fieldArray[fieldIndex].set(dto, rs.getString(rsIdx));
break;
case Types.DATE:
java.sql.Date sqlDate = rs.getDate(rsIdx);
if (fieldArray[fieldIndex].getType() == java.util.Date.class) {
fieldArray[fieldIndex].set(dto, sqlDate);
} else if (fieldArray[fieldIndex].getType() == LocalDate.class) {
if (sqlDate == null) {
fieldArray[fieldIndex].set(dto, null);
} else {
fieldArray[fieldIndex].set(dto, sqlDate.toLocalDate());
}
} else {
throw new UnsupportedTypeException("DATE型を型"
+ fieldArray[fieldIndex].getType().getTypeName()
+ "に変換できません(列:" + colName + ")。");
}
break;
case Types.TIMESTAMP:
java.sql.Timestamp sqlTimestamp = rs.getTimestamp(rsIdx);
if (fieldArray[fieldIndex].getType() == java.util.Date.class) {
fieldArray[fieldIndex].set(dto, sqlTimestamp);
} else if (fieldArray[fieldIndex].getType() == LocalDateTime.class) {
if (sqlTimestamp == null) {
fieldArray[fieldIndex].set(dto, null);
} else {
fieldArray[fieldIndex].set(dto, sqlTimestamp.toLocalDateTime());
}
} else {
throw new UnsupportedTypeException("TIMESTAMP型を型"
+ fieldArray[fieldIndex].getType().getTypeName()
+ "に変換できません(列:" + colName + ")。");
}
break;
default:
throw new UnsupportedTypeException("java.sql.Typesの" + colType + "は未対応です。");
}
}
list.add(dto);
}
return list;
}
}
単独のオブジェクトで使うtoDto()(19行目~)は、単にtoDtoList()を呼んでその先頭を返すだけです。
よって、本体は32行目からのtoDtoList()です。ここでは、ResultSetからResultSetMetaDataを取得し(35行目)、これに設定されているDB側の型情報(61行目のcolType)とリフレクションで取り出したフィールドの型情報(64行目等で参照しているfieldArray[fieldIndex].getType())を突き合わせて、必要な変換を行ってフィールドに格納しています。
クラスのフィールドを参照する際、毎度アノテーションの名前でfieldArrayをループして探さなくてよいように、38~46行目で名前からfieldArrayのインデックスを取得するためのハッシュマップ(nameToIndex)を構築しています。
以下は例外です。特に特筆するようなことはありません。
MultipleMatchException.java
package com.kmaebashi.dbutil;
public class MultipleMatchException extends Exception {
public MultipleMatchException(String message) {
super(message);
}
}
UnsupportedTypeException.java
package com.kmaebashi.dbutil;
public class UnsupportedTypeException extends Exception {
public UnsupportedTypeException(String message) {
super(message);
}
}
公開日: 2024/01/06
不具合等ありましたら、掲示板にご連絡願います。