Доступ к данным, в частности доступ к SQL из Java, никогда не был приятным. Во многом это связано с тем, что в API JDBC много церемоний.
Java 7 значительно улучшила работу с блоками ARM, отняв много церемоний по управлению объектами базы данных, такими как Statements и ResultSets, но в основном поток кода остался прежним.
Java 8 Lambdas дает нам очень хороший инструмент для улучшения потока JDBC.
Первая попытка улучшить это очень просто — упростить работу с java.sql.ResultSet.
Здесь мы просто оборачиваем итерацию ResultSet, а затем делегируем ее в функцию Lambda.
Это очень похоже на концепцию Spring JDBCTemplate .
НОТА: Я выпустил все фрагменты кода, которые вы видите здесь, под лицензией Apache 2.0 на Github.
Сначала мы создаем функциональный интерфейс с именем ResultSetProcessor следующим образом:
@FunctionalInterface public interface ResultSetProcessor { public void process(ResultSet resultSet, long currentRow) throws SQLException; }
Очень просто. Этот интерфейс принимает ResultSet и текущую строку ResultSet в качестве параметра.
Затем мы пишем простую утилиту, которая выполняет запрос и затем вызывает наш ResultSetProcessor каждый раз, когда мы перебираем ResultSet :
public static void select(Connection connection, String sql, ResultSetProcessor processor, Object... params) { try (PreparedStatement ps = connection.prepareStatement(sql)) { int cnt = 0; for (Object param : params) { ps.setObject(++cnt, param)); } try (ResultSet rs = ps.executeQuery()) { long rowCnt = 0; while (rs.next()) { processor.process(rs, rowCnt++); } } catch (SQLException e) { throw new DataAccessException(e); } } catch (SQLException e) { throw new DataAccessException(e); } }
Обратите внимание, что я обернул SQLException в свой собственный непроверенный DataAccessException .
Теперь, когда мы пишем запрос, это так же просто, как вызвать метод select с подключением и запросом:
select(connection, "select * from MY_TABLE",(rs, cnt)-> { System.out.println(rs.getInt(1)+" "+cnt) });
Это замечательно, но я думаю, что мы можем сделать больше …
Одним из замечательных дополнений Lambda в Java является новый Streams API . Это позволило бы нам добавить очень мощную функциональность для обработки ResultSet .
Однако использование Streams API через ResultSet создает немного больше проблем, чем простой выбор с помощью Lambda в предыдущем примере.
Я решил пойти по этому пути, создав собственный тип Tuple, представляющий одну строку из ResultSet .
Мой кортеж здесь реляционная версия, где Tuple — это набор элементов, где каждый элемент идентифицируется атрибутом, в основном набор пар ключ-значение. В нашем случае кортеж упорядочен по порядку столбцов в ResultSet .
Код для Tuple оказался совсем немного, так что если вы хотите взглянуть, посмотрите проект GitHub в ресурсах в конце поста.
В настоящее время API Java 8 предоставляет объект java.util.stream.StreamSupport, который предоставляет набор статических методов для создания экземпляров java.util.stream.Stream . Мы можем использовать этот объект для создания экземпляра потока.
Но для создания потока необходим экземплярjava.util.stream.Spliterator . Это специализированный тип для итерации и разбиения последовательности элементов, который необходим Stream для параллельной обработки операций.
К счастью, API Java 8 также предоставляет класс java.util.stream.Spliterators, который может обернуть существующие типы коллекций и перечислений. Одним из таких типов является java.util.Iterator .
Теперь мы заключаем запрос и ResultSet в итератор:
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; } } }
Этот класс в основном делегирует методы итератора в базовый набор результатов, а затем при вызове next () преобразует текущую строку в ResultSet в мой тип Tuple .
И это основа (этот класс потребует немного больше работы, хотя). Осталось только соединить все вместе, чтобы создать объект Stream . Обратите внимание, что из-за характера ResultSet не стоит пытаться обрабатывать их параллельно, поэтому наш поток не может обрабатывать их параллельно.
public static Stream stream(final Connection connection, final String sql, final Object... parms) { return StreamSupport .stream(Spliterators.spliteratorUnknownSize( new ResultSetIterator(connection, sql), 0), false); }
Теперь это просто для потоковой передачи запроса. В приведенном ниже примере использования у меня есть таблица TEST_TABLE с целочисленным столбцом TEST_ID, которая в основном отфильтровывает все не четные числа, а затем выполняет подсчет:
long result = stream(connection, "select TEST_ID from TEST_TABLE") .filter((t) -> t.asInt("TEST_ID") % 2 == 0) .limit(100) .count();
Вот и все! Теперь у нас есть очень мощный способ работы с ResultSet .
Так что весь этот код доступен под лицензией Apache 2.0 на GitHub здесь . Я довольно слабо назвал проект «лямбда-кортежами», и на самом деле его цель — поэкспериментировать и посмотреть, где можно получить доступ к Java 8 и реляционной БД, поэтому, пожалуйста, загрузите или не стесняйтесь вносить свой вклад.