Skip to content

Міграції схеми бази даних

У практичних завданнях ми вже стикнулися з тим, що таблиці, з яких складається база даних, можуть змінюватись з часом. У цьому топіку ми познайомимось з процесом, який розробники використовують для того, щоб керувати такими змінами. Цей процес називається міграціями бази даних.

Розглянемо приклад. Згадай будь-яку базу даних, яку ми розглянули раніше, наприклад, company. БД розміщується на сервері бази даних, і складається з таблиць, зв’язків між ними, індексів та самих даних. Усе це, окрім даних, — це структура бази даних, тобто її схема. Під час міграцій працюють в основному зі змінами схеми бази даних, саме тому цей процес ще іноді називають schema migration, тобто міграцією схеми бази даних.

💡 Міграцію схеми бази даних не слід плутати з міграцією баз даних (database migration) — процесом перенесення даних з одного серверу баз даних на інший. Назва обох процесів звучить схоже, але вони зовсім різні.

Навіщо застосовувати міграції?

Перш за все, структуру бази даних дуже зручно зберігати у вигляді коду (у Git-репозиторіях), який можна використати для того, щоб цю структуру створити. Якщо в команді, яка розробляє базу даних, більше однієї людини, кожен може переглядати всю структуру, легко відстежувати зміни, а також можна налагодити code review. При цьому кожен член команди може розгорнути власну БД на своєму сервері, щоб було зручніше тестувати її роботу.

💡 Якщо структура бази даних зберігається у вигляді коду та змінюється з часом, цим версіям потрібно присвоювати номер для того, щоб мати можливість їх розрізняти. Завдяки версіюванню, команда точно знатиме, якому коду відповідає кожна база даних та яку вона має структуру.

Оновлення версії схеми

Розглянемо ще один приклад. У нашій команді розробки всі розробники тестують свої зміни на власних серверах баз даних, і є один окремий сервер бази даних, який обслуговує наш вебсайт для кінцевих користувачів — production. Production-сервер уже містить якусь версію структури бази даних. Один з розробників вніс зміну в структуру бази даних та відтестував зміни на своєму сервері бази даних. Після цього, він створив pull request зі своїми змінами, пройшов core review та змерджив їх в головну гілку в Git-репозиторії. Це створило нову версію структури бази даних, і production-сервер бази даних тепер потрібно привести до цієї нової версії. Тобто для процесів розробки нам потрібно мати можливість застосовувати нові версії структури бази даних на базах даних із вже існуючою версією структури та якимись даними.

Rollback

Уявімо, що наш розробник, який створив нову версію структури бази даних, допустив дефект, який проявляється лише на великих кількостях даних. Цей дефект після приведення production бази даних до його нової версії структури значно уповільнив систему. У такому випадку потрібно мати можливість «скасувати» його зміни, тобто привести версію production бази даних до попередньої версії структури, яка працювала стабільно. Таке повернення до попередньої версії називається rollback(відкочування).

Інструменти для міграцій

Отож, щоб налагодити процес розробки структури бази даних, потрібен інструмент, який буде:

  • підтримувати зберігання структури бази даних у вигляді коду, який можна розгорнути на сервері бази даних;
  • дозволятиме створювати версії цього коду;
  • дозволятиме приводити базу даних до нової версії схеми бази даних;
  • дозволятиме відкочуватись до попередніх версій структури бази даних.

Інструменти, які підтримують усі ці функції, і є інструментами міграцій бази даних, а зміни структури, які здійснюються за допомогою цих інструментів, називають міграціями бази даних. Існує безліч інструментів для міграцій бази даних. Вибір конкретного інструменту залежить таких факторів:

  • мова програмування та фреймворк, за допомогою яких розробляється застосунок;
  • складність структури бази даних та планована частота змін в цю структуру;
  • експертиза команди розробки та можливості команди щодо впровадження нових інструментів.

Наприклад, якщо застосунок розробляється на .NET або .NET Core з використанням бібліотеки для роботи з базами даних Entity Framework, то, оскільки сама бібліотека Entity Framework підтримує міграції, вона стає вибором за замовчуванням.

Якщо застосунок розробляється на Node.js чи golang та структура бази даних не складна (до 5 таблиць), то часто інструмент для міграції бази даних не використовують взагалі. Натомість просто пишуть код, який перевіряє, чи потрібні таблиці створені, і якщо ні, то створює їх.

Якщо структура бази даних складна та часто змінюватиметься, а фреймворк, на якому написаний застосунок, не підтримує міграції, то є сенс застосувати окремий інструмент для міграції бази даних, на кшталт Liquibase.

Налагодження процесу міграції тісно пов’язано з DevOps. За допомогою міграцій автоматичне розгортання та тестування нових версій застосунку значно спрощується. Це дозволяє швидше випускати нові версії програм, які використовують бази даних, але при цьому не боятись дефектів. Завдяки міграціям набагато легше доставляти зміни для тестування, та ми можемо бути впевнені, що в production потрапляє саме та версія структури бази даних, яка була протестована. Окрім того, навіть у випадку виявлення дефекту вже в production, завдяки міграціям ми можемо повернутись до попередньої версії структури бази даних, поки працюватимемо над виправленням дефекту.

Запуск Liquibase

Liquibase — це платний інструмент для міграцій схеми БД, але в нього є безплатна версія, якої нам для навчальних цілей буде цілком достатньо.

💡 Запускати Liquibase ми будемо в Docker. Якщо ти не маєш можливості користуватись Docker, то тримай посилання на документацію з встановлення Liquibase для різних операційних систем.

Щоб запустити інтерактивний Docker-контейнер з Liquibase, потрібно виконати команду, яка запустить контейнер з імейджу Liquibase, а також bash всередині контейнера. Так ми зможемо виконувати команди в цьому контейнері:

docker run -it liquibase/liquibase:latest /bin/bash

Оскільки ми працюватимемо з MySQL, то для Liquibase потрібно встановити плагін для роботи з ним. Він не розповсюджується за замовчуванням в імейджі Liquibase через особливості ліцензування MySQL. Для того, щоб встановити цей плагін, нашому Docker-контейнеру потрібно при запуску додати ось таку змінну середовища:

docker run -it -e INSTALL_MYSQL=true liquibase/liquibase /bin/bash

Щоб мати можливість редагувати файли на комп’ютері з контейнера, додамо mount-теки:

docker run -it -v $(pwd):/repos -e INSTALL_MYSQL=true liquibase/liquibase /bin/bash

💡 Частинка з pwd працюватиме лише на Linux або macOS. Якщо ти працюєш на Windows, то для того, щоб скористатись mount-текою, замість цієї конструкції вкажи повний шлях до теки, у якій ти плануєш зберігати файли для Liquibase.

Ще одна важлива річ: якщо ти працюєш на macOS чи Linux, то для теки, яку ти плануєш примаунтити, потрібно налаштувати права, що дозволять всім користувачам читати та записувати файли:

chmod 777 . 
docker run -it -v $(pwd):/repos -e INSTALL_MYSQL=true liquibase/liquibase /bin/bash

Тепер можна працювати з Liquibase, який запущений в Docker-контейнері, і при цьому редагувати файли для нього з текстового редактора, наприклад, VS Code:

liquibase --help

Проєкт Liquibase

Спочатку потрібно створити проєкт — набір файлів, необхідний для роботи з міграціями. Для цього виконай наступну команду в терміналі:

cd /repos 
ls
liquibase init project 
ls

Ця команда створить декілька файлів. Далі в контейнері переналаштуй права на ці файли, щоб дозволити читання та запис усім користувачам. Це потрібно зробити для того, щоб користувач, під яким ти виконуєш команди в контейнері, міг редагувати файли, створені з контейнера в текстовому редакторі на комп’ютері:

ls -la
chmod 777 *  
ls -la

У VS Code подивимось, які файли створились:

✔️ liquibase.properties — найважливіший файл, який зберігає конфігурацію проєкту:

####     _     _             _ _
##      | |   (_)           (_) |
##      | |    _  __ _ _   _ _| |__   __ _ ___  ___
##      | |   | |/ _` | | | | | '_ \ / _` / __|/ _ \
##      | |___| | (_| | |_| | | |_) | (_| \__ \  __/
##      \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___|
##                  | |
##                  |_|
##
##      The liquibase.properties file stores properties which do not change often,
##      such as database connection information. Properties stored here save time
##      and reduce risk of mistyped command line arguments.
##      Learn more: https://docs.liquibase.com/concepts/connections/creating-config-properties.html
####
####
##   Note about relative and absolute paths:
##      The liquibase.properties file requires paths for some properties.
##      The classpath is the path/to/resources (ex. src/main/resources).
##      The changeLogFile path is relative to the classpath.
##      The url H2 example below is relative to 'pwd' resource.
####
# Enter the path for your changelog file.
changeLogFile=example-changelog.sql


#### Enter the Target database 'url' information  ####
liquibase.command.url=jdbc:mysql://10.20.30.40:3306/liquibase


# Enter the username for your Target database.
liquibase.command.username: mysqluser


# Enter the password for your Target database.
liquibase.command.password: P@ssw0rd

У цьому файлі:

  • changeLogFile вказує де зберігається changelog-file, який описує версії схеми бази даних;
  • url вказує налаштування для підключення до бази даних: тип БД, адресу, TCP-порт та ім’я БД, з якою ми працюватимемо в цьому проєкті;
  • username та password вказують ім'я користувача та пароль для сервера бази даних, до якого ми підключатимемось.

✔️ changelog-file — файл, який описує версії схеми бази даних як changesets (набір змін, які потрібно здійснити, щоб привести схему бази даних з попередньої версій до наступної). Кожен чейнжсет має ім’я, версію та команди для переходу на поточну версію з попередньої.

--liquibase formatted sql


--changeset your.name:1 labels:example-label context:example-context
--comment: example comment
create table person (
   id int primary key auto_increment not null,
   name varchar(50) not null,
   address1 varchar(50),
   address2 varchar(50),
   city varchar(30)
)
--rollback DROP TABLE person;


--changeset your.name:2 labels:example-label context:example-context
--comment: example comment
create table company (
   id int primary key auto_increment not null,
   name varchar(50) not null,
   address1 varchar(50),
   address2 varchar(50),
   city varchar(30)
)
--rollback DROP TABLE company;


--changeset other.dev:3 labels:example-label context:example-context
--comment: example comment
alter table person add column country varchar(2)
--rollback ALTER TABLE person DROP COLUMN country;

Liquibase може працювати з чейнжсетами в різних форматах: xmlyamljson та sql. Ми працюватимемо з sql. Чейнжсет — це по суті SQL-код для створення бази даних з коментарями спеціального формату для Liquibase. Якщо ми поглянемо на чейнжсет, який створився за допомогою команти liquibase init project, то можна помітити, що перед початком чейнжсету потрібно вказувати коментар --changeset. Також, туди можна додавати коментарі та rollback-команду, щоб відкотити зміни цього конкретного чейнжсету.

💡 Решта файлів нам не потрібні, тому їх можна видалити.

Виконання міграції

Тепер спробуємо розгорнути базу даних за допомогою Liquibase. Для демонстрації ми використовуватимемо changelog-file, який створився, коли ми ініціалізували проєкт.

Спершу нам потрібно створити пусту БД на нашому сервері бази даних та налаштувати права на неї для користувача, під яким працюватиме Liquibase. Для цього під'єднаємось до нашого сервера бази даних та виконаємо ось такий запит:

create database liquibase;

Тепер підготуємо properties-файл, а саме заповнимо дані для підключення: тип бази, IP-адресу сервера БД, мережевий порт, ім’я БД, ім’я користувача та пароль.

💡 На реальному проєкті ім'я користувача та пароль для сервера бази даних слід вказувати як змінні середовища, і ні в якому випадку не комітити в репозиторій. Більше про те, як користуватись змінними середовища, ти можеш знайти в документації Liquibase Environment Variables.

Повернемось в контейнер з Liquibase. Щоб привести базу даних до останньої версії, описаної в changelog-file, потрібно виконати команду liquibase update. Зверни увагу, що команду потрібно виконувати в теці, у якій зберігається properties-файл — саме з нього Liquibase зчитає налаштування:

liquibase update

Liquibase створив таблиці company i person, які були описані в changelog-file. Окрім них створились ще дві таблиці, які Liquibase використовує в службових цілях для відстежування змін схеми між версіями.

Спробуємо створити новий чейнжсет та додамо таблицю products. Для цього спершу потрібно додати SQL-запит для створення таблиці:

create table products ( id int primary key auto_increment not null, name varchar(50) not null ); 

Щоб цей код став чейнжсетом, потрібно додати відповідний коментар перед кодом:

--changeset user:4

Кожен чейнжсет повинен мати автора та версію. Чейнжсети також можна позначати лейблами та контекстами — тегами, які дозволяють групувати чейнжсети за потреби. Для нашого чейнжсету ми не вказуватимемо теги.

Для чейнжсетів також потрібно прописувати rollback step — код, який потрібно виконати, щоб відкотити зміну цього чейнжсету. У нашому випадку ми додаємо нову таблицю, ролбек степом такої операції буде видалення таблиці. Зі всіма цими змінами, наш чейнжсет виглядатиме ось так:

--changeset user:4
create table products ( id int primary key auto_increment not null, name varchar(50) not null ); 
--rollback DROP TABLE products;

Новий чейнжсет доданий в changelog-file.

Додамо теги в Liquibase, які дозволять фіксувати стан бази після застосування групи чейнжсетів:

liquibase tag other.dev:3

Приведемо схему бази даних до останньої версії:

liquibase update

Цього разу Liquibase під'єднався до бази даних, зі службових таблиць дізнався останній застосований чейнжсет та застосував той, який ми додали і який ще не був застосований.

Тепер уявімо, що зміни, які ми внесли в схему бази даних, призвели до помилки в системі, і нам потрібно їх відкотити. Для цього потрібно скористатись командою liquibase rollback. Їй, як параметр, потрібно вказати тег, до стану якого потрібно відкотитись:

liquibase rollback –tag=other.dev:3

Ця команда знайшла в службовій таблиці в базі даних список всіх чейнжсетів, які були застосовані після тегу, зчитала ці ченжсети з changelog-file та виконала їх ролбеки.

💡 Якби після кожної зміни схеми бази даних ми просто виконували б SQL-скрипт, що створює таблиці, його виконання завершилось би помилкою, оскільки на початку скрипту є команди створення таблиць, які вже існують в БД. Щоб це вирішити, потрібно було б створювати окремі файли для кожної зміни та самостійно слідкувати за тим, які файли виконались. Окрім того, відкочувати зміни таким способом набагато важче.

How to Test Yourself

Just in case you want to test your script on your database before submitting a pull request, you can do it by performing the following actions:

  1. Drop the ShopDB database using the DROP DATABASE ShopDB; statement if you already have it on your database server.
  2. Create an empty ShopDB database on your database server.
  3. Run the initial schema migration for the ShopDB database with Liquibase using Docker on your computer:
docker run -v $(pwd):/repos --workdir /repos/ -e INSTALL_MYSQL=true \
-e LIQUIBASE_COMMAND_USERNAME=<db username> \
-e LIQUIBASE_COMMAND_PASSWORD=<db password> \
-e LIQUIBASE_COMMAND_URL=jdbc:mysql://<db host>:3306/ShopDB \
liquibase/liquibase liquibase update --labels="0.0.1"

Make sure to run the command in the folder where the repository is cloned. If you are running the script on Windows, replace $(pwd) with the full path of the cloned repository.

Replace <db username>, <db password>, and <db host> with values for your database server before running the command.

  1. Tag the database with the initial version, so you will be able to rollback any new changesets:
docker run -v $(pwd):/repos --workdir /repos/ -e INSTALL_MYSQL=true \  
-e LIQUIBASE_COMMAND_USERNAME=<db username> \
-e LIQUIBASE_COMMAND_PASSWORD=<db password> \
-e LIQUIBASE_COMMAND_URL=jdbc:mysql://<db host>:3306/ShopDB \
liquibase/liquibase liquibase tag 0.0.1
  1. Use commands described in steps 3 and 4 to update the database to versions 0.0.2 and 0.0.3 you will create while working on this task.
  2. Test rollback of the changeset by reverting the state of the database to a tag:
docker run -v $(pwd):/repos --workdir /repos/ -e INSTALL_MYSQL=true \  
-e LIQUIBASE_COMMAND_USERNAME=<db username> \
-e LIQUIBASE_COMMAND_PASSWORD=<db password> \
-e LIQUIBASE_COMMAND_URL=jdbc:mysql://<db host>:3306/ShopDB \
liquibase/liquibase liquibase rollback <tag name>

Replace <tag name> with the tag version you want to rollback to, for example, 0.0.2 or 0.0.1.