Java JDBC ResultSet
Перебирайте строки SQL-запроса в Java с помощью интерфейса ResultSet.
ResultSet — это курсор по строкам, возвращённым запросом. Он не держит все строки в памяти одновременно: он указывает на одну строку за раз, и вы перемещаетесь по ним вперёд. Важно: свежий ResultSet расположен перед первой строкой — нужно один раз вызвать next(), чтобы встать на неё. Именно поэтому каждый цикл чтения имеет вид while (rs.next()).
ResultSet возвращается в результате запроса, выполненного через Statement или PreparedStatement. На этой странице рассматривается, как перемещать этот курсор и безопасно читать столбцы.
Цикл чтения
String sql = "SELECT id, name, score FROM player ORDER BY score DESC";
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) { // advance; false when no more rows
int id = rs.getInt("id"); // read by column name...
String name = rs.getString(2); // ...or by 1-based index
int score = rs.getInt("score");
System.out.println(id + " " + name + " " + score);
}
}Чтение столбцов: по имени или по индексу
Оба варианта работают. Имена столбцов более понятны и не ломаются при изменении порядка в SELECT; индексы столбцов (1-based, как всё в JDBC) чуть быстрее. Имена почти всегда выигрывают по поддерживаемости кода. Если во время выполнения нужны имена, типы или количество столбцов — например, для отрисовки универсальной таблицы — прочитайте их из ResultSetMetaData, которые получите с помощью rs.getMetaData().
Проблема NULL и wasNull()
Геттеры примитивных типов не могут возвращать null. getInt возвращает 0 для SQL NULL, getDouble — 0.0 и т.д. — что неотличимо от реального нуля. Когда столбец допускает NULL и разница важна, вызовите rs.wasNull() сразу после геттера:
int score = rs.getInt("score");
if (rs.wasNull()) { /* it was SQL NULL, not a zero */ }(Для объектных типов getObject возвращает null напрямую — это нередко удобнее.)
Тип курсора и параллельный доступ
По умолчанию ResultSet имеет тип TYPE_FORWARD_ONLY и CONCUR_READ_ONLY — можно двигаться только вперёд и только читать. Запросите TYPE_SCROLL_INSENSITIVE, чтобы использовать previous(), absolute(n) или first(), либо CONCUR_UPDATABLE, чтобы редактировать строки на месте. Это стоит дороже, поэтому запрашивайте их только при необходимости.
Закрытие (try-with-resources делает это)
ResultSet держит серверный курсор; если оставить его открытым, это расходует ресурсы базы данных. Закрытие Statement закрывает и его, а try-with-resources обрабатывает и то, и другое. Никогда не возвращайте открытый ResultSet из метода, чей Connection уже закрыт.
Практический пример: однонаправленный курсор и wasNull
Эта программа моделирует курсор ResultSet с помощью небольшого списка в памяти — стартует перед первой строкой, перемещается вперёд шагом в стиле next() — и воспроизводит поведение getInt-возвращает-0-при-NULL с проверкой wasNull, а также константы курсора.
Что следует вынести из запуска:
- Курсор начинается с индекса
-1— перед первой строкой — а шаг++cursorвоспроизводитrs.next(): сначала продвинуться, потом читать. Именно поэтому настоящий цикл — этоwhile (rs.next()), и нельзя читать столбец до первого вызоваnext(). - Строки читаются по одной, а не загружаются списком. Модель использует список для простоты, но настоящий
ResultSetполучает строки с сервера потоком — именно это позволяет ему обрабатывать наборы результатов, значительно превышающие объём памяти. - Оценка
nullу Линуса напечаталась как0— та самая ловушкаgetInt. Без флага нельзя отличить отсутствующую оценку от настоящего нуля. - Маркер
(wasNull)— это средство различения. В настоящем JDBC вы вызываетеrs.wasNull()сразу после геттера, потому что он сообщает о последнем прочитанном столбце — прочитайте другой столбец сначала, и ответ изменится. - Константы (
TYPE_FORWARD_ONLY,CONCUR_READ_ONLY,FETCH_FORWARD) описывают курсор по умолчанию — самый дешёвый: двигаться вперёд, только читать. Прокручиваемые или обновляемые курсоры нужно запрашивать явно, так как они требуют от сервера дополнительной работы.