Java JDBC: Работа с базой данных

Теория: Общие принципы работы

В этом курсе мы погрузимся в различные элементы JDBC, но сначала рассмотрим общие принципы, которые работают одинаково для любых запросов. Эти принципы включают такие пункты:

  • Установка зависимостей
  • Подключение к базе данных
  • Подготовка запроса
  • Выполнение запроса
  • Формирование результата

В этом курсе мы будем работать с базой данных H2. Она полноценно работает с SQL, но есть одно отличие — ее можно создавать только в памяти, что удобно для обучения и тестирования. При необходимости вы с легкостью можете заменить ее на любую полнофункциональную базу.

Подключение базы H2 выполняется одной строчкой:

implementation("com.h2database:h2:2.2.220")

Далее в уроке мы рассмотрим основной принцип работы с JDBC на примере работы с таблицей пользователя. Для начала создадим таблицу в коде, заполним ее и выведем ее данные в консоль:

package io.hexlet;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Application {
    // Нужно указывать базовое исключение,
    // потому что выполнение запросов может привести к исключениям
    public static void main(String[] args) throws SQLException {
        // Создаем соединение с базой в памяти
        // База создается прямо во время выполнения этой строчки
        // Здесь mem означает, что подключение происходит к базе данных в памяти,
        // а hexlet_test — это имя базы данных
        var conn = DriverManager.getConnection("jdbc:h2:mem:hexlet_test");

        var sql = "CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(255), phone VARCHAR(255))";
        // Чтобы выполнить запрос, создадим объект statement
        var statement = conn.createStatement();
        statement.execute(sql);
        statement.close(); // В конце закрываем

        var sql2 = "INSERT INTO users (username, phone) VALUES ('tommy', '123456789')";
        var statement2 = conn.createStatement();
        statement2.executeUpdate(sql2);
        statement2.close();

        var sql3 = "SELECT * FROM users";
        var statement3 = conn.createStatement();
        // Здесь вы видите указатель на набор данных в памяти СУБД
        var resultSet = statement3.executeQuery(sql3);
        // Набор данных — это итератор
        // Мы перемещаемся по нему с помощью next() и каждый раз получаем новые значения
        while (resultSet.next()) {
            System.out.println(resultSet.getString("username"));
            System.out.println(resultSet.getString("phone"));
        }
        statement3.close();

        // Закрываем соединение
        conn.close();
    }
}

Исключения

Методы для работы с базой данных выбрасывают исключения, которые входят в иерархию классов с базовым исключением SQLException. Поэтому мы должны обращать особое внимание на методы, работающие с запросами. Нужно помечать их как методы, выбрасывающие этот вид исключений:

// Локальные редакторы могут либо автоматически помечать такие методы,
// либо подсказывать методы, которые нужно пометить
public static void main(String[] args) throws SQLException {

Соединение с базой данных

Обычно программисты делают так: рядом со своим приложением они поднимают СУБД, внутри которой они заранее создали необходимую базу данных. Таким образом, приложение соединяется с СУБД и подключается к конкретной базе данных внутри. Для этого нужны параметры подключения:

  • IP-адрес или DNS-адрес
  • Порт для подключения
  • Логин и пароль
  • Имя базы данных

В нашем примере все проще. База H2 запускается прямо в памяти нашего приложения, поэтому ей не нужны доступы. Эту базу не нужно создавать заранее, она создается в момент выполнения соединения:

var connection = DriverManager.getConnection("jdbc:h2:mem:hexlet_test");

Дальше мы можем работать с базой H2 с помощью SQL.

Здесь все как с обычной реляционной базой данных. Но важно помнить, что эта база существует, только когда приложение запущено. Если мы остановим или перезапустим приложение, это приведет к потере данных. Это нормально для учебных и тестовых задач, но не подходит для реальных приложений, поэтому в них база H2 не используется.

Стейтмент

В коде выше перед выполнением запроса мы создали стейтмент, а затем закрыли его. Далее мы обсудим, какую роль стейтмент играет в этом процессе, но сначала рассмотрим такой код:

var statement = connection.createStatement();
statement.execute(sql);
statement.close();

Выполнение запроса в базу данных — это более сложная операция, чем кажется на первый взгляд.

Для примера представим, что мы делаем запрос на выборку данных. В этом случае мы вручную пересылаем выборку из базы в приложение, потому что база передает данные только по запросу. Почему это не происходит автоматически? Дело в том, что выборка может быть огромной, тогда автоматическая пересылка привела бы к резкому скачку использования оперативной памяти.

У этой особенности есть неочевидная обратная сторона — дополнительная память начинает активно использоваться внутри самой базы данных.

Чтобы этого не происходило, мы должны четко обозначить отрезок времени, в который СУБД должна хранить запрошенные данные. Именно по этой причине нам приходится закрывать стейтменты. Когда мы это делаем, JDBC посылает сигнал базе данных, обозначая, что данные больше не нужны. В ответ на этот сигнал, база данных освобождает ресурсы.

Может показаться, что мы переложили проблему с клиента на сервер, но это не совсем так. Базы данных стараются максимально оптимизировать работу с данными, поэтому они затрачивают меньше ресурсов на хранение выборок ниже. К тому же, передача данных по сети — это долго и дорого.

А что будет, если не закрыть стейтмент? Стейтменты удерживают ресурсы системы — если забывать их закрывать, то в итоге это приведет к сбоям в работе приложения.

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

  • Запросы на выборку данных выполняются через метод stmt.executeQuery()
  • Запросы на вставку и обновление данных работают через метод stmt.executeUpdate()
  • Все остальные запросы через метод stmt.execute() — в нашем примере это создание таблицы

Объекты класса ResultSet

Последний элемент нашего примера — это ResultSet:

var sql3 = "SELECT * FROM users";
var statement3 = connection.createStatement();
var resultSet = statement3.executeQuery(sql3);
while (resultSet.next()) {
    System.out.println(resultSet.getLong("id"));
    System.out.println(resultSet.getString("username"));
    System.out.println(resultSet.getString("phone"));
}
statement3.close();

Объекты этого класса выполняют роль курсора — указателя на набор данных, хранящийся в памяти в базе. Другими словами, это не набор извлеченных данных из базы, это всего лишь указатель на них. Кроме того, курсор может последовательно перебирать данные через метод next(). Вызов этого метода приводит к тому, что содержимое объекта подменяется новой порцией данных от СУБД.

Извлечение данных из курсора требует преобразования типов, потому что типы данных в базе далеко не всегда совпадают с типами в Java. Поэтому при получении данных мы должны знать, в какой тип мы хотим преобразовать их.

Также отметим, что ResultSet тоже имеет метод close(), но он используется редко. Обычно ResultSet закрывается автоматически при закрытии стейтмента.

Рекомендуемые программы

Завершено

0 / 5