1. JDBC
Для хранения данных мы можем использовать различные базы данных:
-
Postgres
-
MariaDB
-
Oracle
-
MS SQL Server
Все эти системы управления базами данных (СУБД) имеют свои особенности. Главное, что их объединяет это взаимодействие с хранилищем данных посредством команд SQL. Чтобы определить единый механизм взаимодействия с этими СУБД в Java, еще начиная с 1996, был введен специальный прикладной интерфейс API, который называется JDBC.
То есть если нужно в приложении на языке Java взаимодействовать с базой данных, то необходимо использовать функциональные возможности JDBC. Данный API входит в состав Java (последний релиз JDBC: 21 сентября 2017, версия 4.3), в частности, для работы с JDBC в программе Java достаточно подключить пакет java.sql
. Для работы в Jakarta EE есть аналогичный пакет javax.sql
, который расширяет возможности JDBC.
Однако не все СУБД могут поддерживаться через JDBC. Для работы с определенной СУБД также необходим специальный драйвер. Каждый разработчик определенной СУБД обычно предоставляет свой драйвер для работы с JDBC. То есть если необходимо работать с MariaDB, то потребуется специальный драйвер для работы именно с MariaDB. Как правило, большинство драйверов доступны в свободном доступе на сайтах соответствующих СУБД. Обычно они представляют JAR-файлы. И преимущество JDBC как раз и состоит в том, что происходит абстрагирование от строения конкретной СУБД, а используем унифицированный интерфейс, который един для всех.
Для взаимодействия с СУБД через JDBC используются запросы SQL. В то же время возможности SQL для работы с каждой конкретной СУБД могут отличаться. Например, в PostgreSQL это PL/PgSQL, в Oracle - это PL/SQL. Но в целом эти разновидности языка SQL не сильно отличаются.
На процесс компиляции необходимость работы с СУБД никак не сказывается, но влияет на процесс запуска программы. При запуске программы по средствам CLI необходимо указать путь к JAR-файлу драйвера после параметра -classpath
.
java -classpath путь_к_файлу_драйвера:путь_к_классу_программы главный_класс_программы
Например, в директории C:\Java
располагаются файл программы - Program.java
, скомпилированный класс Program
и файл драйвер, допустим, MariaDB - mariadb-java-client-2.7.4.jar
. Для выполнения класса Program
мы можем использовать следующую команду:
java -classpath c:\Java\mariadb-java-client-2.7.4.jar;c:\Java Program
Если C:\Java
является текущей директорией, то команду можно сократить:
java -classpath mariadb-java-client-2.7.4.jar;. Program
В принципе можно и не использовать параметр -classpath
, и запустить программу на выполнение обычным способом с помощью команды java Program
. Но в этом случае путь к драйверу должен быть добавлен в переменную Path
.
2. Driver
Проверка на то, что можно осуществлять взаимодействие с СУБД через драйвер (на примере MariaDB). Для этого определим следующий код программы:
public class Program {
public static void main(String[] args) {
try {
Driver driver = Class.forName("org.mariadb.jdbc.Driver").getDeclaredConstructor().newInstance();
System.out.println("Driver ready!");
} catch (Exception ex) {
System.out.println("Driver is not ready!");
System.out.println(ex);
}
}
}
Для загрузки драйвера здесь применяется строка:
Class.forName("org.mariadb.jdbc.Driver").getDeclaredConstructor().newInstance();
Метод Class.forName()
в качестве параметра принимает строку, которая представляет полный путь к классу драйвера с учетом всех пакетов. В случае MariaDB это путь org.mariadb.jdbc.Driver
. Таким образом, Метод Class.forName()
загружает класс драйвера, который будет использоваться.
Далее вызывается метод getDeclaredConstructor()
, который возвращает конструктор данного класса. И в конце вызывается метод newInstance()
, который создает с помощью конструктора объект данного класса. И после этого можно взаимодействовать с сервером MariaDB.
Скомпилируем и запустим программу на выполнение:
javac Program.java
java -classpath c:\Java\mariadb-java-client-2.7.4.jar;c:\Java Program
Driver ready!
В данном случае класс программы и драйвер размещены в директории C:\Java
. Поэтому при выполнении программы после параметра -classpath
указывается полный путь к файлу драйвера - mariadb-java-client-2.7.4.jar
. Далее после точки с запятой указывается директория, где находятся файлы программы, то есть опять же это директория C:\Java
. И после этого идет название выполняемого класса программы - Program
.
И если все сделано правильно, то при выполнении программы можно увидеть строку Driver ready!
, тем самым убедившись, что драйвер загружен и можно устанавливать connection к СУБД.
Но способ, описанный выше, использовался для драйверов, которые реализуют JDBC ниже версии 4.0
. Начиная с JDBC 4.0
драйвера поддерживают автоматическую загрузку и регистрацию, поэтому для подключения к СУБД не требуется самостоятельно создавать объект типа Driver
, достаточно использовать класс DriverManager
для получения Connection
.
3. Подключение к БД
Вначале создадим на сервере MySQL пустую базу данных с названием store
и с которой будет происходить работа в приложении на Java. Для создания базы данных применяется выражение SQL:
CREATE SCHEMA store;
Его можно выполнить либо из консольного клиента MySQL Command Line Client, либо из графического клиента MySQL Workbench, которые устанавливаются вместе с RDBMS MySQL. Так же можно использовать другое программное обеспечение для работы с СУБД.
Для подключения к базе данных необходимо создать объект java.sql.Connection
. Для его создания применяется статический метод DriverManager
:
DriverManager.getConnection(url, username, password)
Метод DriverManager.getConnection()
в качестве параметров принимает адрес источника данных, логин и пароль. В качестве логина и пароля передаются логин и пароль от СУБД. Адрес локальной базы данных MySQL указывается в следующем формате: jdbc:mysql://localhost/your_database
Пример создания подключения к созданной выше локальной базе данных store
:
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/store", "root", "password");
После завершения работы с подключением его следует закрыть с помощью метода close()
:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/store", "root", "password");
// работа с БД
connection.close();
Либо мы можем использовать конструкцию try-with-resources
:
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/store", "root", "password")) {
// работа с базой данных
}
3.1. Возможные проблемы с часовыми поясами и SSL
При подключении к базе данных MySQL мы можем столкнуться с рядом проблем. Например, определим следующий код подключения:
import java.sql.Connection;
import java.sql.DriverManager;
public class Program {
public static void main(String[] args) {
try {
String url = "jdbc:mysql://localhost/store";
String username = "root";
String password = "password";
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
try (Connection conn = DriverManager.getConnection(url, username, password)) {
System.out.println("Connection to Store DB successful!");
}
} catch (Exception ex) {
System.out.println("Connection failed...");
System.out.println(ex);
}
}
}
Даже если указаны правильно адрес базы данных, логин, пароль, мы все равно можем столкнуться с ошибками time zone и SSL. Чтобы решить данную проблему, необходимо указать в адресе подключения часовой пояс БД и параметры для использования SSL. Например, можно указать, что SSL не будет использоваться и что часовым поясом будет минский часовой пояс:
String url = "jdbc:mysql://localhost/store?serverTimezone=Europe/Minsk&useSSL=false";
Параметры подключения указываются после вопросительного знака после названия базы данных. Параметр serverTimezone
указывает на название часового пояса сервера БД. В данном случае Europe/Minsk
. Параметр useSSL=false
указывает, что SSL не будет применяться.
3.2. Файлы конфигурации
Мы можем определить все данные для подключения непосредственно в программе. Однако что если какие-то данные были изменены? В этом случае потребуется заново собрать приложения. Иногда это не всегда удобно, например, отсутствует доступ к исходникам, или сборка займет довольно продолжительное время. В этом случае мы можем хранить настройки в файле.
Так, создадим в папке программы новый текстовый файл database.properties
, в котором определим настройки подключения:
url=jdbc:mysql://localhost/store?serverTimezone=Europe/Minsk&useSSL=false
username=root
password=password
Загрузим эти настройки в программе:
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class Program {
public static void main(String[] args) {
try {
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
try (Connection conn = getConnection()) {
System.out.println("Connection to Store DB successful!");
}
} catch (Exception ex) {
System.out.println("Connection failed...");
System.out.println(ex);
}
}
public static Connection getConnection() throws SQLException, IOException {
Properties props = new Properties();
try (InputStream in = Files.newInputStream(Paths.get("database.properties"))) {
props.load(in);
}
String url = props.getProperty("url");
String username = props.getProperty("username");
String password = props.getProperty("password");
return DriverManager.getConnection(url, username, password);
}
}
4. Выполнение команд
Для взаимодействия с базой данных приложение отправляет серверу MySQL команды на языке SQL. Чтобы выполнить команду, вначале необходимо создать объект Statement
.
Для его создания у объекта Connection
вызывается метод createStatement()
:
Statement statement = conn.createStatement();
Для выполнения команд SQL в классе Statement
определены методы:
-
int executeUpdate(String sql)
выполняет такие команды, какINSERT
,UPDATE
,DELETE
,CREATE TABLE
,DROP TABLE
. В качестве результата возвращает количество строк, затронутых операцией (например, количество добавленных, измененных или удаленных строк), или0
, если ни одна строка не затронута операцией или если команда не изменяет содержимое таблицы (например, команда создания новой таблицы) -
ResultSet executeQuery(String sql)
выполняет командуSELECT
. Возвращает объектResultSet
, который содержит результаты запроса. -
boolean execute(String sql)
выполняет любые команды и возвращаетtrue
- если команда возвращает набор строк (SELECT
), иначе возвращаетсяfalse
-
int[] executeBatch()
Рассмотрим метод executeUpdate()
. В качестве параметра в него передается собственно команда SQL:
int executeUpdate("Команда_SQL")
Ранее была создана база данных store
, но она пустая, в ней нет таблиц и соответственно данных. Создадим таблицу и добавим в нее начальные данные:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Program {
public static void main(String[] args) {
try {
String url = "jdbc:mysql://localhost/store?serverTimezone=Europe/Moscow&useSSL=false";
String username = "root";
String password = "password";
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
// команда создания таблицы
String sqlCommand = "CREATE TABLE products (Id INT PRIMARY KEY AUTO_INCREMENT, ProductName VARCHAR(20), Price INT)";
try (Connection conn = DriverManager.getConnection(url, username, password)) {
Statement statement = conn.createStatement();
// создание таблицы
statement.executeUpdate(sqlCommand);
System.out.println("Database has been created!");
}
} catch (Exception ex) {
System.out.println("Connection failed...");
System.out.println(ex);
}
}
}
То есть в данном случае мы выполняем команду CREATE TABLE products (Id INT PRIMARY KEY AUTO_INCREMENT, ProductName VARCHAR(20), Price INT)
, которая создает таблицу Products
с тремя столбцами: Id
- идентификатор стоки, ProductName
- строковое название товара и Price
- числовая цена товара.
При этом если необходимо выполнить сразу несколько команд, то необязательно создавать новый объект Statement
:
Statement statement = conn.createStatement();
statement.executeUpdate("Команда_SQL1");
statement.executeUpdate("Команда_SQL2");
statement.executeUpdate("Команда_SQL3");
5. CRUD команды
Для добавления, редактирования и удаления данных мы можем использовать рассмотренный в прошлой теме метод executeUpdate()
. С помощью результата метода мы можем проконтролировать, сколько строк было добавлено, изменено или удалено.
5.1. Добавление
Так, возьмем созданную в прошлой теме таблицу Products
:
CREATE TABLE Products (
Id INT PRIMARY KEY AUTO_INCREMENT,
ProductName VARCHAR(20),
Price INT
)
И добавим в эту таблицу несколько объектов:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Program {
public static void main(String[] args) {
try {
String url = "jdbc:mysql://localhost/store?serverTimezone=Europe/Minsk&useSSL=false";
String username = "root";
String password = "password";
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
try (Connection conn = DriverManager.getConnection(url, username, password)) {
Statement statement = conn.createStatement();
int rows = statement.executeUpdate("INSERT Products(ProductName, Price) VALUES ('iPhone X', 76000)," +
"('Galaxy S9', 45000), ('Nokia 9', 36000)");
System.out.printf("Added %d rows", rows);
}
} catch (Exception ex) {
System.out.println("Connection failed...");
System.out.println(ex);
}
}
}
Для добавления данных в БД применяется команда INSERT
. В данном случае в таблицу Products
добавляется три объекта. И после выполнения программы на консоли мы увидим число добавленных объектов:
C:\Java>javac Program.java
C:\Java>java -classpath c:\Java\mysql-connector-java-8.0.11.jar;c:\Java Program
Added 3 rows
C:\Java>
5.2. Редактирование
Изменим строки в таблице, например, уменьшим цену товара на 5000 единиц. Для изменения применяется команда UPDATE
:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Program {
public static void main(String[] args) {
try {
String url = "jdbc:mysql://localhost/store?serverTimezone=Europe/Minsk&useSSL=false";
String username = "root";
String password = "password";
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
try (Connection conn = DriverManager.getConnection(url, username, password)) {
Statement statement = conn.createStatement();
int rows = statement.executeUpdate("UPDATE Products SET Price = Price - 5000");
System.out.printf("Updated %d rows", rows);
}
} catch (Exception ex) {
System.out.println("Connection failed...");
System.out.println(ex);
}
}
}
5.3. Удаление
Удалим один объект из таблицы с помощью команды DELETE
:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Program {
public static void main(String[] args) {
try {
String url = "jdbc:mysql://localhost/store?serverTimezone=Europe/Minsk&useSSL=false";
String username = "root";
String password = "password";
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
try (Connection conn = DriverManager.getConnection(url, username, password)) {
Statement statement = conn.createStatement();
int rows = statement.executeUpdate("DELETE FROM Products WHERE Id = 3");
System.out.printf("%d row(s) deleted", rows);
}
} catch (Exception ex) {
System.out.println("Connection failed...");
System.out.println(ex);
}
}
}
Как видно из примеров, не так сложно взаимодействовать с базой данных. Достаточно передать в метод executeUpdate()
нужную команду SQL.
6. Получение данных из БД
6.1. Метод executeQuery.
Для выборки данных с помощью команды SELECT
применяется метод executeQuery
:
ResultSet executeQuery("Команда_SQL")
Метод возвращает объект ResultSet
, который содержит все полученные данные. Как эти данные получить?
В объекте ResultSet
итератор устанавливается на позиции перед первой строкой. Чтобы переместиться к первой строке (и ко всем последующим) необходимо вызвать метод next()
. Пока в наборе ResultSet
есть доступные строки, метод next()
будет возвращать true
. Типичное перемещение по набору строк:
ResultSet resultSet = statement.executeQuery("SELECT * FROM Products");
while (resultSet.next()) {
// получение содержимого строк
}
То есть пока в resultSet
есть доступные строки, будет выполняться цикл while
, который будет переходить к следующей строке в наборе.
После перехода к строке мы можем получить ее содержимое. Для этого у ResultSet
определен ряд методов. Некоторые из них:
-
boolean getBoolean()
-
Date getDate()
-
double getDouble()
-
int getInt()
-
float getFloat()
-
long getLong()
-
String getNString()
-
String getString()
В зависимости от того, данные какого тип хранятся в том или ином столбце, мы можем использовать тот или иной метод. Каждый из этих методов имеет две версии (на примере int
):
-
int getInt(int columnIndex)
-
int getInt(String columnLabel)
Первая версия получает данные из столбца с номером columnIndex
. Вторая версия получает данные из столбца с названием columnLabel
.
Например, была создана таблица, которая имеет три столбца:
CREATE TABLE products (
Id INT PRIMARY KEY AUTO_INCREMENT,
ProductName VARCHAR(20),
Price INT
)
Получим из нее данные:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class Program {
public static void main(String[] args) {
try {
String url = "jdbc:mysql://localhost/store?serverTimezone=Europe/Moscow&useSSL=false";
String username = "root";
String password = "password";
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
try (Connection conn = DriverManager.getConnection(url, username, password)) {
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM Products");
while (resultSet.next()) {
int id = resultSet.getInt(1);
String name = resultSet.getString(2);
int price = resultSet.getInt(3);
System.out.printf("%d. %s - %d \n", id, name, price);
}
}
} catch (Exception ex) {
System.out.println("Connection failed...");
System.out.println(ex);
}
}
}
Первый столбец в таблице — столбец Id
представляет тип int
, поэтому для его получения используется метод getInt()
. Второй столбец - ProductName
представляет строку, поэтому для получения его данных применяется метод getString()
. То есть между типом данных и методом есть соответствие. И мы не можем, к примеру, получить значение столбца ProductName
с помощью метода getInt()
.
Нужно отметить, что индексация столбцов начинается с 1
, а не с 0
.
Возможный консольный вывод программы:
C:\Java>javac Program.java
C:\Java>java -classpath c:\Java\mysql-connector-java-8.0.11.jar;c:\Java Program
1. iPhone X - 71000
2. Galaxy S9 - 40000
C:\Java>
Однако мы можем точно не знать порядок следования данных в полученном наборе. В этом случае мы можем вместо номеров столбцов можно передать названия столбцов:
ResultSet resultSet = statement.executeQuery("SELECT * FROM Products");
while (resultSet.next()) {
int id = resultSet.getInt("Id");
String name = resultSet.getString("ProductName");
int price = resultSet.getInt("Price");
System.out.printf("%d. %s - %d \n", id, name, price);
}
7. PreparedStatement
Кроме класса Statement
в java.sql
мы можем использовать для выполнения запросов еще один класс - PreparedStatement
. Кроме собственно выполнения запроса этот класс позволяет подготовить запрос, отформатировать его должным образом.
Например, в прошлых темах была создана таблица, которая имеет три столбца:
CREATE TABLE products (
Id INT PRIMARY KEY AUTO_INCREMENT,
ProductName VARCHAR(20),
Price INT
)
С помощью PreparedStatement
добавим в нее один объект:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.Scanner;
public class Program {
public static void main(String[] args) {
try {
String url = "jdbc:mysql://localhost/store?serverTimezone=Europe/Moscow&useSSL=false";
String username = "root";
String password = "password";
Scanner scanner = new Scanner(System.in);
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
System.out.print("Input product name: ");
String name = scanner.nextLine();
System.out.print("Input product price: ");
int price = scanner.nextInt();
try (Connection conn = DriverManager.getConnection(url, username, password)) {
String sql = "INSERT INTO Products (ProductName, Price) Values (?, ?)";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
preparedStatement.setString(1, name);
preparedStatement.setInt(2, price);
int rows = preparedStatement.executeUpdate();
System.out.printf("%d rows added", rows);
}
} catch (Exception ex) {
System.out.println("Connection failed...");
System.out.println(ex);
}
}
}
В данном случае данные вводятся с консоли и затем добавляются в базу данных. Для создания объекта PreparedStatement
применяется метод prepareStatement()
класса Connection
. В этот метод передается выражение sql INSERT INTO Products (ProductName, Price) Values (?, ?)
. Это выражение может содержать знаки вопроса ?
- знаки подстановки, вместо которых будут вставляться реальные значения.
Чтобы связать отдельные знаки подстановки с конкретными значениями у класса PreparedStatement
определен ряд методов для различных типов данных. Все методы, которые поставляют значения вместо знаков подстановки, в качестве первого параметра принимают порядковый номер знака подстановки (нумерация начинается с 1), а в качестве второго параметра — собственно значение, вставляемое вместо знака подстановки.
Например, первый знак подстановки ?
, в выражении sql, представляет значение для столбца ProductName
, который хранит строку. Поэтому для связи значения с первым знаком подстановки применяется метод preparedStatement.setString(1, name)
.
Второй знак подстановки должен передавать значение для столбца Price
, который хранит целые числа. Поэтому для вставки значения используется метод preparedStatement.setInt(2, price)
.
Кроме setString()
и setInt()
PreparedStatement
имеет еще ряд подобных методов, которые работают подобным образом. Некоторые из них:
-
setBigDecimal()
-
setBoolean()
-
setDate()
-
setDouble()
-
setFloat()
-
setLong()
-
setNull()
-
setTime()
Для выполнения запроса PreparedStatement
имеет три метода:
-
boolean execute()
выполняет любую SQL-команду -
ResultSet executeQuery()
выполняет командуSELECT
, которая возвращает данные какResultSet
-
int executeUpdate()
выполняет такие SQL-команды, какINSERT
,UPDATE
,DELETE
,CREATE
и возвращает количество измененных строк
При этом в отличие от методов Statement
эти методы не принимают SQL-выражение.
Пример выполнения программы:
C:\Java>javac Program.java
C:\Java>java -classpath c:\Java\mysql-connector-java-8.0.11.jar;c:\Java Program
Inpit product name: Xiaomi Mi 8
Input product price: 35000
1 rows added
C:\Java>
Подобным образом мы можем выполнять и другие выражения. Например, получим товары, у которых цена меньше 50000
:
public class Program {
public static void main(String[] args) {
// code
int maxPrice = 50000;
PreparedStatement preparedStatement =
conn.prepareStatement("SELECT * FROM Products WHERE Price < ?");
preparedStatement.setInt(1, maxPrice);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
int id = resultSet.getInt("Id");
String name = resultSet.getString("ProductName");
int price = resultSet.getInt("Price");
System.out.printf("%d. %s - %d \n", id, name, price);
}
}
}
8. CallableStatement
Подобно тому, как объект Connection
создает объекты Statement
и PreparedStatement
, он также создает объект CallableStatement
, который будет использоваться для выполнения вызова хранимой процедуры из базы данных. Хранимая процедура — последовательность команд, хранимых в БД.
Синтаксис вызова хранимой процедуры в JDBC показан ниже. Квадратные скобки означают, что параметры необязательны и сами по себе не являются частью синтаксиса.
{call имя_процедуры[(?, ?, ...)]}
{? = call имя_процедуры[(?, ?, ...)]}
{call имя_процедуры}
8.1. Создание объекта CallableStatement
Объекты CallableStatement
создаются методом prepareCall()
объекта Connection
. Пример, который создает экземпляр CallableStatement
, содержащий вызов хранимой процедуры с двумя аргументами и без возвращаемого параметра:
CallableStatement cstmt = con.prepareCall("{call procedure_name(?, ?)}");
8.2. Входные и выходные IN
и OUT
-параметры
Для передачи и получения значения из хранимой процедуры есть три типа параметров: IN
, OUT
и INOUT
. Объект CallableStatement
может использовать все три.
Параметры | Описание |
---|---|
|
Параметр, значение которого присваивается по умолчанию при создании оператора SQL. C помощью метода |
|
Параметр, значение которого предоставляется оператором SQL, возвращаемого им. Извлечение значения из параметров |
|
Параметр, который предоставляет как входные, так и выходные значения. Можно связать переменные с помощью методов |
Передача значений входных параметров объекта CallableStatement
осуществляется с помощью методов setXXX()
, унаследованных от PreparedStatement
. Типы передаваемых значений определяются тем, какой из методов setXXX()
используется (setString()
для передачи значений типа String
, setInt()
для передачи значений типа int
и т.п.)
JDBC-типы всех OUT
-параметров хранимых процедур должны быть зарегистрированы перед их вызовом. Регистрация типов данных выходного параметра производится методом registerOutParameter()
. Только в этом случае после вызова хранимой процедуры CallableStatement.executeQuery()
можно получить результаты выполнения с помощью методов getXXX()
. Необходимо использовать подходящий по типу данных метод getXXX()
в соответствии с зарегистрированным JDBC-типом параметра.
Другими словами, registerOutParameter()
использует JDBC-тип, который подходит к JDBC-типу возвращаемого из значения, а getXXX()
преобразует его в тип Java.
Пример регистрации выходных параметров хранимой процедуры и чтение выходных значений.
CallableStatement cstmt = con.prepareCall ("{call getData(?, ?)}");
cstmt.registerOutParameter(1, java.sql.Types.TINYINT);
cstmt.registerOutParameter(2, java.sql.Types.DECIMAL, 2);
// Вызов хранимой процедуры
cstmt.executeQuery();
// Чтение выходных данных
byte x = cstmt.getByte(1);
java.math.BigDecimal n = cstmt.getBigDecimal(2, 2);
В примере метод getByte()
извлекает байт из первого выходного параметра, а getBigDecimal()
возвращает объект BigDecimal
(с двумя цифрами после десятичной точки) из второго выходного параметра.
Если параметр является одновременно и входным, и выходным (INOUT
), то необходимо вызывать как метод setXXX()
, так и метод registerOutParameter()
. Метод setXXX()
устанавливает входное значение параметра, а registerOutParameter()
регистрирует тип выходного значения.
Следующий пример демонстрирует вызов хранимой процедуры с одним INOUT
-параметром. Метод setByte()
устанавливает значение параметра в 25
, которое будет передано хранимой процедуре базе данных как TINYINT
. Далее метод registerOutParameter()
регистрирует первый параметр как TINYINT
. После выполнения хранимой процедуры возвращается значение типа TINYINT
, которое будет считано методом getByte()
в виде типа byte
.
CallableStatement cstmt = con.prepareCall("{call procedureName(?)}");
// Определение значение параметра
cstmt.setByte(1, 25);
// Регистрация выходного параметра
cstmt.registerOutParameter(1, java.sql.Types.TINYINT);
cstmt.executeUpdate();
// Чтение параметра
byte x = cstmt.getByte(1);
Если хранимая процедура оформлена функцией, т.е возвращает значение не через параметры, а через оператор RETURNS
, то для вызова используйте execute()
вместо executeUpdate()
.
8.3. Закрытие объекта CallableStatement
Так же, как нужно закрывать объект Statement
, по той же причине необходимо закрывать объект CallableStatement
.
Простой вызов метода close()
сделает эту работу. Если сначала закрыть объект Connection
, он также закроет объект CallableStatement
. Однако всегда нужно явно закрывать объект CallableStatement
, чтобы обеспечить правильную очистку.
9. Управление транзакциями
Транзакции состоят из одного или более выражений, которые после выполнения либо фиксируются (commit), либо откатываются назад (rollback).
Для работы с транзакциями используют следующие методы:
-
commit()
- делает окончательными все изменения в БД, проделанные SQL-выражением, и снимает все блокировки, установленные транзакцией. -
rollback()
- игнорирует изменения и откатывает изменения до предыдущего состояния.
При вызове этих методов текущая транзакция заканчивается, и начинается другая.
Каждое новое соединение по умолчанию находится в режиме автоматической фиксации, что означает автоматическую фиксацию транзакций после каждого запроса. В этом случае транзакция состоит из одного запроса. Однако, если автоматическая фиксация запрещена, транзакция не заканчивается вплоть до явного вызова commit()
и rollback()
, включая, таким образом, все выражения, выполненные с момента последнего завершения транзакции.
Рассмотрим следующий код:
public class Program {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost/store";
String username = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, username, password)) {
connection.setAutoCommit(false);
// устанавливаем именнованную точку сохранения
Savepoint svpt = connection.setSavepoint("NewEmp");
// any code
Statement st = connection.createStatement();
int rows = st.executeUpdate("INSERT INTO Employees (FirstName, LastName) VALUES ('Игорь', 'Цветков') ");
rows = st.executeUpdate("UPDATE Employees SET Address = 'ул.Седых.19-34' WHERE LastName = 'Цветков'");
// any code
// Запись о работнике вставлена, но адрес не обновлен.
connection.commit(); // завершает транзакцию и подтверждаем все изменения.
} catch (SQLException e) {
connection.rollback(svpt); // возвращаемся к предыдущему стабильному состоянию.
}
}
}
Метод connection.setAutoCommit(false)
позволяет снять автоматическое управление транзакциями. Это означает, что подтверждение (commit) и откат (rollback) теперь управляются в ручном режиме.
C помощью Savepoint svpt = connection.setSavepoint("NewEmp");
устанавливаем точку сохранения. Такая точка позволяет запомнить состояние внутри БД и выполнить rollback до этого момента.
С помощью connection.commit();
подтверждаем завершение транзакции. Однако если во время выполнения транзакции произошел сбой, благодаря методу connection.rollback();
находящемуся в блоке catch
, произойдет откат БД до предыдущего стабильного состояния. Сделать это можно так же указав точку сохранения внутри функции connection.rollback(svpt)
, которых может быть множество.
9.1. BATCH-команды
BATCH-команды позволяют запускать на исполнение в СУБД массив запросов SQL вместе как одну единицу. Например:
st = com.createStatement();
st.addBatch("INSERT INTO ...");
st.addBatch("INSERT INTO ...");
st.addBatch("INSERT INTO ...");
int[] updateCounts = st.executeBatch();
С помощью функции addBatch()
накапливаем SQL
-запросы и отправляем на выполнение. Следует учесть, что при возникновении исключения мы не узнает на каком именно запросе произошла ошибка. Однако, с помощью блока try … catch
и функции rollback()
можно вернуться до предыдущего стабильного состояния.