Базы данных всё таки о данных, а не о запросах. В JDBC данные, которые возвращают запросы, представлены в виде объектов 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 немного не интуитивно, но сравнительно просто: перемещаем курсор в нужные строки и запрашиваем содержимое столбцов.
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.