Статьи

Добавление Java 8 Lambda Goodness в JDBC

Доступ к данным, в частности доступ к 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 и реляционной БД, поэтому, пожалуйста, загрузите или не стесняйтесь вносить свой вклад.