Java.util.stream с ResultSet

У меня есть несколько таблиц с большим объемом данных (около 100 миллионов записей). Поэтому я не могу хранить эти данные в памяти, но я хотел бы передать этот набор результатов с помощью класса java.util.stream и передать этот поток в другой класс. Я читал о операторах Stream.of и Stream.Builder, но они являются буферизованными потоками в памяти. Так есть ли способ решить этот вопрос? Спасибо заранее.

ОБНОВЛЕНИЕ # 1

Хорошо, я googled и нашел jooq. Я не уверен, но похоже, что это может быть применимо к моему тестовому примеру. Подводя итог, у меня есть несколько таблиц с большим объемом данных. Я хотел бы передать свой набор результатов и передать этот поток другому методу. Что-то вроде этого:

// why return Stream<String>? Because my result set has String type
private Stream<Record> writeTableToStream(DataSource dataSource, String table) {

    Stream<Record> record = null;
    try (Connection connection = dataSource.getConnection()) {
        String sql = "select * from " + table;

        try (PreparedStatement pSt = connection.prepareStatement(sql)) {
            connection.setAutoCommit(false);
            pSt.setFetchSize(5000);
            ResultSet resultSet = pSt.executeQuery();
            //
            record = DSL.using(connection)
                    .fetch(resultSet).stream();
        }
    } catch (SQLException sqlEx) {
        logger.error(sqlEx);
    }

    return record;
}

Может ли кто-нибудь посоветовать, правильно? Спасибо.

ОБНОВЛЕНИЕ # 2

Я сделал несколько экспериментов на jooq и теперь могу сказать, что это решение для меня не подходит. Этот код record = DSL.using(connection).fetch(resultSet).stream(); занимает слишком много времени

Ответ 1

Первое, что вам нужно понять, это код, например

try (Connection connection = dataSource.getConnection()) {
    …
    try (PreparedStatement pSt = connection.prepareStatement(sql)) {
        …
        return stream;
    }
}

не работает по мере того, как вы покидаете блоки try, ресурсы закрываются, пока обработка Stream еще не началась.

Конструкция управления ресурсами "попробуйте с ресурсами" работает для ресурсов, используемых в пределах области блока внутри метода, но вы создаете метод factory, возвращающий ресурс. Поэтому вы должны убедиться, что закрытие возвращаемого потока закроет ресурсы, и вызывающий абонент отвечает за закрытие Stream.


Кроме того, вам нужна функция, которая выдает элемент из одной строки из ResultSet. Предположим, у вас есть метод вроде

Record createRecord(ResultSet rs) {
    …
}

вы можете создать Stream<Record> в основном как

Stream<Record> stream = StreamSupport.stream(new Spliterators.AbstractSpliterator<Record>(
    Long.MAX_VALUE,Spliterator.ORDERED) {
        @Override
        public boolean tryAdvance(Consumer<? super Record> action) {
            if(!resultSet.next()) return false;
            action.accept(createRecord(resultSet));
            return true;
        }
    }, false);

Но для правильной работы вам необходимо включить обработку исключений и закрытие ресурсов. Вы можете использовать Stream.onClose для регистрации действия, которое будет выполняться при закрытии Stream, но оно должно быть Runnable, которое не может выбрать проверенные исключения. Точно так же методу tryAdvance не разрешено выдавать проверенные исключения. И поскольку мы не можем просто вложить блоки try(…) здесь, логика программ исключений подавления, добавленных в close, когда есть уже ожидающее исключение, не приходит бесплатно.

Чтобы помочь нам здесь, мы вводим новый тип, который может завершать операции закрытия, которые могут выдавать проверенные исключения и доставлять их, завернутые в неконтролируемое исключение. Внедряя AutoCloseable самостоятельно, он может использовать конструкцию try(…) для безопасной цепочки операций:

interface UncheckedCloseable extends Runnable, AutoCloseable {
    default void run() {
        try { close(); } catch(Exception ex) { throw new RuntimeException(ex); }
    }
    static UncheckedCloseable wrap(AutoCloseable c) {
        return c::close;
    }
    default UncheckedCloseable nest(AutoCloseable c) {
        return ()->{ try(UncheckedCloseable c1=this) { c.close(); } };
    }
}

При этом вся операция становится:

private Stream<Record> tableAsStream(DataSource dataSource, String table)
    throws SQLException {

    UncheckedCloseable close=null;
    try {
        Connection connection = dataSource.getConnection();
        close=UncheckedCloseable.wrap(connection);
        String sql = "select * from " + table;
        PreparedStatement pSt = connection.prepareStatement(sql);
        close=close.nest(pSt);
        connection.setAutoCommit(false);
        pSt.setFetchSize(5000);
        ResultSet resultSet = pSt.executeQuery();
        close=close.nest(resultSet);
        return StreamSupport.stream(new Spliterators.AbstractSpliterator<Record>(
            Long.MAX_VALUE,Spliterator.ORDERED) {
            @Override
            public boolean tryAdvance(Consumer<? super Record> action) {
                try {
                    if(!resultSet.next()) return false;
                    action.accept(createRecord(resultSet));
                    return true;
                } catch(SQLException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }, false).onClose(close);
    } catch(SQLException sqlEx) {
        if(close!=null)
            try { close.close(); } catch(Exception ex) { sqlEx.addSuppressed(ex); }
        throw sqlEx;
    }
}

Этот метод завершает необходимую операцию закрытия для всех ресурсов Connection, Statement и ResultSet в одном экземпляре класса утилиты, описанного выше. Если во время инициализации возникает исключение, операция закрытия выполняется немедленно, и исключение доставляется вызывающему. Если построение потока завершается успешно, операция закрытия регистрируется через onClose.

Поэтому вызывающий должен обеспечить правильное закрытие, например

try(Stream<Record> s=tableAsStream(dataSource, table)) {
    // stream operation
}

Обратите внимание, что доставка SQLException через RuntimeException была добавлена ​​в метод tryAdvance. Поэтому теперь вы можете без проблем добавить throws SQLException в метод createRecord.

Ответ 2

jOOQ

Я собираюсь ответить на jOOQ часть вашего вопроса. Начиная с jOOQ 3.8, теперь появилось немало дополнительных функций, связанных с объединением jOOQ с Stream. Другие способы использования также документированы на этой странице JOOQ.

Ваше рекомендуемое использование:

Вы пробовали это:

Stream<Record> stream = DSL.using(connection).fetch(resultSet).stream();

Действительно, это не работает для больших наборов результатов, потому что fetch(ResultSet) извлекает весь набор результатов в память, а затем вызывает Collection.stream() для него.

Лучшее (ленивое) использование:

Вместо этого вы могли бы написать это:

try (Stream<Record> stream = DSL.using(connection).fetchStream(resultSet)) {
    ...
}

... что по сути удобно для этого:

try (Cursor<Record> cursor = DSL.using(connection).fetchLazy(resultSet)) {
    Stream<Record> stream = cursor.stream();
    ...
}

Смотрите также DSLContext.fetchStream(ResultSet)

Конечно, вы также можете позволить jOOQ выполнять вашу строку SQL, а не бороться с JDBC:

try (Stream<Record> stream = 
     DSL.using(dataSource)
        .resultQuery("select * from {0}", DSL.name(table)) // Prevent SQL injection
        .fetchSize(5000)
        .fetchStream()) {
    ...
}

Об использовании ресурсов с пробой

Обратите внимание, что Stream, созданный jOOQ, является "находчивым", то есть содержит ссылку на открытый ResultSetPreparedStatement). Итак, если вы действительно хотите вернуть этот поток за пределы вашего метода, убедитесь, что он закрыт правильно!

Ответ 3

Я не знаю ни одной известной библиотеки, которая сделает это за вас.

Тем не менее, в этой статье показано, как обернуть набор результатов с помощью Iterator (ResultSetIterator) и передать его в качестве первого параметра в Spliterators.spliteratorUnknownSize(), чтобы создать Spliterator.

Затем Spliterator можно использовать StreamSupport, чтобы создать Stream поверх него.

Предлагаемая реализация класса ResultSetIterator:

public class ResultSetIterator implements Iterator {

    private ResultSet rs;
    private PreparedStatement ps;
    private Connection connection;
    private String sql;

    public ResultSetIterator(Connection connection, String sql) {
        assert connection != null;
        assert sql != null;
        this.connection = connection;
        this.sql = sql;
    }

    public void init() {
        try {
            ps = connection.prepareStatement(sql);
            rs = ps.executeQuery();

        } catch (SQLException e) {
            close();
            throw new DataAccessException(e);
        }
    }

    @Override
    public boolean hasNext() {
        if (ps == null) {
            init();
        }
        try {
            boolean hasMore = rs.next();
            if (!hasMore) {
                close();
            }
            return hasMore;
        } catch (SQLException e) {
            close();
            throw new DataAccessException(e);
        }

    }

    private void close() {
        try {
            rs.close();
            try {
                ps.close();
            } catch (SQLException e) {
                //nothing we can do here
            }
        } catch (SQLException e) {
            //nothing we can do here
        }
    }

    @Override
    public Tuple next() {
        try {
            return SQL.rowAsTuple(sql, rs);
        } catch (DataAccessException e) {
            close();
            throw e;
        }
    }
}

а затем:

public static Stream stream(final Connection connection, 
                                       final String sql, 
                                       final Object... parms) {
  return StreamSupport
                .stream(Spliterators.spliteratorUnknownSize(
                        new ResultSetIterator(connection, sql), 0), false);
}

Ответ 4

Вот простейший пример от AbacusUtil.

final DataSource ds = JdbcUtil.createDataSource(url, user, password);
final SQLExecutor sqlExecutor = new SQLExecutor(ds);
sqlExecutor.stream(sql, parameters);

Раскрытие информации: Я разработчик AbacusUtil.

Ответ 5

Я просто сделал резюме, чтобы предоставить реальный пример о том, как передавать ResultSet и выполнять простой запрос SQL без использования 3-го нажмите здесь для подробностей

Цитата: Java 8 предоставила семейство Stream и простое управление им. Способ использования конвейера сделал код понятным и умным. Тем не менее, ResultSet все еще идет с очень устаревшим способом обработки. Для фактического использования ResultSet это действительно полезно, если преобразовать в поток.

....   StreamUtils.uncheckedConsumer требуется для преобразования SQLException в runtimeException, чтобы сделать Lamda прозрачной.