Интерфейс JDBC ориентирован на работу с декларативными текстовыми запросами (проще говоря — с sql запросами). Однако имея уже установленное соединение с базой напрямую отправить запрос нельзя. Вначале необходимо получить из соединения объект запроса и работать уже с ним.
Statement
Объект запроса получается простым обращением к объекту соединения:
1
|
Statement dataQuery = db.createStatement()
|
У интерфейса Statement есть три главных метода: executeQuery(), executeUpdate() и execute(). Первый метод ориентирован на запросы, возвращающие данные (select запросы) и возвращает объект данных (result set). Второй метод, executeUpdate(), служит для выполнения запросов не возвращающих данных, таких как insert или update. Этот метод возвращает число строк, которые затронул запрос. Последний метод, execute(), подходит для всех типов запросов и своим значением сообщает, вернул запрос данные или нет: true если запрос вернул данные и false если нет. В первом случае у объекта Statement можно запросить полученный ResultSet вызовом метода getResultSet().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
try (Statement dataQuery = db.createStatement()) {
dataQuery.execute(CREATE_QUERY);
for(int i=1; i<=5; i++) {
dataQuery.executeUpdate("INSERT INTO ORDER_ITEMS (CLIENT_ID, ORDER_ID, ITEM_ID) values (1, 1, "+i+")");
}
}
try (Statement results = db.createStatement()) {
try (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("ITEM_ID")));
}
}
}
|
Экземпляр Statement можно использовать несколько раз и, используя один и тот же объект, выполнять несколько запросов. Есть ровно одно условие — так как объект ResultSet жёстко привязан к Statement, его породившему, то любой следующий запрос закрывает ранее созданный ResultSet и, если требуется, создаёт новый ResultSet. Таким образом, если требуется одновременно работать с несколькими ResultSet, необходимо иметь и несколько Statement.
1
2
3
4
5
6
7
|
try (Statement results = db.createStatement()) {
try (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("ITEM_ID")));
}
}
}
|
PreparedStatement
Главным недостатком интерфейса Statement я бы назвал необходимость передавать весь запрос целиком в виде строки, с параметрами и данными. Во-первых это приводит к неуклюжим конструкциям вида "INSERT INTO ORDER_ITEMS (CLIENT_ID, ORDER_ID, ITEM_ID) values (1, 1, "+i+")". Во-вторых, при формировании такого запроса очень легко ошибиться, особенно если он собирается из несколько строк и длинее моего примера раз в пять. В третьих, это прямой путь к sql инъекции, что в 21м веке право слово смешно.
Для решения этих проблем в JDBC предусмотрен интерфейс PreparedStatement, который расширяет Statement. Создаётся он тоже из объекта соединения, причём при создании сразу требуется передать запрос. Запрос этот может (и скорее всего будет) содержать параметры, которые на момент создания запроса неизвестны и будут установлены позднее.
Использование PreparedStatement позволяет убить сразу небольшую стайку зайцев: запрос один раз компилируется и оптимизируется на стороне базы данных; параметры передаются безопасно и не могут вызвать sql инъекцию; код становится читабельнее и запрос проще переиспользовать:
1
2
3
4
5
6
7
|
try (PreparedStatement query =
db.prepareStatement("INSERT INTO ORDER_ITEMS (CLIENT_ID, ORDER_ID, ITEM_ID) values (1, 2, ?)")) {
for(int i=1; i<=5; i++) {
query.setInt(1, i);
query.executeUpdate();
}
}
|
Самое интересное использование PreparedStatement, это использование в batch режиме. И Statement и, соответственно, PreparedStatement поддерживает режим, в котором запросы выполняются не сразу, а накапливаются и потом выполняются одним большим запросом. Например, если вы делаете несколько update запросов, то даже при использовании PreparedStatement каждый запрос будет выполнен как отдельный самостоятельный запрос. В batch режиме запросы будут отправлены одним заданием, что эффективнее.
1
2
3
4
5
6
7
8
9
|
try (PreparedStatement batch =
db.prepareStatement("INSERT INTO ORDER_ITEMS (CLIENT_ID, ORDER_ID, ITEM_ID) values (1, ?, ?)")) {
for(int i=1; i<=5; i++) {
batch.setInt(1, 3);
batch.setInt(2, i);
batch.addBatch();
}
batch.executeBatch();
}
|
У batch режима есть ограничение: запросы, которые возвращают данные (select) не поддерживаются. Попытка выполнить такой запрос приведёт к BatchUpdateException.
CallableStatement
Другой интерфейс, расширяющий Statement служит для вызова sql процедур. Отдельный вариант Statement понадобился потому, что процедура имеет не только входные параметры, как обычный запрос, но и изменяемые параметры и возвращаемое значение. CallableStatement позволяет назначать входные параметры, связывать переменные и получать результат, возвращаемый процедурой.
1
2
3
4
5
6
|
try(CallableStatement upperProc = db.prepareCall("{ ? = call upper( ? ) }")) {
upperProc.registerOutParameter(1, Types.VARCHAR);
upperProc.setString(2, "lowercase to uppercase");
upperProc.execute();
System.out.println(upperProc.getString(1));
}
|
Код примера доступен на github.