Repeatable migrations в Flyway
После статьи о логике в БД я много дискутирую в комментариях. Часто вижу заблуждение, что любая миграция при таком подходе — это новый файл, который невозможно сравнить со старым. Я покажу что это не так на примере Flyway.
Что такое Repeatable migrations (RM)?
Это миграции, обозначенные особым префиксом R__. Файл с запросами будет бежать в одном из случаев:
- Миграция еще не была выполнена успешно
- Хэш-сумма миграции изменилась
- Файл был переименован
Если у вас несколько RM, запускаться они будут в алфавитном порядке и только после успешного выполнения рядовых Vxx__ миграций.
В таблице состояния Flyway будет записано каждое изменение хэш суммы:
Что писать в RM
Functions, stored procedures, views и захардкоженные данные. А в последствии изменять их в этих же файлах. Мой подход — по возможности разделить хранимые в бд объекты по типу + один файл на данные. Итого R__create_functions.sql, R__create_stored_procedures.sql, R__create_views.sql, R__data.sql. Трудности начнутся, если изменения в этих файлах зависимы одни на другие. В этом случаи переименовывайте, и разделяйте заново.
Один нюанс — все запросы в RM должны быть идемпотентны. Это одно из немногих мест, где идемпотентные миграции допустимы:
-- захардкоженные данные в БД, на которые лень писать админку
INSERT INTO country (country_id, country)
VALUES (1, 'USA'),
(2, 'Ukraine'),
(3, 'Belarus')
ON CONFLICT (country_id) DO NOTHING;
Триггеры и индексы лучше оставить в том месте, где создана\изменена таблица:
CREATE TABLE film
(
film_id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
release_year year,
language_id SMALLINT REFERENCES language ON UPDATE CASCADE ON DELETE RESTRICT NOT NULL,
original_language_id SMALLINT REFERENCES language ON UPDATE CASCADE ON DELETE RESTRICT,
rental_duration SMALLINT DEFAULT 3 NOT NULL,
rental_rate NUMERIC(4, 2) DEFAULT 4.99 NOT NULL,
length SMALLINT,
replacement_cost NUMERIC(5, 2) DEFAULT 19.99 NOT NULL,
rating mpaa_rating DEFAULT 'G',
special_features feature[],
last_update TIMESTAMPTZ DEFAULT now() NOT NULL,
fulltext TSVECTOR NOT NULL
);
CREATE TRIGGER film_fulltext_trigger
BEFORE INSERT OR UPDATE
ON film
FOR EACH ROW
EXECUTE PROCEDURE tsvector_update_trigger('fulltext', 'pg_catalog.english', 'title', 'description');
CREATE INDEX ON film (fulltext);
CREATE INDEX ON film (title);
CREATE INDEX ON film (language_id);
CREATE INDEX ON film (original_language_id);
Вот так у вас будет выглядеть diff в гите:
Ссылка на гитхаб.
Официальная дока.