Базы данных всё таки о данных, а не о запросах. В JDBC данные, которые возвращают запросы, представлены в виде объектов ResultSet.
Для доступа к данным интерфейс 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 немного не интуитивно, но сравнительно просто: перемещаем курсор в нужные строки и запрашиваем содержимое столбцов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | protected static void readResultSet(Connection db) throws SQLException { System.out.println("Dumping ORDER_ITEMS table:"); try (Statement results = db.createStatement()) { ResultSet rs = results.executeQuery("SELECT * FROM ORDER_ITEMS"); while (rs.next()) { System.out.println( String.format( "client=%d, order=%d, item=%d", rs.getInt("CLIENT_ID"), rs.getInt("ORDER_ID"), rs.getInt(3))); } } } |
1 2 3 4 5 6 7 8 9 10 11 | Dumping ORDER_ITEMS table: client=1, order=1, item=1 client=1, order=1, item=2 client=1, order=1, item=3 client=1, order=1, item=4 client=1, order=1, item=5 client=2, order=2, item=1 [skip] client=3, order=3, item=4 client=3, order=3, item=5 |
Методы getType() возвращают объект нужного типа, с учётом типа в базе данных. К столбцам можно обращаться как по имени, причем без учёта регистра, так и по номеру, причём отсчёт столбцов начинается с единицы.
Запись в ResultSet
Да да, именно запись в ResultSet. JDBC позволяет не только читать данные из ResultSet, но и записывать их обратно и эти изменения будут автоматически переданы в базу. Конечно, такое поведение может быть несколько непривычно для тех, кто работает с SQL базами данных, но JDBC рассчитан не только на SQL базы, но и например, на какой-нибудь там FoxPro, в котором именно так данные и обновляют (или обновляли, лет 20 назад).
Обновление данных производится не сложнее, чем чтение: выбираем строку, пишем в столбцы и сохраняем.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | protected static void updateResultSet(Connection db) throws SQLException { try (Statement updatableResult = db.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) { ResultSet rs = updatableResult.executeQuery("SELECT * FROM ORDER_ITEMS"); rs.absolute(5); rs.updateInt("CLIENT_ID", 2); rs.updateRow(); rs.moveToInsertRow(); rs.updateInt("CLIENT_ID", 1); rs.updateInt("ORDER_ID", 1); rs.updateInt("ITEM_ID", 10); rs.insertRow(); } } |
1 2 3 4 5 6 7 8 9 10 | Dumping ORDER_ITEMS table: client=1, order=1, item=1 client=1, order=1, item=2 client=1, order=1, item=3 client=1, order=1, item=4 client=2, order=1, item=5 client=2, order=2, item=1 [skip] client=3, order=3, item=5 client=1, order=1, item=10 |
Вторая часть кода из примера показывает, что строки можно не только обновлять но и добавлять! Добавляются строки почти так же, как и обновляются: перемещаемся в специальное место(ага, девятый метод позиционирования в ResultSet), обновляем значения столбцов, вставляем строку.
Опять таки, не всякий ResultSet может обновлять данные, а только созданный с CONCUR_UPDATABLE и только если драйвер JDBC этот режим поддерживает.
RowSet
RowSet расширяет ResultSet и делает его совместимым с концепцией JavaBean (то есть с конструктором по умолчанию, сериализуемым и т.д.). Поскольку интерфейс RowSet расширяет интерфейс ResultSet, весь вышеперечисленный функционал, разумеется, остаётся доступным и в RowSet. Главными отличиями RowSet от ResultSet является тот факт, что RowSet есть JavaBean, со свойствами и нотификациями. Кроме того, RowSet можно строить напрямую из соединения с базой, пропуская отдельно создание запроса.Самая простая реализация RowSet — JdbcRowSet, обычно обёртка над ResultSet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | protected static void jdbcRowSet(Connection db) throws SQLException { JdbcRowSet rs = new JdbcRowSetImpl(db); rs.setCommand("SELECT * FROM ORDER_ITEMS"); rs.execute(); rs.moveToInsertRow(); rs.updateInt("CLIENT_ID", 1); rs.updateInt("ORDER_ID", 1); rs.updateInt("ITEM_ID", 11); rs.insertRow(); rs.execute(); rs.beforeFirst(); System.out.println("Dumping ORDER_ITEMS table using RowSet:"); while (rs.next()) { System.out.println( String.format( client=%d, order=%d, item=%d", rs.getInt("CLIENT_ID"), rs.getInt("ORDER_ID"), rs.getInt("ITEM_ID"))); } } |
JdbcRowSet можно создать как в примере выше, а можно напрямую из ResultSet. Этот тип RowSet, так же как и ResultSet, требует наличия соединения с базой для работы с данными.
CachedRowSet
CachedRowSet, оправдывая своё название, сохраняет работоспособность и тогда, когда соединение с базой уже закрыто, сразу кэшируя в память все данные, которые вернул запрос.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | protected static CachedRowSet cachedRowSet(Connection db) throws SQLException { try (Statement results = db.createStatement()) { ResultSet rs = results.executeQuery("SELECT * FROM ORDER_ITEMS"); CachedRowSet cs = new CachedRowSetImpl(); cs.populate(rs); return cs; } } public static void main(final String[] args) { CachedRowSet cs = null; try (Connection db = DriverManager.getConnection("jdbc:h2:mem:")) { cs = cachedRowSet(db); } catch (SQLException ex) { System.out.println("Database connection failure: " + ex.getMessage()); } catch (IOException ex) { System.out.println("I/O error: " + ex.getMessage()); } try { System.out.println("Dumping ORDER_ITEMS table without database connection:"); assert cs != null; while(cs.next()) { System.out.println( String.format( "client=%d, order=%d, item=%d", cs.getInt("CLIENT_ID"), cs.getInt("ORDER_ID"), cs.getInt("ITEM_ID"))); } } catch (SQLException ex) { System.out.println("Database connection failure: " + ex.getMessage()); } } |
JoinRowSet
Делает слияние таблиц в памяти. Конечно, с точки зрения эффективности выгоднее делать join непосредственно на стороне базы, но не всякая база умеет join (вспоминаем FoxPro опять, ага). Чтобы слить таблицы, вначале необходимо получить две таблицы для слияния и потом добавить их в JoinRowSet, указав по какому полю сливать их.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | protected static void joinRowSet(Connection db) throws SQLException { CachedRowSet orders = new CachedRowSetImpl(); CachedRowSet clients = new CachedRowSetImpl(); try (Statement results = db.createStatement()) { ResultSet rs = results.executeQuery("SELECT * FROM ORDER_ITEMS"); orders.populate(rs); } try (Statement results = db.createStatement()) { ResultSet rs = results.executeQuery("SELECT * FROM CLIENTS"); clients.populate(rs); } JoinRowSet jrs = new JoinRowSetImpl(); jrs.addRowSet(orders, "CLIENT_ID"); jrs.addRowSet(clients, "ID"); System.out.println("Dumping client logins and their items:"); while (jrs.next()) { System.out.println( String.format( "client=%s, order=%d", jrs.getString("LOGIN"), jrs.getInt("ORDER_ID"))); } } |
1 2 3 4 5 | client=test, order=1 client=test, order=1 client=example, order=3 client=example, order=3 [skip] |
После для слияния указывается для каждого участника слияния отдельно. Можно использовать или название столбца (регистронезависимое) или номер (нумерация начинается с единицы). Тип столбца в обоих таблицах должен совпадать.
FilteredRowSet
RowSet который умеет сам себя фильтровать. На первый вгляд кажется бесполезной вещью (даже FoxPro умеет в условия), но на самом деле очень удобен, так как фильтры в коде могут быть гораздо гибче, чем условия выборки в базе. Да и фильтровать какой-нибудь постоянно висящий в памяти словарь становится выгоднее, чем постоянно его перезапрашивать.Создаётся и наполняется данными FilteredRowSet как обычно, а потом ему методом setFilter() назначается объект класса, реализующего интерфейс Predicate. Причём это не тот модный функциональный интерфейс Predicate, который используется с лямбда выражениями и потоками, а собственный кондовый Predicate из JDBC. Так что никаких лямбд 🙁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | private static class ClientFilter implements Predicate { @Override public boolean evaluate(RowSet rs) { try { return rs.getInt("CLIENT_ID") == 3; } catch (SQLException e) { return false; } } @Override public boolean evaluate(Object value, int column) throws SQLException { return !(column == 1 && !"3".equals(value)); } @Override public boolean evaluate(Object value, String columnName) throws SQLException { return !("CLIENT_ID".equals(columnName) && !"3".equals((String) value)); } } protected static void filteredRowSet(Connection db) throws SQLException { try (Statement results = db.createStatement()) { ResultSet rs = results.executeQuery("SELECT * FROM ORDER_ITEMS"); FilteredRowSet fs = new FilteredRowSetImpl(); fs.populate(rs); fs.setFilter(new ClientFilter()); System.out.println("Dumping only 3rd client from ORDER_ITEMS table:"); while (fs.next()) { System.out.println( String.format( "client=%d, order=%d, item=%d", fs.getInt("CLIENT_ID"), fs.getInt("ORDER_ID"), fs.getInt("ITEM_ID"))); } } } } |
1 2 3 4 5 6 | Dumping only 3rd client from ORDER_ITEMS table: client=3, order=3, item=1 client=3, order=3, item=2 client=3, order=3, item=3 client=3, order=3, item=4 client=3, order=3, item=5 |
WebRowSet
WebRowSet умеет сам сохранять себя в XML и создавать себя из XML же. В том году, когда его изобретали, это было удивительным достижением конечно, а сейчас выглядит слегка архаично.1 2 3 4 5 6 7 8 9 10 | protected static void webRowSet(Connection db) throws SQLException, IOException { try (Statement results = db.createStatement()) { ResultSet rs = results.executeQuery("SELECT * FROM ORDER_ITEMS"); WebRowSet ws = new WebRowSetImpl(); ws.populate(rs); ws.writeXml(System.out); } } |
XML представление
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | <?xml version="1.0"?> <webRowSet xmlns="http://java.sun.com/xml/ns/jdbc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/jdbc http://java.sun.com/xml/ns/jdbc/webrowset.xsd"> <properties> <command><null/></command> <concurrency>1008</concurrency> <datasource><null/></datasource> <escape-processing>true</escape-processing> <fetch-direction>1000</fetch-direction> <fetch-size>0</fetch-size> <isolation-level>2</isolation-level> <key-columns> </key-columns> <map> </map> <max-field-size>0</max-field-size> <max-rows>0</max-rows> <query-timeout>0</query-timeout> <read-only>true</read-only> <rowset-type>ResultSet.TYPE_SCROLL_INSENSITIVE</rowset-type> <show-deleted>false</show-deleted> <table-name><null/></table-name> <url><null/></url> <sync-provider> <sync-provider-name>com.sun.rowset.providers.RIOptimisticProvider</sync-provider-name> <sync-provider-vendor>Oracle Corporation</sync-provider-vendor> <sync-provider-version>1.0</sync-provider-version> <sync-provider-grade>2</sync-provider-grade> <data-source-lock>1</data-source-lock> </sync-provider> </properties> <metadata> <column-count>4</column-count> <column-definition> <column-index>1</column-index> <auto-increment>true</auto-increment> <case-sensitive>true</case-sensitive> <currency>false</currency> <nullable>0</nullable> <signed>true</signed> <searchable>true</searchable> <column-display-size>20</column-display-size> <column-label>ID</column-label> <column-name>ID</column-name> <schema-name>PUBLIC</schema-name> <column-precision>19</column-precision> <column-scale>0</column-scale> <table-name>ORDER_ITEMS</table-name> <catalog-name>UNNAMED</catalog-name> <column-type>-5</column-type> <column-type-name>BIGINT</column-type-name> </column-definition> <column-definition> <column-index>2</column-index> <auto-increment>false</auto-increment> <case-sensitive>true</case-sensitive> <currency>false</currency> <nullable>0</nullable> <signed>true</signed> <searchable>true</searchable> <column-display-size>20</column-display-size> <column-label>CLIENT_ID</column-label> <column-name>CLIENT_ID</column-name> <schema-name>PUBLIC</schema-name> <column-precision>19</column-precision> <column-scale>0</column-scale> <table-name>ORDER_ITEMS</table-name> <catalog-name>UNNAMED</catalog-name> <column-type>-5</column-type> <column-type-name>BIGINT</column-type-name> </column-definition> <column-definition> <column-index>3</column-index> <auto-increment>false</auto-increment> <case-sensitive>true</case-sensitive> <currency>false</currency> <nullable>0</nullable> <signed>true</signed> <searchable>true</searchable> <column-display-size>11</column-display-size> <column-label>ORDER_ID</column-label> <column-name>ORDER_ID</column-name> <schema-name>PUBLIC</schema-name> <column-precision>10</column-precision> <column-scale>0</column-scale> <table-name>ORDER_ITEMS</table-name> <catalog-name>UNNAMED</catalog-name> <column-type>4</column-type> <column-type-name>INTEGER</column-type-name> </column-definition> <column-definition> <column-index>4</column-index> <auto-increment>false</auto-increment> <case-sensitive>true</case-sensitive> <currency>false</currency> <nullable>0</nullable> <signed>true</signed> <searchable>true</searchable> <column-display-size>11</column-display-size> <column-label>ITEM_ID</column-label> <column-name>ITEM_ID</column-name> <schema-name>PUBLIC</schema-name> <column-precision>10</column-precision> <column-scale>0</column-scale> <table-name>ORDER_ITEMS</table-name> <catalog-name>UNNAMED</catalog-name> <column-type>4</column-type> <column-type-name>INTEGER</column-type-name> </column-definition> </metadata> <data> <currentRow> <columnValue>1</columnValue> <columnValue>1</columnValue> <columnValue>1</columnValue> <columnValue>1</columnValue> </currentRow> <currentRow> <columnValue>2</columnValue> <columnValue>1</columnValue> <columnValue>1</columnValue> <columnValue>2</columnValue> </currentRow> <currentRow> <columnValue>3</columnValue> <columnValue>1</columnValue> <columnValue>1</columnValue> <columnValue>3</columnValue> </currentRow> <currentRow> <columnValue>4</columnValue> <columnValue>1</columnValue> <columnValue>1</columnValue> <columnValue>4</columnValue> </currentRow> <currentRow> <columnValue>5</columnValue> <columnValue>2</columnValue> <columnValue>1</columnValue> <columnValue>5</columnValue> </currentRow> <currentRow> <columnValue>6</columnValue> <columnValue>2</columnValue> <columnValue>2</columnValue> <columnValue>1</columnValue> </currentRow> <currentRow> <columnValue>7</columnValue> <columnValue>2</columnValue> <columnValue>2</columnValue> <columnValue>2</columnValue> </currentRow> <currentRow> <columnValue>8</columnValue> <columnValue>2</columnValue> <columnValue>2</columnValue> <columnValue>3</columnValue> </currentRow> <currentRow> <columnValue>9</columnValue> <columnValue>2</columnValue> <columnValue>2</columnValue> <columnValue>4</columnValue> </currentRow> <currentRow> <columnValue>10</columnValue> <columnValue>2</columnValue> <columnValue>2</columnValue> <columnValue>5</columnValue> </currentRow> <currentRow> <columnValue>11</columnValue> <columnValue>3</columnValue> <columnValue>3</columnValue> <columnValue>1</columnValue> </currentRow> <currentRow> <columnValue>12</columnValue> <columnValue>3</columnValue> <columnValue>3</columnValue> <columnValue>2</columnValue> </currentRow> <currentRow> <columnValue>13</columnValue> <columnValue>3</columnValue> <columnValue>3</columnValue> <columnValue>3</columnValue> </currentRow> <currentRow> <columnValue>14</columnValue> <columnValue>3</columnValue> <columnValue>3</columnValue> <columnValue>4</columnValue> </currentRow> <currentRow> <columnValue>15</columnValue> <columnValue>3</columnValue> <columnValue>3</columnValue> <columnValue>5</columnValue> </currentRow> <currentRow> <columnValue>16</columnValue> <columnValue>1</columnValue> <columnValue>1</columnValue> <columnValue>10</columnValue> </currentRow> <currentRow> <columnValue>17</columnValue> <columnValue>1</columnValue> <columnValue>1</columnValue> <columnValue>11</columnValue> </currentRow> </data> </webRowSet> |
ResultSet vs RowSet
Что же выбрать? Оба интерфейса выглядят хорошо и сравнительно одинаково. Какой из них использовать? Ответ мой любимый: «это зависит». С одной стороны, ResultSet выглядит более низкоуровневым и неудобным. В RowSet можно и listeners приделывать и в памяти сразу фильтровать и работать с данными в отсутствие базы (и обновлять кстати можно тоже). Но. Цена этого удобства — память. ResultSet в общем виде может не иметь доступа более чем к одной строке результатов и обращаться к базе при каждом движении указателя. И это хорошо: во-первых можно начинать работу с данными, когда они только начали поступать, не дожидаясь, пока сформируется весь ответ. Во-вторых, если данных слишком много, а памяти слишком мало, может не получиться их обработать. Я могу дать только такой совет: если вам нужен функционал RowSet, используйте его. Если нет, выбирайте по ситуации, что использовать.
Код примера доступен на github.