torutkのブログ

ソフトウェア・エンジニアのブログ

ResultSetがStreamになったら嬉しいかも

はじめに

JDBCでデータベース検索のプログラムを書いていると、テーブル毎に似たような、そしてちょっとずつ異なるコードを書くことになります。 異なる部分は、カラム名、データ型、そして取り出したバラバラの値をJavaのデータクラス(ドメインクラスであったり、DTOであったり)に詰める処理です。

典型的には以下のようなプログラミングになるかと思います。

class DbAccessor {
    Connection conn;

    void initialize() {
        conn = DriverManager.getConnection("...");  // データベース接続用のURL・パラメータを指定しコネクションを取得
    }

    List<MyModel> getMyModelListOfDelta(int delta) {
        List<MyModel> models = new ArrayList<>();
        try (PreparedStatement statement = conn.prepareStatement("SELECT alfa,bravo,charlie,kilo FROM Phonetics WHERE delta=?")) {
            statement.setInt(1, delta);
            ResultSet result = statement.executeQuery();
            while (result.next()) {
                int alfa = result.getInt("alfa");
                String bravo = result.getNString("bravo");
                double charlie = result.getDouble("charlie");
                LocalDateTime kilo = result.getObject("kilo", LocalDateTime.class);
                models.add(new MyModel(alfa, bravo, charlie, kilo));
            }
        } catch (SQLException ex) {
            // ...
        }
        return models;
    }
}

JDBCでは、SQL検索文を発行した結果をResultSetで取得します。ResultSetはnext()でカーソルを進めて、getXXメソッドでカラムの値を取り出す仕様となっています。SQL文、PreparedStatementへのプレースホルダーの値設定、ResultSetからのカラムの取り出し、といった部分はテーブル固有の記載となります。

上述のように結構長々と書くことになり、また、同じテーブルでも検索の条件毎にSQL文とプレースホルダー、戻り値が異なるので、別々なメソッドにResultSetのイテレーション処理を記述することになります。

ResultSetをStreamにしたら

探してみると、ResultSetをjava.util.stream.Streamでラップするというブログ等がいくつか見つかりました。 また、jooq というライブラリも存在します。

ResultSetをStreamにするブログ等

  • Implementing a Spliterator for a JDBC ResultSet
    java.util.Spliterator インタフェースを使ってResultSetをStreamにするという内容。実際にはjava.util.SpliteratorsクラスのネストクラスAbstractSpliteratorを継承し、ResultSetからオブジェクトへのマッピングは著者の別ライブラリSqlMapperを使っている。Stream処理内(tryAdvanceメソッド内)で発生したSQLExceptionは、ResultSetのnextで生じたものはRuntimeExceptionにラップして投げており、ResultSetから値を取り出す際に生じたものは結果を返すオブジェクトにエラーを格納して戻り値としている。

  • 書籍 Java Closures and Lambda | Robert Fischer | Apress
    java.util.Spliterator インタフェースを使ってResultSetをStreamにする内容。こちらもAbstractSpliteratorを継承したResultSetSpliteratorクラスを定義し、ReslutSetからオブジェクトへのマッピングは、そのResultSetSpliteratorクラスの抽象メソッドprocessRowをさらに用途に応じてサブクラスで実装するというアプローチ。Stream処理内(tryAdvanceメソッド内)で発生したSQLExceptionはRuntimeExceptionにラップして投げている。

  • Against Boredom: Java 8: JDBC ResultSet to Stream AbstractSpliteratorのtryAdvanceを実装した無名クラスを生成し、ResultSetからは固定のRecordクラスにマッピングしている。Recordクラスは、カラム名をキーに、値をObject型で格納するMapをフィールドに保持する。

  • ResultSet の Stream 化 - なんとなくな Developer のメモ Spliteratorインタフェースを実装したクラスを定義している。ResultSetから任意の型のオブジェクトに変換するFunction(検査例外をスローする独自インタフェース)を受け取り、Stream処理内からSQLExceptionをスローし外側でキャッチできるようにしている。

お試し実装

AbstractSpliteratorを継承したResultSetSpliteratorを定義し、ResultSetの各行を任意の型のオブジェクトに変換するFunctionを外から渡せるようにします。上述ブログ・書籍の最後の記事にほぼ沿った実装です。

ResultSetSpliterator クラス
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class ResultSetSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
    public static final int CHARACTERISTICS = Spliterator.IMMUTABLE | Spliterator.NONNULL;
    private final ResultSet resultSet;
    private TryFunction<ResultSet, T, SQLException> converter;

    public ResultSetSpliterator(
            ResultSet resultSet, TryFunction<ResultSet, T, SQLException> converter
    ) {
        this(resultSet, converter, 0);
    }

    public ResultSetSpliterator(
            ResultSet resultSet, TryFunction<ResultSet, T, SQLException> converter, int additionalCharacteristics
    ) {
        super(Long.MAX_VALUE, CHARACTERISTICS | additionalCharacteristics);
        Objects.requireNonNull(resultSet, "result set");
        this.resultSet = resultSet;
        this.converter = converter;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        Objects.requireNonNull(action, "action to be performed");
        try {
            if (resultSet.isClosed() || !resultSet.next()) {
                return false;
            }
            action.accept(converter.apply(resultSet));
            return true;
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }
    }

    public Stream<T> stream() {
        return StreamSupport.stream(this, false);
    }
}
TryFunction<T, R, E>クラス
@FunctionalInterface
public interface TryFunction<T, R, E extends Exception> {
    R apply(T t) throws E;
}

使用例

ResultSetをStreamにしたら、次のように記述できます。

    List<MyModel> getMyModelListOfDelta(int delta) {
        try (PreparedStatement statement = conn.prepareStatement("SELECT alfa,bravo,charlie,kilo FROM Phonetics WHERE delta=?")) {
            statement.setInt(1, delta);
            ResultSet result = statement.executeQuery();
            return new ResultSetSpliterator<>(result, r -> new MyModel(
                    r.getInt("alfa"), r.getNString("bravo"), r.getDouble("charlie"), r.getObject("kilo", LocalDateTime.class)
            ).stream().collect(toList());
        } catch (SQLException ex) {
            return List.of();
        }
    }

ここでは、ResultSetからオブジェクトへの変換をラムダ式でインライン記述しています。 複数個所で同じラムダ式を記述する必要が生じた場合は、ラムダ式をフィールドに定義して再利用する等ができます。

class Xxx {
    private TryFunction<ResultSet, MyModel, SQLException> resultSet2MyModel = r -> new MyModel(
            r.getInt("alfa"), r.getNString("bravo"), r.getDouble("charlie"), r.getObject("kilo", LocalDateTime.class)
    );

    List<MyModel> getAllMyModel() {
        try (var stmt = conn.prepareStatement("SELECT alfa,bravo,charlie, kilo FROM Phonetics")) {
            var resultSet = stmt.executeQuery();
            return new ResultSetSpliterator<>(resultSet, resultSet2MyModel).stream().collect(toList());
        } catch (SQLException ex) {
            return List.of();
        }
    }
}