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