惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Cloudbric
Cloudbric
P
Privacy International News Feed
T
The Exploit Database - CXSecurity.com
C
Cisco Blogs
P
Palo Alto Networks Blog
C
Cybersecurity and Infrastructure Security Agency CISA
V
Visual Studio Blog
The Cloudflare Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
V
V2EX
Google DeepMind News
Google DeepMind News
G
GRAHAM CLULEY
L
LINUX DO - 热门话题
S
Securelist
C
Cyber Attacks, Cyber Crime and Cyber Security
NISL@THU
NISL@THU
T
Tenable Blog
E
Exploit-DB.com RSS Feed
W
WeLiveSecurity
TaoSecurity Blog
TaoSecurity Blog
Know Your Adversary
Know Your Adversary
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Hacker News
The Hacker News
V
Vulnerabilities – Threatpost
S
Security @ Cisco Blogs
S
Security Affairs
Forbes - Security
Forbes - Security
P
Privacy & Cybersecurity Law Blog
MongoDB | Blog
MongoDB | Blog
T
Tailwind CSS Blog
F
Fortinet All Blogs
GbyAI
GbyAI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
N
Netflix TechBlog - Medium
Y
Y Combinator Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
Project Zero
Project Zero
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Tor Project blog
Microsoft Security Blog
Microsoft Security Blog
美团技术团队
C
Check Point Blog
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
I
InfoQ
SecWiki News
SecWiki News

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Spring Boot 4, Flyway и Postgres: как не стрелять по ногам
Карандашов Владислав · 2026-06-15 · via Все публикации подряд на Хабре

Средний

12 мин

91

По Flyway миграциям уже написано много туториалов: как их называть, куда класть и зачем нужна таблицаflyway_schema_history. Это полезно, но сегодня мы закопаемся поглубже в детали актуальной интеграции Flyway в приложение.

В этой статье не будет пересказа базовых команд Flyway. Вместо этого разберем несколько интересных нюансов:

  1. что приносят и как устроены spring-boot-starter-flyway и spring-boot-starter-flyway-test;

  2. зачем нужны flyway-database-postgresql и другие адаптеры;

  3. как настройка transactional-lock влияет на нашу жизнь;

  4. какие бонусные фичи может нам предложить flyway в spring boot сервисе.

Признаюсь честно: триггером к детальному разбору Flyway и его настройкам послужила весьма банальная проблема с зависшей миграцией. Простое создание индекса намертво не давало запуститься моему сервису. Хочу поделиться этим опытом и сэкономить другим часы жизни.

Постараемся внедрить Flyway в Spring Boot 4.x качественно! И посмотрим на интересные фичи.

Spring Boot 4.x и новые стартеры

Начинаем с базы. Раньше требовалось подключить flyway-core + flyway-database-postgresql (или другой адаптер). Но в Spring Boot 4.x изменилась модель зависимостей: многие интеграции получили собственные main/test starters. Теперь используется отдельный spring-boot-starter-flyway, это часть общей модульной перестройки SB4:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-flyway")
    runtimeOnly("org.flywaydb:flyway-database-postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-flyway-test")
}

Если заглянуть в зависимости starter’а, там нет большого сюрприза:

spring-boot-starter-flyway
 ├─ spring-boot-starter
 ├─ spring-boot-starter-jdbc
 ├─ spring-boot-flyway
 └─ spring-boot-jdbc

Auto-configuration включается при нескольких условиях:

  • в classpath есть org.flywaydb.core.Flyway;

  • есть подходящий DataSource или отдельные connection details для Flyway;

  • spring.flyway.enabled не выключен.

После этого Boot не просто вызывает Flyway.configure() и запускает миграции. Он собирает полноценный Flyway bean из нескольких источников конфигурации. Происходит примерно следующее:

  • берется основной DataSource приложения или отдельный DataSource, указанный для Flyway;

  • читаются значения из spring.flyway.* настроек;

  • из Spring context подхватываются callbacks и Java migrations;

  • пользовательские FlywayConfigurationCustomizer получают последний шанс изменить конфигурацию;

  • после этого создается готовый объект Flyway, который Spring Boot сможет запустить при старте приложения.

Callbacks и Java migrations можно подключать как Spring beans (об этом ниже). Это удобно, когда callback должен использовать инфраструктуру приложения.

Зачем нужен flyway-database-postgresql

Начиная с новых версий Flyway поддержка многих баз вынесена в отдельные plugin modules.

Этот модуль:

  • регистрирует PostgreSQL database type как Flyway plugin;

  • содержит PostgreSQL parser;

  • знает особенности PostgreSQL DDL;

  • определяет, какие statements могут выполняться в транзакции;

  • реализует PostgreSQL advisory lock для flyway_schema_history;

  • предоставляет PostgreSQLConfigurationExtension, куда Spring Boot передает spring.flyway.postgresql.*.

Например, в PostgreSQLParser есть отдельное правило для CREATE INDEX CONCURRENTLY:

private static final Pattern CREATE_INDEX_CONCURRENTLY_REGEX =
        Pattern.compile("^(CREATE|DROP)( UNIQUE)? INDEX CONCURRENTLY");

protected Boolean detectCanExecuteInTransaction(String simplifiedStatement, List<Token> keywords) {
    if (CREATE_INDEX_CONCURRENTLY_REGEX.matcher(simplifiedStatement).matches() /* ... */) {
        return false;
    }
    // ...
}

То есть Flyway не просто отправляет SQL как строку в JDBC. Он парсит миграцию, понимает типы statements и строит модель выполнения. Это особенно важно для PostgreSQL, где большинство DDL можно откатить транзакцией, но некоторые команды принципиально должны идти вне transaction block.

Базовая настройка в приложении

Если spring.flyway.url не задан, Boot использует основной DataSource. Но можно и разнести доступы:

spring:
  flyway:
    url: ${DATABASE_URL}
    user: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}
    default-schema: ${DATABASE_SCHEMA}

Еще одна полезная деталь — {vendor} в locations:

spring:
  flyway:
    locations:
      - classpath:db/migration/common
      - classpath:db/migration/{vendor}

Внутри FlywayAutoConfiguration.LocationResolver Spring Boot определяет vendor по JDBC URL и заменяет {vendor} на идентификатор базы. Для PostgreSQL это будет postgresql. При аккуратном использовании это позволяет держать общие миграции отдельно от database-specific SQL. Но злоупотреблять этим не стоит: если приложение официально поддерживает только PostgreSQL, отдельная папка postgresql может создать лишнюю иллюзию переносимости.

Что дает spring-boot-starter-flyway-test

В Spring Boot 4.x у многих технологий появился companion test starter. Для Flyway это:

testImplementation("org.springframework.boot:spring-boot-starter-flyway-test")

Это не отдельный большой фреймворк для тестирования миграций, просто агрегация:

spring-boot-starter-flyway
spring-boot-starter-test
spring-boot-starter-jdbc-test

То есть он подтягивает обычную test-инфраструктуру Boot, JDBC test support и сам Flyway starter. Это соответствует новой модели Spring Boot 4.x: в тестах подключаем starter’ы тех технологий, которые реально находятся под тестом.

Если ваше приложение и так живет в экосистеме Spring Boot: подключать дополнительно spring-boot-starter-flyway-test едва ли потребуется.

Главный кейс: CREATE INDEX CONCURRENTLY

В PostgreSQL создание индекса — не всегда безобидная операция. Обычный CREATE INDEX блокирует записи в таблицу на время построения индекса. Для больших production-таблиц это может быть неприемлемо, поэтому PostgreSQL поддерживает:

CREATE INDEX CONCURRENTLY idx_orders_created_at 
    ON orders (created_at);

В документации PostgreSQL 18 по CREATE INDEX описаны компромиссы: concurrent build не блокирует обычные insert/update/delete, но требует больше работы, выполняет несколько проходов по таблице, ждет старые транзакции и при ошибке может оставить invalid index, который придется отдельно удалить или перестроить.

Ключевое ограничение: CREATE INDEX CONCURRENTLY нельзя выполнять внутри transaction block.

Flyway это знает. Как было показано выше, PostgreSQL parser помечает такой statement как non-transactional. Кажется, на этом проблема должна закончиться: Flyway увидел non-transactional migration и запустил ее вне транзакции.

Но есть второй уровень — lock самого Flyway.

Перед миграцией Flyway должен защититься от ситуации, когда два экземпляра приложения одновременно начинают менять схему. В PostgreSQL для этого используется advisory lock. В flyway-database-postgresql это класс:

org.flywaydb.database.postgresql.PostgreSQLAdvisoryLockTemplate

Сильно упрощенный фрагмент:

public <T> T execute(Callable<T> callable) {
    PostgreSQLConfigurationExtension extension =
            configuration.getPluginRegister().getExact(PostgreSQLConfigurationExtension.class);

    if (extension.isTransactionalLock()) {
        return new TransactionalExecutionTemplate(jdbcTemplate.getConnection(), true)
                .execute(() -> execute(callable, this::tryLockTransactional));
    } else {
        try {
            return execute(callable, this::tryLock);
        } finally {
            unlock(rethrow);
        }
    }
}

А внутри:

SELECT pg_try_advisory_xact_lock(...)
--или
SELECT pg_try_advisory_lock(...)

Разница принципиальная. PostgreSQL advisory locks бывают transaction-level и session-level. В документации PostgreSQL 18 это описано так: transaction-level lock автоматически освобождается в конце транзакции, а session-level lock живет до явного unlock или завершения сессии.

По умолчанию Flyway для PostgreSQL использует transactional lock. Это удобно для большинства миграций: lock привязан к транзакции, rollback гарантированно освобождает его. Но для CREATE INDEX CONCURRENTLY такой подход может стать проблемой.

Почему? Потому что lock template сам открывает транзакционную обертку вокруг участка, внутри которого Flyway применяет миграцию. Даже если сама SQL-миграция распознана как non-transactional, внешний transaction-level advisory lock уже создал transaction block. Для PostgreSQL этого достаточно, чтобы CREATE INDEX CONCURRENTLY оказался в недопустимом контексте.

Именно поэтому у Flyway есть PostgreSQL-specific настройка:

spring:
  flyway:
    postgresql:
      transactional-lock: false

В терминах чистого Flyway это соответствует:

flyway.postgresql.transactional.lock=false

Redgate в документации по PostgreSQL прямо указывает этот кейс: transactional lock по умолчанию может создавать проблемы с некоторыми SQL statements, особенно с CREATE INDEX CONCURRENTLY — в таком случае его заменяют session-level lock.

После переключения Flyway все еще сериализует миграции advisory lock’ом, но больше не заворачивает выполнение в transaction-level lock. CREATE INDEX CONCURRENTLY получает нужное ему условие: отсутствие внешнего transaction block.

Нужно ли всегда делать transactional-lock: false

Короткий ответ: нет, но для PostgreSQL-проектов с online DDL это нормальная осознанная настройка.

Если в проекте все миграции транзакционные, дефолтный transactional lock хорош: он проще в плане жизненного цикла и автоматически освобождается вместе с транзакцией. Это надежный default.

Если у вас есть или планируются миграции с:

  • CREATE INDEX CONCURRENTLY;

  • DROP INDEX CONCURRENTLY;

  • некоторыми вариантами REINDEX;

  • VACUUM;

  • ALTER SYSTEM;

  • другими командами, которые PostgreSQL запрещает внутри transaction block,

то spring.flyway.postgresql.transactional-lock=false стоит рассматривать как часть стандартной конфигурации.

Компромисс такой: session-level lock требует явного освобождения. Flyway делает pg_advisory_unlock(...) в finally, а PostgreSQL дополнительно освобождает session locks при завершении соединения. Это достаточно надежно, но семантически уже не так чисто, как transaction-level lock. Если соединение остается жить в pool, а unlock по какой-то причине не прошел, lock может прожить дольше, чем хотелось бы. Это редкий сценарий, но именно поэтому default остается консервативным.

Отдельно стоит подчеркнуть: spring.flyway.execute-in-transaction=false не является полным заменителем postgresql.transactional-lock=false. Первое управляет тем, будет ли Flyway выполнять SQL migrations внутри транзакции. Второе управляет тем, как Flyway берет lock вокруг миграционного процесса. Для CREATE INDEX CONCURRENTLY важны оба уровня, и ловушка часто находится именно во втором.

group, mixed и транзакционная модель

Вокруг этого кейса легко перепутать несколько похожих настроек.

spring.flyway.group=true говорит Flyway попытаться применить все pending migrations в одной транзакции. В документации Flyway эта настройка рекомендуется только для баз с нормальной поддержкой DDL transactions. PostgreSQL такую поддержку в целом имеет, но не для всех statements. Если в наборе pending migrations есть CREATE INDEX CONCURRENTLY, group=true почти наверняка не то, что вам нужно.

spring.flyway.mixed=true разрешает смешивать transactional и non-transactional statements в одной migration или migration group. По умолчанию false, и это хорошо. Если Flyway видит, что в одном SQL-файле смешаны обычный DDL и CREATE INDEX CONCURRENTLY, он остановится и скажет об этом. Это раздражает ровно до момента, пока не вспомнить, что rollback у такой миграции будет частичным или невозможным.

Практическое правило простое: non-transactional PostgreSQL statements лучше выносить в отдельную миграцию:

-- V12__add_orders_status.sql
ALTER TABLE orders
    ADD COLUMN status VARCHAR(32);

-- V13__idx_orders_status_concurrently.sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_status
    ON orders (status);

Так проще читать историю, проще понимать rollback-поведение и проще диагностировать сбои.

spring.flyway.lock-retry-count — еще одна настройка рядом с этой темой. Flyway берет lock, чтобы параллельные инстансы не применяли миграции одновременно. Если lock сразу недоступен, он повторяет попытки. В документации Flyway default — 50, попытки идут с интервалом около секунды. В Kubernetes или другом окружении с несколькими стартующими репликами это может быть важнее, чем кажется: одна реплика мигрирует, остальные ждут lock. Если миграции тяжелые, 50 секунд может быть мало.

Несколько свойств, которые стоит знать

FlywayProperties большой. Делать из статьи полный справочник бессмысленно, но есть настройки, которые регулярно всплывают в реальных проектах.

baseline-on-migrate

spring:
  flyway:
    baseline-on-migrate: true

Это автоматический baseline при migrate, если схема непустая, но таблицы history еще нет. В официальной документации отдельно предупреждают: настройка удобна для первичного внедрения Flyway в существующую production-базу, но убирает safety net, который защищает от миграции не той базы при ошибке конфигурации.

Важно не путать:

  • baseline command — зарегистрировать существующее состояние схемы как стартовую точку;

  • baseline migration — отдельный тип миграционного скрипта, который может быстро создать новую базу с некоторой версии.

Кстати, про baseline migrations на Хабре недавно был хороший материал: Flyway Baseline миграция без лишних слов. При настройке Boot достаточно понимать, что baseline-on-migrate автоматизирует baseline для непустой схемы без history table.

clean-disabled

spring:
  flyway:
    clean-disabled: true

В Spring Boot default — true, и это правильно. clean удаляет объекты схемы, которыми управляет Flyway. В production это почти всегда команда из категории “нельзя случайно”. Для локальной разработки или тестов можно отключать защиту в отдельном profile, но включать clean в общем конфиге приложения — плохой сигнал.

out-of-order

spring:
  flyway:
    out-of-order: true

Разрешает применить миграцию с версией ниже текущей, если она появилась позже. Например, production уже на V10, а из долгоживущей ветки приехала V8_1. С out-of-order=false Flyway откажется. С true применит.

Это может помочь командам с параллельными release branches, но делает историю менее линейной. Если вы включаете out-of-order, это должно быть частью release-процесса, а не способом регулярно “как-нибудь пронести” забытые миграции.

validate-migration-naming

spring:
  flyway:
    validate-migration-naming: true

По умолчанию Boot оставляет false. В реальной работе я бы рекоменендовал ставить true: лучше упасть на старте из-за файла stupid_init.sql, чем тихо проигнорировать миграцию с неправильным именем и получить расхождение окружений.

ignore-migration-patterns

Default в Boot — *:future. Это означает, что future migrations не валят validation. Future migration — запись в history table, версия которой выше, чем все известные локальному приложению миграции. Такое бывает при rollback приложения на предыдущую версию кода.

Default разумный для rolling deployments: старая версия приложения может стартовать против базы, которую уже мигрировала новая версия. Но если у вас строгая модель “код и база всегда разворачиваются атомарно”, это поведение можно пересмотреть.

target

spring:
  flyway:
    target: "12"

Ограничивает версию, до которой Flyway должен мигрировать. В обычном приложении почти всегда latest. Но target полезен в тестах, при воспроизведении проблем и в редких staged rollout сценариях, когда нужно поднять базу только до конкретного состояния.

placeholders

spring:
  flyway:
    placeholders:
      app_schema: app

В миграции:

CREATE TABLE ${app_schema}.events
(...);

Механизм удобный, но его стоит использовать ограниченно. Чем больше миграция зависит от runtime-подстановок, тем сложнее гарантировать, что один и тот же файл создает одну и ту же схему во всех окружениях. Для имен схемы, tablespace или небольших environment-specific деталей это нормально. Для условной бизнес-логики — нет.

callbacks

Flyway callbacks позволяют подключаться к lifecycle событиям: beforeMigrate, afterMigrate, afterEachMigrate, beforeClean и так далее. В документации среди типичных сценариев указаны перекомпиляция процедур, обновление materialized views, housekeeping вроде PostgreSQL VACUUM.

В Spring Boot есть два способа:

  1. Положить SQL callbacks в configured callback locations.

  2. Создать bean типа org.flywaydb.core.api.callback.Callback.

Пример Java callback:


@Component
class MigrationMetricsCallback extends BaseCallback {

    @Override
    public boolean supports(Event event, Context context) {
        return event == Event.AFTER_MIGRATE;
    }

    @Override
    public void handle(Event event, Context context) {
        // записать техническую метрику, обновить служебное состояние
    }
}

Callbacks — мощный механизм, но его легко испортить. Хороший callback обслуживает миграционный процесс. Плохой callback незаметно меняет прикладные данные, ходит во внешние сервисы или начинает зависеть от Spring beans, которые сами зависят от уже мигрированной схемы.

skip-executing-migrations

Настройка опасная и поэтому интересная:

spring:
  flyway:
    skip-executing-migrations: true

Она говорит Flyway обновлять schema history, не выполняя содержимое миграций. Это не ускоритель и не способ “пропустить тяжелые SQL”. Это инструмент для специальных процедур: ручного выравнивания history table после внешнего применения изменений, восстановления окружения, редких migration adoption сценариев. В обычном application startup ему почти никогда не место.

Практический шаблон для PostgreSQL

Для Spring Boot 4.x приложения на PostgreSQL я бы начинал примерно с такого набора:

spring:
  flyway:
    enabled: true
    url: ${DATABASE_URL}
    user: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}
    default-schema: ${DATABASE_SCHEMA}
    locations:
      - classpath:db/migration
    validate-on-migrate: true
    validate-migration-naming: true
    clean-disabled: true
    postgresql:
      transactional-lock: false

Если в проекте принципиально нет online DDL и все миграции транзакционные, transactional-lock можно оставить true. Но если вы используете PostgreSQL как PostgreSQL, а не как абстрактную SQL-базу, рано или поздно вам понадобятся CONCURRENTLY, VACUUM, REINDEX или другие non-transactional statements.

Главные выводы

CREATE INDEX CONCURRENTLY — хороший пример того, почему миграции нельзя воспринимать как набор SQL. PostgreSQL запрещает эту команду внутри transaction block. Flyway умеет распознавать ее как non-transactional, но PostgreSQL transactional advisory lock по умолчанию сам создает внешнюю транзакционную обертку. Настройка spring.flyway.postgresql.transactional-lock=false переключает lock на session-level и убирает этот конфликт.

Остальные свойства FlywayProperties стоит включать не по привычке, а по конкретной причине. baseline-on-migrate полезен при внедрении Flyway в существующую базу, но снижает защиту от ошибки окружения. clean-disabled=true должен оставаться production default. group и mixed требуют понимания транзакционной модели конкретной базы. callbacks хороши для технического обслуживания миграций, но не для скрытой прикладной логики.

В итоге качественная интеграция Flyway в Spring Boot 4.x — это не только зависимость и папка db/migration. Это осознанный выбор database plugin, транзакционной модели, lock strategy, test runtime и набора свойств, которые соответствуют реальному поведению вашей СУБД.