JDBCのResultSetからクラスにマッピング

これは何?

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.TypesJavaの型
Types.INTEGERintまたはInteger
Types.REALdoubleまたはDouble
Types.BITまたはTypes.BOOLEANbooleanまたはBoolean
Types.CHARString
Types.VARCHARまたはTypes.NVARCHARString
Types.DATEjava.util.DateまたはLocalDate
Types.TIMESTAMPjava.util.DateまたはLocalDateTime
上記以外UnsupportedTypeExceptionを投げる

DBの型がこの表の左側である場合、DTOのフィールドの型はこの表の右側の型のいずれかでなければいけません(異なる場合、UnsupportedTypeExceptionを投げます)。

DBの値がNULLの時、intdoublebooleanといったプリミティブ型で受けると、0や0.0やfalseが設定されます(これは、ResultSetgetXXX()の仕様を踏襲しています)。

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


不具合等ありましたら、掲示板にご連絡願います。

ひとつ上のページに戻る