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

Для взаимодействия с СУБД через 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
Output
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 может использовать все три.

Параметры Описание

IN

Параметр, значение которого присваивается по умолчанию при создании оператора SQL. C помощью метода setXXX() можно установить входные значения с параметром IN.

OUT

Параметр, значение которого предоставляется оператором SQL, возвращаемого им. Извлечение значения из параметров OUT выполняется с помощью методов getXXX().

INOUT

Параметр, который предоставляет как входные, так и выходные значения. Можно связать переменные с помощью методов setXXX() и извлекаете значения с помощью методов getXXX().

Передача значений входных параметров объекта 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() можно вернуться до предыдущего стабильного состояния.