В этом курсе мы погрузимся в различные элементы 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
закрывается автоматически при закрытии стейтмента.
Самостоятельная работа
- Создайте новый Gradle-проект на своем компьютере
- Выполните все шаги из этого урока
- Создайте на GitHub репозиторий с именем hexlet-jdbc
- Залейте в него код нашего приложения
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.