Интеграция СУБД и версионирование БД #
Базы данных предоставляют возможности:
- хранения данных;
- доступа к данным;
- корректного изменения данных из разных приложений одновременно.
Встроенные базы данных #
Для больших систем, обрабатывающих большие объёмы данных, рекомендуется использовать специализированные СУБД. В таком случае они обычно запускаются в качестве отдельной системы, доступ к которой осуществляется по сетевому соединению. В больших компаниях доступ к этим системам зачастую регламентируется и управляется выделенными персоналом.
В рамках лабораторных работ будем использовать СУБД, поставляемую вместе с приложением. Такой подход обладает рядом преимуществ:
- достаточно поставить и настроить одно приложение;
- окружение для разработки совпадает с окружением для поставки приложения.
Однако у него есть и ряд недостатков:
- встроенные базы данных по функциональности проигрывают выделенным решениям;
- легче повредить данным приложения, т.к. они хранятся рядом с исходным кодом приложения;
- приложение использует больше ресурсов, необходимо поддержать работу СУБД.
Класс встроенных баз данных достаточно широк, сущетсвует множество полноценных решений, ориентированных на работу в JVM-окружении:
Ну и есть решение, используемое почти во всех мобильных приложениях SQLite.
База данных H2 #
Рассмотрим из данных решений СУБД H2. Данная субд достаточно популярна, поддерживается большинством инструментов и предоставляет встроенное средство для выполнения SQL-запросов.
Для хранения данных используется отдельный файл. Обычно у него расширение .h2
.
Документация #
Подключение базы к проекту #
В список зависимостей добавьте библиотеку для работы с базой данных. Ввиду того, что это встраиваемая БД, этой зависимости будет достаточно для реализации поддержки СУБД в приложении.
В файл gradle.properties
добавьте версию приложения:
h2dbVersion=2.1.214
В файл build.gradle
добавьте подключение библиотеки в список зависимостей:
dependencies {
implementation group: "com.h2database", name: "h2", version: h2dbVersion
}
Запуск служб СУБД #
H2 поддерживает 2 режима запуска:
- запуск встроенной БД через подключение к файлу;
- запуск отдельного сервера, предоставляющего доступ по сети.
Первый способ требует меньше ресурсов, но запрещает одновременный доступ к данным из разных процессов. Это ограничение для нас существенно, поэтому в рамках лабораторных работ будем использовать второй вариант.
Для запуска сетевого сервера библиотека предоставляет класс org.h2.tools.Server, с помощью которого можно создавать:
- TCP-сервер для подключения к базе данных, createTcpServer.
- Веб-сервер интерактивного приложения для выполнения запросов к базе данных, createWebServer.
Данным методам передаются аргументы, которые определяют работу данных серверов. Список аргументов и их назначение описаны в методе main.
Для запуска интересующих служб можно воспользоваться следующим кодом:
tcpServer = Server.createTcpServer(
"-tcpPort", "9092",
"-baseDir", ".",
"-ifNotExists",
).start()
webServer = Server.createWebServer(
"-webPort", WEB_PORT.toString(),
"-baseDir", ".",
"-ifNotExists",
).start()
Изучите данные аргументы и выясните, что они обозначают.
Интеграция с приложением #
Для упрощённой интеграции данных служб в приложении предоставляется разработанный класс H2DatabaseManager.
Данный класс предоставляет методы для управления сервером:
initialize()
— запускает TCP и Web-серверы, регистрирует хук для остановки данных серверов в момент остановки приложения.stopServers()
— выполняет корректную остановку серверов.
Для его интеграции в приложении предлагается следующая схема:
fun main() {
val h2databaseManager = H2DatabaseManager().initialize()
val webServer = startWebServer()
println("Сервер доступен по адресу http://localhost:" + webServer.port())
println("Веб-интерфейс базы данных доступен по адресу http://localhost:${H2DatabaseManager.WEB_PORT}")
println("Введите любую строку, чтобы завершить работу приложения")
readlnOrNull()
webServer.stop()
h2databaseManager.stopServers()
}
После запуска следующие порты будут задействованы серверами СУБД:
9092
для обслуживания запросов по протоколу H2.8082
для веб-сервера, предоставляющего доступ к функциям выполнения SQL-запросов.
Считывание данных со стандартного потока ввода #
По умолчанию Gradle не обеспечивает доступ к стандартному потоку ввода данных. Поэтому в его конфигурацию необходимо добавить соответствующее разрешение:
run {
standardInput = System.in
}
Задача #
Внесите следующие изменения в ваше приложение по работе с треугольниками
- Добавьте в приложение поддержку работы с СУБД H2.
- Добавьте класс
H2DatabaseManager
в своё приложение. - После старта приложения убедитесь, что веб-интерфейс базы данных доступен.
- Используйте следующую JDBC-строку для подключения к базе данных:
jdbc:h2:tcp://localhost/database.h2
. - С помощью веб-интерфейса и команды
CREATE TABLE
создайте следующие таблицы в базе данных:- Треугольник,
TRIANGLE
. С полями:- сторона
SIDE_A
, SQL-типFLOAT
, - сторона
SIDE_B
, SQL-типFLOAT
, - сторона
SIDE_C
, SQL-типFLOAT
.
- сторона
- Ромб,
RHOMBUS
. С полями:- идентификатор
ID
, SQL-тип:INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
, - длина стороны
SIDE
, SQL-тип:FLOAT
, - угол
ANGLE
, SQL-тип:FLOAT
.
- идентификатор
- Треугольник,
- С помощью веб-интерфейса и SQL-запроса
INSERT
вставьте в каждую из таблиц по 5 значений. - Остановите работу приложения.
- Запустите приложение. Удостоверьтесь, что таблицы и данные приложения сохранились.
- Остановите работу приложения.
- Удалите файл
database.h2.mv.db
. - Запустите приложение.
- Удостоверьтесь, что в базе данных нет информации.
Управление версиями базы данных #
При разработке и поддержке приложения структура базы данных изменяется: добавляются новые сущности (таблицы), добавляются новые параметры, убираются ненужные элементы. При этом код конкретной версии приложения умеет работать только с одной (последней) структурой базы данных.
Подходы к управлению версиями базы данных #
Самая простая система — отказ от автоматического управления версиями базы данных. В этом случае процедура изменения структуры выполняется нетехнологическим способом: скрипты по изменению БД передаются системным администраторам, которые должны выполнить изменение вручную перед разворачиванием системы. Такой подход очень сложный, может вести к проблемам работы приложения из-за человеческого фактора.
Альтернативный подход заключается в автоматическом выполнении данной операции. Автоматизация может быть достигнута как на уровне выполнения произвольного кода внутри приложения, так и с помощью привлечения специализированных инструментов. Последний вариант позволяет сэкономить время при разработке типовых решений. Мы с вами рассмотрим один из таких инструментов.
Возможные варианты данных инструментов:
Процесс выполнения миграций #
Исходные данные:
- Версия базы данных, хранится внутри БД. Если не установлено, то версия считается равной нулю.
- Список миграций баз данных, записанных в отдельных файлах.
При выполнении запроса на миграцию библиотека проверяет текущий номер версии. Затем применяет все миграции, номера которых выше текущего номера базы данных. При выполнении каждой миграции версия схемы внутри базы данных устанавливается равной номеру миграции.
Flyway #
Данный инструмент предоставляет возможности по управлению версиями базы данных:
- выполнение миграций,
- очистка базы данных,
- валидации схемы базы данных,
- вывода информации о схеме базы данных,
- восстановления структуры БД.
Его можно использовать как отдельный инструмент, а можно использовать программный API для выполнения данных операций. Список возможных вариантов использования описан в документации. Мы будем ориентироваться на использование программного интерфейса, т.к. это обеспечит наибольшую совместимость структуры базы данных и исходного кода приложения.
Подключение Flyway #
Описание подключения библиотеки к приложению описан в документации.
В список зависимостей приложения добавим библиотеку flyway-core
. В списке свойств пропишем версию библиотеки:
flywayVersion=9.5.1
Добавим в список зависимостей библиотеку в файле build.gradle
:
implementation group: "org.flywaydb", name: "flyway-core", version: flywayVersion
Запуск миграций при старте приложения #
Для выполнения миграций при старте приложения добавим специализированную функцию. Её можно расположить в файле MigrationsManager.kt
.
fun performMigrations() {
val flyway = Flyway
.configure()
.locations("ac/ru/uniyar/db/migrations")
.validateMigrationNaming(true)
.dataSource(H2DatabaseManager.JDBC_CONNECTION, "sa", null)
.load()
flyway.migrate()
}
Данный код использует строку подключения к базе, которая определена в классе H2DatabaseManager
. В качестве имени пользователя используется sa
, суперпользователь по умолчанию в базах данных H2.
Мы также указываем местоположение для файлов миграций, согласно конфигурации они расположены в ресурсах, по пути ac/ru/uniyar/db/migrations
. Не стоит их смешивать с другими ресурсами вашего приложения.
Далее указываем, что название файлов с миграциями должно быть проверено. Это позволит на самом раннем этапе найти ошибки в наименовании файлов миграции.
Созданную функцию необходимо выполнить после запуска сервера базы данных и до запуска нашего приложения. Поэтому её стоит разместить между запусками данных серверов:
val h2databaseManager = H2DatabaseManager().initialize()
performMigrations()
val webServer = startWebServer()
Создание файлов-миграций #
Файлы с миграциями должны быть названы в соответствии с шаблоном, который распознает библиотека Flyway. В шаблоне указывается следующая информация:
- Тип миграции. Мы будем рассматривать только версионные миграции.
- Номер миграции. Рекомендуется использовать обычные натуральные числа для указания номера миграции.
- Описание. У каждого изменения необходимо добавить описание на обычном языке, чтобы можно было понять её назначение.
Шаблон названия описан в документации:
V015__Add_more_usefull_tables.sql
V
— обязательный префикс для версионных миграций.015
— порядковый номер миграции схемы базы данных.__
— обязательный разделитель между номером и описанием.Add_more_usefull_tables
— текстовое описание, слова в описании разделены подчёркиванием..sql
— обязательный суффикс.
Созданим первую миграцию V001__Add_triangle_table.sql
со следующим содержимым:
CREATE TABLE TRIANGLES (
ID INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
SIDE_A FLOAT NOT NULL,
SIDE_B FLOAT NOT NULL,
SIDE_C FLOAT NOT NULL
);
При запуске приложения можно убедиться, что миграция была успешно выполнена:
окт. 23, 2022 12:49:29 PM org.flywaydb.core.internal.command.DbValidate validate
INFO: Successfully validated 1 migration (execution time 00:00.009s)
окт. 23, 2022 12:49:29 PM org.flywaydb.core.internal.command.DbMigrate migrateGroup
INFO: Current version of schema "PUBLIC": << Empty Schema >>
окт. 23, 2022 12:49:29 PM org.flywaydb.core.internal.command.DbMigrate doMigrateGroup
INFO: Migrating schema "PUBLIC" to version "001 - Create triangles table"
окт. 23, 2022 12:49:30 PM org.flywaydb.core.internal.command.DbMigrate logSummary
Задача #
- Подключите к приложению библиотеку Flyway.
- Добавьте метод по выполнению миграций
performMigrations()
. - Вызовите данный метод в рамках инициализации приложения.
- Создайте файл
broken_migration.sql
в каталоге с миграциямиsrc/main/resources/ac/ru/uniyar/db/migrations
. - Запустите приложение. Посмотрите на вывод приложения. Как повела библиотека при выполнении миграции?
- Сделайте так, чтобы приложение завершало своё работу в случае, если миграции не удалось успешно выполнить.
- Добавьте рабочую миграцию по созданию таблицы для хранения треугольников. Все обязательные столбцы таблицы не должны иметь возможность записи
NULL
-значений. - Запустите приложение и удостоверьтесь, что таблица была создана. Какая ещё таблица была создана? Что вней находится?
- Добавьте миграцию по созданию таблицы для хранения ромбов. Все обязательные столбцы таблицы не должны иметь возможности записи
NULL
-значений. - Перезапустите приложение и удостоверьтесь, что в базе данных находится 2 пустых таблицы.
- Добавьте миграцию по наполнению двух таблиц базовой информацией с 5 фигурами в каждой из таблиц.
- Удостоверьтесь, что миграция была успешно выполнена и данные были добавлены в таблицы.