Введение в JDBC

1. Обзор

В этой статье мы рассмотрим JDBC (Java Database Connectivity), который представляет собой API для подключения и выполнения запросов к базе данных.

JDBC может работать с любой базой данных, если предоставляются надлежащие драйверы.

2. Драйверы JDBC

Драйвер JDBC - это реализация API JDBC, используемая для подключения к базе данных определенного типа. Существует несколько типов драйверов JDBC:

  • Тип 1 - содержит отображение на другой API доступа к данным; пример

это драйвер JDBC-ODBC ** Тип 2 - это реализация, которая использует клиентские библиотеки

целевая база данных; также называется драйвером native-API ** Тип 3 - использует промежуточное ПО для преобразования вызовов JDBC в базу данных

звонки; также известный как драйвер сетевого протокола ** Тип 4 - подключение напрямую к базе данных путем преобразования вызовов JDBC в

специфичные для базы данных звонки; известные как драйверы протокола базы данных или тонкие драйверы,

Наиболее часто используемый тип - это тип 4, так как он имеет преимущество в том, что не зависит от платформы . Подключение напрямую к серверу базы данных обеспечивает лучшую производительность по сравнению с другими типами. Недостатком этого типа драйвера является то, что он зависит от базы данных - учитывая, что каждая база данных имеет свой собственный определенный протокол.

3. Подключение к базе данных

Чтобы подключиться к базе данных, нам просто нужно инициализировать драйвер и открыть соединение с базой данных.

3.1. Регистрация водителя

В нашем примере мы будем использовать драйвер протокола базы данных типа 4.

Поскольку мы используем базу данных MySQL, нам нужен https://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%22mysql-connector-java%22%20AND%20g%3A%22mysql Зависимость% 22[ mysql-connector-java ]:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>6.0.6</version>
</dependency>

Далее, давайте зарегистрируем драйвер, используя метод Class.forName () , который динамически загружает класс драйвера:

Class.forName("com.mysql.cj.jdbc.Driver");

3.2. Создание соединения

Чтобы открыть соединение, мы можем использовать метод getConnection () класса DriverManager . Этот метод требует подключения URL String параметр:

Connection con = DriverManager
  .getConnection("jdbc:mysql://localhost:3306/myDb", "user1", "pass");

Синтаксис URL-адреса соединения зависит от типа используемой базы данных.

Давайте посмотрим на несколько примеров:

jdbc:mysql://localhost:3306/myDb?user=user1&password=pass
jdbc:postgresql://localhost/myDb
jdbc:hsqldb:mem:myDb

Чтобы подключиться к указанной myDb базе данных, нам нужно будет создать базу данных и пользователя и добавить предоставить необходимый доступ:

CREATE DATABASE myDb;
CREATE USER 'user1' IDENTIFIED BY 'pass';
GRANT ALL on myDb.**  TO 'user1';

4. Выполнение операторов SQL

Для отправки инструкций SQL в базу данных мы можем использовать экземпляры типа Statement , PreparedStatement или CallableStatement . Они получены с использованием объекта Connection .

4.1. Утверждение

Интерфейс Statement содержит основные функции для выполнения команд SQL.

Сначала давайте создадим объект Statement :

Statement stmt = con.createStatement();

Выполнение инструкций SQL может быть выполнено с использованием трех методов:

  • executeQuery () для инструкций SELECT

  • executeUpdate () для обновления данных или структуры базы данных

  • execute () может использоваться в обоих вышеупомянутых случаях, когда результат

неизвестный

Давайте используем метод execute () , чтобы добавить таблицу students в нашу базу данных:

String tableSql = "CREATE TABLE IF NOT EXISTS employees"
  + "(emp__id int PRIMARY KEY AUTO__INCREMENT, name varchar(30),"
  + "position varchar(30), salary double)";
stmt.execute(tableSql);
  • Если для обновления данных используется метод execute () , то метод stmt.getUpdateCount () возвращает количество затронутых строк. **

Если результат равен 0, то либо строки не были затронуты, либо это была команда обновления структуры базы данных.

Если значение равно -1, то команда была запросом SELECT. Затем результат можно получить с помощью stmt.getResultSet () .

Далее, давайте добавим запись в нашу таблицу, используя метод executeUpdate () :

String insertSql = "INSERT INTO employees(name, position, salary)"
  + " VALUES('john', 'developer', 2000)";
stmt.executeUpdate(insertSql);

Метод возвращает количество затронутых строк для команды, которая обновляет строки, или 0 для команды, которая обновляет структуру базы данных.

Мы можем извлечь записи из таблицы, используя метод executeQuery () , который возвращает объект типа ResultSet :

String selectSql = "SELECT **  FROM employees";
ResultSet resultSet = stmt.executeQuery(selectSql);

4.2. Подготовленное заявление

Объекты PreparedStatement содержат предварительно скомпилированные последовательности SQL. Они могут иметь один или несколько параметров, обозначенных знаком вопроса.

Давайте создадим PreparedStatement , который обновляет записи в таблице employees на основе заданных параметров:

String updatePositionSql = "UPDATE employees SET position=? WHERE emp__id=?";
PreparedStatement pstmt = con.prepareStatement(updatePositionSql);

Чтобы добавить параметры в PreparedStatement , мы можем использовать простые установщики - setX () - где X - тип параметра, а аргументы метода - порядок и значение параметра:

pstmt.setString(1, "lead developer");
pstmt.setInt(2, 1);

Оператор выполняется одним из тех же трех методов, которые были описаны ранее: executeQuery (), executeUpdate (), execute () без параметра SQL String :

int rowsAffected = pstmt.executeUpdate();

4.3. CallableStatement

Интерфейс CallableStatement позволяет вызывать хранимые процедуры.

Для создания объекта CallableStatement мы можем использовать метод prepareCall () для Connection :

String preparedSql = "{call insertEmployee(?,?,?,?)}";
CallableStatement cstmt = con.prepareCall(preparedSql);

Установка значений входных параметров для хранимой процедуры выполняется аналогично интерфейсу PreparedStatement с использованием методов setX () :

cstmt.setString(2, "ana");
cstmt.setString(3, "tester");
cstmt.setDouble(4, 2000);

Если хранимая процедура имеет выходные параметры, нам нужно добавить их с помощью метода registerOutParameter () :

cstmt.registerOutParameter(1, Types.INTEGER);

Затем давайте выполним инструкцию и получим возвращенное значение, используя соответствующий метод getX () :

cstmt.execute();
int new__id = cstmt.getInt(1);

Например, для работы нам нужно создать хранимую процедуру в нашей базе данных MySql:

delimiter//CREATE PROCEDURE insertEmployee(OUT emp__id int,
  IN emp__name varchar(30), IN position varchar(30), IN salary double)
BEGIN
INSERT INTO employees(name, position,salary) VALUES (emp__name,position,salary);
SET emp__id = LAST__INSERT__ID();
END//delimiter ;

Вышеуказанная процедура insertEmployee вставит новую запись в таблицу employees с использованием заданных параметров и вернет идентификатор новой записи в параметре emp id__ out.

Чтобы иметь возможность запускать хранимую процедуру из Java, пользователь соединения должен иметь доступ к метаданным хранимой процедуры. Это может быть достигнуто путем предоставления пользователю прав на все хранимые процедуры во всех базах данных:

GRANT ALL ON mysql.proc TO 'user1';

Кроме того, мы можем открыть соединение со свойством noAccessToProcedureBodies , установленным в true :

con = DriverManager.getConnection(
  "jdbc:mysql://localhost:3306/myDb?noAccessToProcedureBodies=true",
  "user1", "pass");

Это сообщит JDBC API, что у пользователя нет прав на чтение метаданных процедуры, поэтому он создаст все параметры как параметры INOUT String .

5. Анализ результатов запроса

После выполнения запроса результат представляется объектом ResultSet , структура которого похожа на таблицу со строками и столбцами.

5.1. ResultSet Интерфейс

ResultSet использует метод next () для перехода к следующей строке.

Давайте сначала создадим класс Employee для хранения наших найденных записей:

public class Employee {
    private int id;
    private String name;
    private String position;
    private double salary;

   //standard constructor, getters, setters
}

Далее, давайте пройдемся по ResultSet и создадим объект Employee для каждой записи:

String selectSql = "SELECT **  FROM employees";
ResultSet resultSet = stmt.executeQuery(selectSql);

List<Employee> employees = new ArrayList<>();

while (resultSet.next()) {
    Employee emp = new Employee();
    emp.setId(resultSet.getInt("emp__id"));
    emp.setName(resultSet.getString("name"));
    emp.setPosition(resultSet.getString("position"));
    emp.setSalary(resultSet.getDouble("salary"));
    employees.add(emp);
}

Получить значение для каждой ячейки таблицы можно с помощью методов типа getX ( ), где X представляет тип данных ячейки.

Методы getX () можно использовать с параметром int , представляющим порядок ячейки, или параметром String , представляющим имя столбца. Последний вариант предпочтителен в случае, если мы изменим порядок столбцов в запросе.

5.2. Обновляемый ResultSet

Неявно, объект ResultSet может быть пройден только вперед и не может быть изменен.

Если мы хотим использовать ResultSet для обновления данных и их перемещения в обоих направлениях, нам нужно создать объект Statement с дополнительными параметрами:

stmt = con.createStatement(
  ResultSet.TYPE__SCROLL__INSENSITIVE,
  ResultSet.CONCUR__UPDATABLE
);

Для навигации по этому типу ResultSet мы можем использовать один из методов:

  • first (), last (), beforeFirst (), beforeLast () - перейти к первому

или последняя строка ResultSet или строка перед этими ** next (), previous () - для перемещения вперед и назад в

ResultSet ** getRow () – , чтобы получить текущий номер строки

  • moveToInsertRow (), moveToCurrentRow () - перейти к новой пустой строке

вставить и вернуться к текущему, если в новой строке ** absolute (int row) – , чтобы перейти к указанной строке

  • relative (int nrRows) - для перемещения курсора на указанное количество строк

Обновление ResultSet может быть выполнено с использованием методов в формате updateX () , где X - тип данных ячейки. Эти методы обновляют только объект ResultSet , а не таблицы базы данных.

Чтобы сохранить изменения ResultSet в базе данных, мы должны дополнительно использовать один из методов:

  • updateRow () - сохранить изменения в текущей строке в

база данных ** insertRow (), deleteRow () - добавить новую строку или удалить текущую

один из базы данных ** refreshRow () - обновить ResultSet при любых изменениях

база данных ** cancelRowUpdates () - отменить изменения, внесенные в текущую строку

Давайте рассмотрим пример использования некоторых из этих методов путем обновления данных в таблице employee’s :

Statement updatableStmt = con.createStatement(
  ResultSet.TYPE__SCROLL__INSENSITIVE, ResultSet.CONCUR__UPDATABLE);
ResultSet updatableResultSet = updatableStmt.executeQuery(selectSql);

updatableResultSet.moveToInsertRow();
updatableResultSet.updateString("name", "mark");
updatableResultSet.updateString("position", "analyst");
updatableResultSet.updateDouble("salary", 2000);
updatableResultSet.insertRow();

6. Разбор метаданных

JDBC API позволяет искать информацию о базе данных, которая называется метаданными.

6.1. DatabaseMetadata

Интерфейс DatabaseMetadata может использоваться для получения общей информации о базе данных, такой как таблицы, хранимые процедуры или диалект SQL.

Давайте кратко рассмотрим, как мы можем получить информацию о таблицах базы данных:

DatabaseMetaData dbmd = con.getMetaData();
ResultSet tablesResultSet = dbmd.getTables(null, null, "%", null);
while (tablesResultSet.next()) {
    LOG.info(tablesResultSet.getString("TABLE__NAME"));
}

6.2. ResultSetMetadata

Этот интерфейс может использоваться для поиска информации об определенном ResultSet , такой как номер и имя его столбцов:

ResultSetMetaData rsmd = rs.getMetaData();
int nrColumns = rsmd.getColumnCount();

IntStream.range(1, nrColumns).forEach(i -> {
    try {
        LOG.info(rsmd.getColumnName(i));
    } catch (SQLException e) {
        e.printStackTrace();
    }
});

7. Обработка транзакций

По умолчанию каждый оператор SQL фиксируется сразу после его завершения.

Однако также возможно управлять транзакциями программно .

Это может быть необходимо в тех случаях, когда мы хотим сохранить согласованность данных, например, когда мы хотим зафиксировать транзакцию, только если предыдущая успешно завершена.

Сначала нам нужно установить для свойства autoCommit Connection значение false , затем использовать методы commit () и rollback () для управления транзакцией.

Давайте добавим второй оператор обновления для столбца salary после обновления столбца position сотрудника и обернем их оба в транзакцию.

Таким образом, зарплата будет обновляться только в том случае, если должность была успешно обновлена:

String updatePositionSql = "UPDATE employees SET position=? WHERE emp__id=?";
PreparedStatement pstmt = con.prepareStatement(updatePositionSql);
pstmt.setString(1, "lead developer");
pstmt.setInt(2, 1);

String updateSalarySql = "UPDATE employees SET salary=? WHERE emp__id=?";
PreparedStatement pstmt2 = con.prepareStatement(updateSalarySql);
pstmt.setDouble(1, 3000);
pstmt.setInt(2, 1);

boolean autoCommit = con.getAutoCommit();
try {
    con.setAutoCommit(false);
    pstmt.executeUpdate();
    pstmt2.executeUpdate();
    con.commit();
} catch (SQLException exc) {
    con.rollback();
} finally {
    con.setAutoCommit(autoCommit);
}

8. Закрытие соединения

Когда мы больше не используем, необходимо закрыть соединение, чтобы освободить ресурсы базы данных.

Это можно сделать с помощью API close () :

con.close();

9. Заключение

В этом уроке мы рассмотрели основы работы с JDBC API.

Как всегда, полный исходный код примеров можно найти over на GitHub .