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