ResultSet の Stream 化

java.sql.ResultSetjava.util.stream.Stream 化する方法はいくつか考えられますが、今回は以下の方法を試してみました。

  • Spliterator インターフェースの実装クラスを作成

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20151026/

Spliterator インターフェースを直接実装

java.util.Spliterator インターフェースを直接実装する場合、以下の 4つのメソッドを実装します。

メソッド 内容 備考
tryAdvance 要素を個々にトラバースする 要素が存在する場合に引数の Consumer をその要素で実行し true を返す
estimateSize 残りの要素数の推定値を返す 不明な場合は Long.MAX_VALUE を返す
characteristics 構造や要素の特性を返す 複数の特性値を | で連結できる
trySplit 要素の一部を分割できる場合に分割する 分割できない場合は null を返す

今回は tryAdvance メソッドの引数 Consumer へ ResultSet を加工した値を渡すように、SQLException を伴う処理を TryFunction インターフェースとして扱っています。

characteristics では要素の順序が定義されている事を示す ORDERED のみを返すようにしていますが、(今回のように加工しないで) ResultSet をそのまま使うのなら NONNULL も追加できると思います。

なお、Stream を返す stream メソッドは、あると便利なので定義しているだけです。 (無くても問題ありません)

src/main/java/sample/ResultSetSpliterator.java
package sample;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class ResultSetSpliterator<T> implements Spliterator<T> {

    private ResultSet resultSet;
    // ResultSet を加工する処理
    private TryFunction<ResultSet, T, SQLException> converter;

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

    @Override
    public boolean tryAdvance(Consumer<? super T> consumer) {
        try {
            if (resultSet.next()) {
                consumer.accept(converter.apply(resultSet));
                return true;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    @Override
    public Spliterator<T> trySplit() {
        return null;
    }

    @Override
    public long estimateSize() {
        return Long.MAX_VALUE;
    }

    @Override
    public int characteristics() {
        return ORDERED;
    }

    // Stream 化して返す
    public Stream<T> stream() {
        return StreamSupport.stream(this, false);
    }
}

TryFunction の内容は次の通りです。

src/main/java/sample/TryFunction.java
package sample;

@FunctionalInterface
public interface TryFunction<T, R, E extends Exception> {
    R apply(T t) throws E;

    static <T, E extends Exception> TryFunction<T, T, E> identity() {
        return r -> r;
    }
}

動作確認

動作確認のための実行クラスを用意します。

src/main/java/sample/SampleApp.java
package sample;

import java.sql.*;

public class SampleApp {
    private static final String SQL = "select * from product";

    public static void main(String... args) throws Exception {
        try (Connection con = DriverManager.getConnection(args[0])) {
            sample1(con);

            System.out.println("---");

            sample2(con);
        }
    }

    private static void sample1(Connection con) throws SQLException {
        try (
            PreparedStatement ps = con.prepareStatement(SQL);
            ResultSet rs = ps.executeQuery()
        ) {
            new ResultSetSpliterator<>(
                rs,
                r -> r.getString("name")
            ).stream().forEach(System.out::println);
        }
    }

    private static void sample2(Connection con) throws SQLException {
        try (
            PreparedStatement ps = con.prepareStatement(SQL);
            ResultSet rs = ps.executeQuery()
        ) {
            long count = new ResultSetSpliterator<>(
                rs,
                TryFunction.identity()
            ).stream().count();

            System.out.println("count : " + count);
        }
    }
}

Gradle による実行結果は以下の通り。 一応、Stream として処理できているようです。

実行結果
> gradle run -Pargs="jdbc:mysql://localhost/sample?user=root"

・・・
sample1
sample2
sample3
---
count : 3

Gradle のビルド定義には以下を使いました。

build.gradle
apply plugin: 'application'

mainClassName = 'sample.SampleApp'

repositories {
    jcenter()
}

dependencies {
    runtime 'mysql:mysql-connector-java:5.1.36'
}

run {
    if (project.hasProperty('args')) {
        args project.args
    }
}

Spliterators.AbstractSpliterator をサブクラス化

直接 Spliterator インターフェースを実装するよりも、Spliterators.AbstractSpliterator のサブクラスとした方が、少しだけコードを減らせます。

この場合、estimateSize・characteristics の値は super コンストラクタで指定します。

src/main/java/sample/ResultSetSpliterator2.java
package sample;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class ResultSetSpliterator2<T> extends Spliterators.AbstractSpliterator<T> {

    private ResultSet resultSet;
    private TryFunction<ResultSet, T, SQLException> converter;

    public ResultSetSpliterator2(ResultSet resultSet, TryFunction<ResultSet, T, SQLException> converter) {
        // estimateSize・characteristics の値を指定
        super(Long.MAX_VALUE, ORDERED);

        this.resultSet = resultSet;
        this.converter = converter;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> consumer) {
        try {
            if (resultSet.next()) {
                consumer.accept(converter.apply(resultSet));
                return true;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    public Stream<T> stream() {
        return StreamSupport.stream(this, false);
    }
}