JDBC ResultSet и RowSet

give-me-all-the-rowsБазы данных всё таки о данных, а не о запросах. В данные, которые возвращают запросы, представлены в виде объектов ResultSet.

ResultSet, в свою очередь, жёстко связан со Statement, который его породил и существует только до момента закрытия этого самого Statement  или даже раньше, до выполнения нового запроса в Statement.

Для доступа к данным интерфейс ResultSet реализует смесь шаблонов Итератор и Курсор: внутри ResultSet есть указатель, который указывает на какую-либо строку (или даже не на строку, а в никуда) в данных. Этот указатель можно передвигать программно и запрашивать данные из столбцов текущей строки. По умолчанию курсор ResultSet находится перед первой строкой набора данных.

Существует целых восемь методов, перемещающих курсор по ResultSet:

  • next() — перемещает курсор на одну строку вперёд. Возвращает true, если перемещение удалось и false, если курсор уже находится за последней строкой.
  • previous() — очевидно, антоним next(). Перемещает курсорс на одну строку назад и тоже возвращает true, если перемещение удалось и false, если курсор находится перед первой строкой.
  • first() и last() — перемещают курсор соответственно на первую и последнюю строку набора данных. В случае, если набор данных пуст, возвращают false. В случае успешного перемещения возвращают true.
  • beforeFirst() и afterLast() — перемещают курсор на позицию перед первой строкой или после последней строки.
  • relative() — перемещает курсор на указанное число строк от текущей позиции.
  • absolute() — перемещает курсор на указанное число строк от первой позиции.

Стоит отметить, что не все эти методы всегда работают. ResultSet (а точнее конкретная его реализация драйвером JDBC) может не поддерживать перемещение кроме как вперёд. Такой ResultSet называется TYPE_FORWARD_ONLY. В случае, если перемещение возможно, открытый ResultSet может следить за изменениями в базе данных, произошедшими после его открытия или не следить. В первом случае это будет TYPE_SCROLL_SENSITIVE ResultSet, во втором TYPE_SCROLL_INSENSITIVE.

Чтение из ResultSet

Читать из ResultSet немного не интуитивно, но сравнительно просто: перемещаем курсор в нужные строки и запрашиваем содержимое столбцов.

Методы getType() возвращают объект нужного типа, с учётом типа в базе данных. К столбцам можно обращаться как по имени, причем без учёта регистра, так и по номеру, причём отсчёт столбцов начинается с единицы.

Запись в ResultSet

Да да, именно запись в ResultSet. JDBC позволяет не только читать данные из ResultSet, но и записывать их обратно и эти изменения будут автоматически переданы в базу. Конечно, такое поведение может быть несколько непривычно для тех, кто работает с базами данных, но JDBC рассчитан не только на базы, но и например, на какой-нибудь там FoxPro, в котором именно так данные и обновляют (или обновляли, лет 20 назад).

Обновление данных производится не сложнее, чем чтение: выбираем строку, пишем в столбцы и сохраняем.

Вторая часть кода из примера показывает, что строки можно не только обновлять но и добавлять! Добавляются строки почти так же, как и обновляются: перемещаемся в специальное место(ага, девятый метод позиционирования в ResultSet), обновляем значения столбцов, вставляем строку.

Опять таки, не всякий ResultSet может обновлять данные, а только созданный с CONCUR_UPDATABLE и только если драйвер JDBC этот режим поддерживает.

RowSet

RowSet расширяет ResultSet и делает его совместимым с концепцией JavaBean (то есть с конструктором по умолчанию, сериализуемым и т.д.). Поскольку интерфейс RowSet расширяет интерфейс ResultSet, весь вышеперечисленный функционал, разумеется, остаётся доступным и в RowSet. Главными отличиями RowSet от ResultSet является тот факт, что RowSet есть JavaBean, со свойствами и нотификациями. Кроме того, RowSet можно строить напрямую из соединения с базой, пропуская отдельно создание запроса.

Самая простая реализация RowSet — JdbcRowSet, обычно обёртка над ResultSet:

JdbcRowSet можно создать как в примере выше, а можно напрямую из ResultSet. Этот тип RowSet, так же как и ResultSet, требует наличия соединения с базой для работы с данными.

CachedRowSet

CachedRowSet, оправдывая своё название, сохраняет работоспособность и тогда, когда соединение с базой уже закрыто, сразу кэшируя в память все данные, которые вернул запрос.
CachedRowSet можно спокойно возвращать и передавать куда угодно, не опасаясь, что его соединение с базой внезапно закроется и RowSet превратится в тыкву. Такой тип RowSet называется disconnected, как и все нижеописанные RowSet.

JoinRowSet

Делает слияние таблиц в памяти. Конечно, с точки зрения эффективности выгоднее делать join непосредственно на стороне базы, но не всякая база умеет join (вспоминаем FoxPro опять, ага). Чтобы слить таблицы, вначале необходимо получить две таблицы для слияния и потом добавить их в JoinRowSet, указав по какому полю сливать их.

После для слияния указывается для каждого участника слияния отдельно. Можно использовать или название столбца (регистронезависимое) или номер (нумерация начинается с единицы). Тип столбца в обоих таблицах должен совпадать.

FilteredRowSet

RowSet который умеет сам себя фильтровать. На первый вгляд кажется бесполезной вещью (даже FoxPro умеет в условия), но на самом деле очень удобен, так как фильтры в коде могут быть гораздо гибче, чем условия выборки в базе. Да и фильтровать какой-нибудь постоянно висящий в памяти словарь становится выгоднее, чем постоянно его перезапрашивать.

Создаётся и наполняется данными FilteredRowSet как обычно, а потом ему методом setFilter() назначается объект класса, реализующего интерфейс Predicate. Причём это не тот модный функциональный интерфейс Predicate, который используется с лямбда выражениями и потоками, а собственный кондовый Predicate из JDBC. Так что никаких лямбд 🙁

WebRowSet

WebRowSet умеет сам сохранять себя в XML и создавать себя из XML же. В том году, когда его изобретали, это было удивительным достижением конечно, а сейчас выглядит слегка архаично.

XML представление

Результат в XML настолько огромен и ужасен, что его приходится прятать под кат.

ResultSet vs RowSet

Что же выбрать? Оба интерфейса выглядят хорошо и сравнительно одинаково. Какой из них использовать? Ответ мой любимый: «это зависит». С одной стороны, ResultSet выглядит более низкоуровневым и неудобным. В RowSet можно и listeners приделывать и в памяти сразу фильтровать и работать с данными в отсутствие базы (и обновлять кстати можно тоже). Но. Цена этого удобства — память. ResultSet в общем виде может не иметь доступа более чем к одной строке результатов и обращаться к базе при каждом движении указателя. И это хорошо: во-первых можно начинать работу с данными, когда они только начали поступать, не дожидаясь, пока сформируется весь ответ. Во-вторых, если данных слишком много, а памяти слишком мало, может не получиться их обработать. Я могу дать только такой совет: если вам нужен функционал RowSet, используйте его. Если нет, выбирайте по ситуации, что использовать.

Код примера доступен на github.