JDBC
Ключевые интерфейсы:
Driver
;Connection
;Statement
;PreparedStatement
;CallableStatement
;ResultSet
;DriverManager
- OCP получает соединение (Connection) через него.
В случае запуска JVM в модульном режиме, JDBC требует подключения в module-info
как:
1 | requires java.sql; |
url
для подключения к БД выглядит следующим образом:
jdbc | : | ${dbtype} | : | //${host}:${port}/${database} |
---|---|---|---|---|
Протокол всегда JDBC | Протокол соединяет, фактически, имя вендора или продукта БД | Параметры подключения |
Получение соединения
Промышленный стандарт рекомендует использовать класс DataSource
. Вместо этого, OCP использует класс DriverManager
или фабрику для подключения. Ниже рассматривается именно он:
1 | Connection conn = DriverManager.getConnection("jdbc:..."); |
Некоторые вендоры требуют логина и пароля для соединения:
1 | Connection conn = DriverManager.getConnection("jdbc:...", "myDbUser", "myDbPass"); |
PreparedStatement
Призван для:
- Производительности. Строит план запроса на уровне БД;
- Безопасность. Защищает от SQL-инъекций;
- Читабельность. Запрос составляется отдельно; параметры передаются отдельно;
- Отложенное использование. Достаточно хранить ссылку на объект с запросом и передать параметры в другом участке кода.
1 | try(PreparedStatement pstmt = conn.prepareStatement("...")) { ... } |
Тело запроса обязательно, за его указанием необходимо следить.
executeUpdate
- выполняет толькоINSERT
,UPDATE
,DELETE
и подобные типы запросов. Возвращаетint
- количество фактических изменений (некоторые вендоры могут возвращать собственные коды);executeQuery
- возвращаетResultSet
, как результат выполненияSELECT
запроса;execute
- выполняет запрос и возвращаетboolean
флаг, который определяет, является ли результат выполненияResultSet
. Затем в зависимости отboolean
можно определить логику:
1 | boolean hasResultSet = pstmt.execute(); |
Параметры
Передаются как символ “?”. В случае отсутствия параметра, или указания лишнего - будет выведена ошибка. Основные setter-методы для параметров соответствуют типам. Если указать setObject
, то переданному параметру будет передан эквивалент. Для примитива будет найден wrapper-класс, который и будет использоваться:
1 | pstmt.setInt((int) 123); |
Аналогично:
1 | pstmt.setObject((int) 123); |
Batch запросы
JDBC API позволяет выполнять batch запросы:
1 | var sql = "..."; |
Результатом будет количество измененных строк в каждом запросе, а те, в свою очередь, будут указывать количество выполненных изменений. Количество элементов массива будет эквивалентно количеству выполненных addBatch
запросов.
Получение результатов SELECT
Использование ResultSet
(тоже является AutoCloseable
):
1 | try(ResultSet rs = pstmt.executeQuery("...")) { |
Проход по ResultSet
осуществляется с помощью курсора. В случае обращения к результату по несуществующей колонке (с не корректным именем или индексом), последует ошибка исполнения.
Как и на вход, ResultSet
имеет метод getObject
, который возвращает объект произвольного типа, зависящий от типа колонки. Впоследствии этот тип можно проверить стандартными средствами Java (например, instanceof
).
CallableStatement
Выполняет хранимую процедуру на стороне БД
1 | try(CallableStatement cstmt = conn.prepareCall("{call my_procedure()}")) { |
Передача параметров в процедуру допускает как используя имя, так и по индексу:
1 | cstmt.setString(1, "val"); |
При этом, аргумент для запроса так же надо указать:
1 | try(CallableStatement cstmt = conn.prepareCall("{call my_proc_with_args(my_arg=>?)}")) { |
Выполнение хранимой процедуры с результатом выполнения:
1 | var sql = "{?=call procedure_name(?)}"; |
Выполнение хранимой процедуры с параметрами и результатом выполнения:
1 | var sql = "{call procedure_name(?)}"; |
Если процедура типа OUT
, то она может выполнять registerOutParameter
; может включать (может и НЕ включать) символы “?=”.
Если процедура INOUT
, то она может выполнять registerOutParameter
; может включать (может и НЕ включать) символы “?=”; может принимать параметры на вход.
ResultSet
Перед выполнением запроса можно передать параметры модифицирующие ResultSet
и то как он будет считываться:
1 | connection.prepareStatement(sql, ResultSet.${param}, ResultSet.${param}); |
Ниже представлены параметры, доступные в ResultSet
:
TYPE_FORWARD_ONLY
-ResultSet
может двигаться в чтении только вперед, вызовResultSet.previous()
будет выдавать ошибку;TYPE_SCROLL_INSENSITIVE
- во время прохода по результату запроса, изменения, сделанные в таблицах в момент считывания, будут проигнорированы;TYPE_SCROLL_SENSITIVE
- обратное кTYPE_SCROLL_INSENSITIVE
;CONCUR_READ_ONLY
- изменения вResultSet
не будут применимы;CONCUR_UPDATABLE
- изменения вResultSet
допустимы.
Чтение в обратную сторону
ResultSet
позволяет выполнять чтение в обратном направлении, но для этого должны быть выполнены некоторые условия:
- драйвер должен позволять это делать;
- необходимо получить достаточный объем данных используя
setFetchSize
; - сдвинуть курсор чтения в конец методом
afterLast
; - выполнять previous для сдвига курсора в обратном направлении.
Для TYPE_SCROLL_INSENSITIVE
, де-факто, выполняется кэширование всех полученных результатов (для этого и выполняется setFetchSize
) и ResultSet
оперирует этими данными, а не обращается к БД для прочтения каждой следующей строки. Не помогает, в том числе, и работа в изолированной транзакции, т.к. чтение фактически выполняется в небольшой промежуток времени для загрузки в кэш.
Для TYPE_SCROLL_SENSITIVE
так же поддерживается возможность чтения в 2 направлениях, но чтение каждой новой строки (в любом направлении) производится на уровне БД. Это накладывает дополнительную нагрузку на БД!
Изменения через ResultSet
Некоторые драйвера подключения к БД поддерживают изменение полученных данных напрямую в ResultSet
с последующим подтверждением изменений в БД. За это отвечает ResultSet.CONCUR_UPDATABLE
. Если драйвер не поддерживает подобную функцию, будет выброшена ошибка, которую можно избежать предварительно проверив наличие такой функции:
1 | boolean supported = DatabaseMetaData.supportsResultSetConcurrency(ResultSet.CONCUR_UPDATABLE); |
Пример чтения и изменения:
1 | boolean supportsBackward = connection.getMetaData() |
Закрытие соединения
Большое количество классов, работающих с БД реализуют AutoCloseable
, а значит их можно вставлять в try-with-resources блоки. Если в рамках 1-го открытого Statement
выполняется насколько ResultSet
, то все кроме последнего будут закрыты автоматически - т.е. при открытии нового ResultSet
, уже открытый будет принудительно закрыт.