В этом гайде мы расскажем, как подключить и настроить Flyway в Spring Boot приложении, сгенерировать скрипты инициализации и миграции схемы базы данных вместе с Amplicode!

Примерное время прочтения: 20 минут.

Введение

В процессе разработки приложения нам время от времени приходится вносить изменения в JPA модель, что влечет за собой эволюцию схемы базы данных.

Без использования системы контроля версий существует риск потери данных и возникновения сложностей при совместной работе нескольких разработчиков над проектом. На сегодняшний день наиболее распространенной системой контроля версий является git, который отлично подходит для отслеживания изменений во всех файлах проекта.

Однако, помимо кода, не менее важно версионировать и базу данных, чтобы избежать потери данных при изменениях и иметь возможность отката к предыдущим версиям схемы БД в случае неудачного обновления или ошибки в процессе разработки.

Для эффективного управления версиями базы данных существуют специализированные решения, такие как Flyway.

Данный гайд посвящен использованию Flyway в Spring Boot приложениях с помощью Amplicode для наиболее удобного и эффективного управления базами данных.

Список задач, рассматриваемых в данном гайде

  1. Подключение и настройка Flyway

  2. Расширение JPA модели и написание скриптов миграции БД

  3. Валидация JPA модели и БД

Обзор приложения

Прежде чем приступать к решению поставленных задач, необходимо изучить структуру используемого приложения BlogApplicaion с помощью панели Amplicode Explorer.

Панель выглядит следующим образом:

Чтобы проанализировать приложение в контексте используемых фреймворков и библиотек с помощью панели Amplicode Explorer, нажмите на значок стрелочки рядом с именем проекта, чтобы развернуть его. Здесь мы можем узнать, какие модули подключены к нашему проекту, что из себя представляет слой данных, какие эндпоинты доступны, а также какие файлы для развертывания приложений уже есть в проекте. Вы можете продолжать нажимать на стрелочки рядом с различными разделами панели, чтобы получить больше информации:

Исходя из знаний Amplicode о нашем проекте, мы можем сделать следующие выводы:

  1. Для удобной работы с персистентным слоем в приложении используется модуль Spring Data JPA (1).

  2. В качестве СУБД используется PostgreSQL (2).

  3. Мы можем более детально ознакомиться с моделью данных нашего приложения.

  • В качестве базовой для всех сущности используется mappedsuperclass BaseEntity (3.1).

  • Сущности Post и User связаны отношением "многие к одному" (3.2).

  1. Наконец, мы можем посмотреть на docker compose файлы и их элементы (4).

Подключение и настройка Flyway к приложению

Первая задача — это подключение и настройка Flyway к Spring Boot приложению с уже разработанной ранее JPA моделью. В процессе решения этой задачи, помимо добавления необходимой зависимости и настройки файла application.properties, нам также потребуется создать скрипт инициализации базы данных.

Для взаимодействия с PostgreSQL было бы удобно иметь также сервис pgAdmin. Amplicode предлагает pgAdmin в списке рекомендуемых сервисов, так как видит сервис PostgreSQL в текущем файле. Чтобы воспользоваться этой возможностью, нажмите на иконку в виде лампочки и выберите Add pgAdmin service в появившемся меню:

Откроется всплывающий диалог Add pgAdmin to Docker Compose. Чтобы воспользоваться предоставленной Amplicode возможностью для настройки автоматического подключения PostgreSQL к pgAdmin:

  • Отметьте чекбокс Configure DB server connections

  • Оставьте значения всех остальных параметров по умолчанию

  • Нажмите OK.

Amplicode добавит в файл docker-compose.yaml код для pgAdmin сервиса:

pgadmin:
    image: dpage/pgadmin4:8.9
    restart: "no"
    ports:
      - "5050:80"
    volumes:
      - pgadmin_data:/var/lib/pgadmin
      - ./docker/pgadmin/servers.json:/pgadmin4/servers.json
      - ./docker/pgadmin/pgpass:/pgadmin4/pgpass
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@admin.com
      PGADMIN_DEFAULT_PASSWORD: root
      PGADMIN_CONFIG_SERVER_MODE: "False"
      PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:80/misc/ping || exit -1
      interval: 10s
      timeout: 5s
      start_period: 10s
      retries: 5
    entrypoint: /bin/sh -c "chmod 600 /pgadmin4/pgpass; /entrypoint.sh;"
volumes:
  pgadmin_data:

Теперь необходимо запустить все сервисы, чтобы посмотреть на актуальное состояние базы данных. Для этого необходимо нажать на иконку двойной стрелочки напротив слова services в файле docker-compose.yaml.

После запуска Amplicode добавит inlay для удобного открытия адреса, связанного с сервисом, прямо из IntelliJ IDEA.

На данный момент база данных находится в том же состоянии, что и JPA модель. Переходим к подключению Flyway к нашему приложению.

Amplicode Explorer позволяет добавить необходимые стартеры и библиотеки к проекту. Для этого необходимо:

  • Щелкнуть правой кнопкой мыши по узлу Configuration

  • Выбрать опцию Add Configuration

  • В открывшемся окне выбрать DB Migration Configuration

Откроется диалоговое окно DB Migration Settings.

Далее необходимо сделать следующее:

  • В открывшемся всплывающем диалоговом окне выбрать Flyway

  • Оставить выбранный автоматически файл application.properties, значения для префикса и сепаратора генерируемых файлов, а также путь, по которому будут сохраняться скрипты миграции, как задано по умолчанию

  • Отметить чекбоксы Create init DB scripts и Enable baseline on migrate.

Благодаря выбору первого чекбокса произойдет автоматическое перенаправление к окну генерации скрипта базы данных. Благодаря выбору второго чекбокса Amplicode добавит свойство в файл application.properties, позволяющее запускать приложение и выполнять скрипт инициализации только в том случае, если база данных будет пустой. В остальных случаях скрипт инициализации будет отмечен как выполненный, но фактически выполняться не будет.

  • В качестве источника данных для генерации скрипта инициализации выберем базу данных. Для анализа ее структуры Amplicode потребуется подключение к базе данных.

  • Выбрать опцию создания нового подключения.(Здесь необходимо отметить, что Amplicode позволяет создать подключение к базе данных с нуля, либо отталкиваясь от информации, указанной для источника данных в приложении. Здесь следует выбрать второй вариант, так как источник данных в приложении уже настроен.)

Появится следующее диалоговое окно:

  • Нажмите Create DB Connection. Теперь вы увидите следующее окно:

  • Нажмите Test Connection, чтобы проверить подключение. После этого нажмите OK.

  • Нажмите OK также в главном диалоговом окне.

  • Amplicode добавит необходимые зависимости в build.gradle и свойства в application.properties, сгенерирует директорию db.migration и начнет процесс генерации скрипта инициализации базы данных. В открывшемся окне Change Preview мы можем изменить расположение и название .sql файла, а также убедиться в корректности всего файла и его отдельных скриптов.

  • Нажмите OK

По результатам выполнения Amplicode сохранит файл миграции по указанному пути. Последнее, что необходимо сделать — это выполнить сгенерированный скрипт. Для этого можно либо запустить приложение целиком, либо настроить Flyway плагин для Gradle и воспользоваться специально предназначенной для выполнения файлов миграции командой flywayMigrate. Воспользуемся вторым вариантом.

(Помимо прочих преимуществ, Flyway плагин для Gradle пригодится вам в будущем для настройки CI/CD пайплайна.)

Для этого необходимо:

  • Добавить плагин в файл build.gradle

    plugins {
        id "org.flywaydb.flyway" version "10.15.2"
    }
    
  • Добавить зависимость для работы с PostgreSQL

    dependencies {
        runtimeOnly 'org.postgresql:postgresql'
    }
    
  • Указать необходимые параметры для подключения

    buildscript {
        dependencies {
            classpath("org.flywaydb:flyway-database-postgresql:10.10.0")
        }
    }
    flyway {
        url = 'jdbc:postgresql://localhost:5432/blog'
        user = 'root'
        password = 'root'
    }
    

    В результате файл build.gradle будет выглядеть слебдующим образом:

    buildscript {
        dependencies {
            classpath("org.flywaydb:flyway-database-postgresql:10.10.0")
        }
    }
    
    plugins {
        id 'java'
        id 'org.springframework.boot' version '3.2.5'
        id 'io.spring.dependency-management' version '1.1.4'
        id 'org.flywaydb.flyway' version '10.10.0'
    }
    
    group = 'io.amplicode'
    version = '0.0.1-SNAPSHOT'
    
    java {
      sourceCompatibility = '17'
    }
    
    repositories {
      mavenCentral()
    }
    
    dependencies {
      implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
      runtimeOnly 'org.postgresql:postgresql'
      testImplementation 'org.springframework.boot:spring-boot-starter-test'
      testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
      implementation 'org.flywaydb:flyway-core'
    }
    
    tasks.named('test') {
      useJUnitPlatform()
    }
    
    flyway {
      url = 'jdbc:postgresql://localhost:5432/blog'
      user = 'root'
      password = 'root'
    }
  • Выполнить команду flywayInfo, чтобы убедиться в корректности работы плагина

  • Выполнить команду flywayBaseline, чтобы отметить скрипт инициализации как выполненный

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

Модификация JPA модели

Приступим к модификации текущей JPA модели. Так как использование факса стало редкостью, следует убрать его из модели, удалив следующий (выделенный) код, а также связанные с ним геттер и сеттер:

Для обеспечения полноты данных сделаем атрибут email обязательным с помощью Amplicode Designer.

Также нам необходимо добавить новую сущность — Comment и установить отношение "многие ко многим" с сущностью Post.

Предварительно откроем класс сущности Post. Чтобы добавить новую сущность — Comment и установить отношение "многие ко многим" с сущностью Post с помощью Amplicode Designer:

  • Откройте окно создания ассоциации двойным щелчком на пункте меню Attributes -> Association в палитре

  • Создайте новую сущность нажатием на кнопку "Плюс", укажите ей имя и выберите родительский класс.

  • Измените кардинальность ассоциации на "многие ко многим". Amplicode уже выбрал наиболее оптимальный тип для ассоциации "многие ко многим" — Set, но для полностью корректной работы Set с JPA сущностями также необходимо чтобы у неё были корректно переопределены методы equals() и hashCode(). Amplicode знает об этом и предупреждает, что текущая реализация может быть неоптимальной в плане производительности и предложит сгенерировать реализацию методов equals() и hashCode(). Чтобы согласиться с этим предложением, нажмите соответствующую ссылку.

  • Нажмите ОК, чтобы создать соответствующий класс Comment.

Amplicode сгенерирует следующий код для сущности:

@Entity
@Table(name = "comment")
public class Comment extends BaseEntity {
    @Override
    public final boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        Class<?> oEffectiveClass = o instanceof HibernateProxy
                ? ((HibernateProxy) o).getHibernateLazyInitializer()
                .getPersistentClass()
                : o.getClass();
        Class<?> thisEffectiveClass = this instanceof HibernateProxy
                ? ((HibernateProxy) this).getHibernateLazyInitializer()
                .getPersistentClass()
                : this.getClass();
        if (thisEffectiveClass != oEffectiveClass) return false;
        Comment comment = (Comment) o;
        return getId() != null && Objects.equals(getId(), comment.getId());
    }

    @Override
    public final int hashCode() {
        return this instanceof HibernateProxy
                ? ((HibernateProxy) this).getHibernateLazyInitializer()
                .getPersistentClass()
                .hashCode()
                : getClass().hashCode();
    }
}

Теперь необходимо сгенерировать для сущности Comment новый базовый атрибут text, а также создать отношение "многие к одному" с сущностью User. Для этого:

  • Установите курсор на имени класса Comment в соответствующем файле

  • Вызовите всплывающее окно Generate, нажав Alt+Insert для Windows/Linux или ⌘+N для macOS

  • Выберите Entity Attribute

  • В окне типа атрибута выберите Persist и введите имя типа User

  • Введите тип (String) и имя (text) для атрибута

  • Нажмите OK

Amplicode сгенерирует код атрибута, а также getter и setter к нему:

@Column(name = "text")
private String text;

public String getText() {
    return text;
}

public void setText(String text) {
    this.text = text;
}
  • Еще раз вызовите окно Generate

  • Выберите Entity Attribute

  • В окне типа атрибута выберите Persist и введите имя типа User

  • Задайте название атрибута (author) и сделайте его обязательным.

  • Нажмите OK

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "author_id", nullable = false)
private User author;

public User getAuthor() {
    return author;
}

public void setAuthor(User author) {
    this.author = author;
}

Задача по модификации JPA модели выполнена. Следующий шаг — генерация скриптов миграции Flyway.

Генерация Flyway скриптов миграции для синхронизации JPA модели и схемы БД

Для создания Flyway скрипта миграции необходимо обратиться к панели Amplicode Explorer и в секции DB Versioning выбрать пункт Flyway Versioned Migration.

В открывшемся окне следует убедиться в правильности выбранных persistence unit и подключения к базе данных.

С окном предпросмотра файла миграции мы уже познакомились в процессе создания скриптов инициализации базы данных.

Стоит отметить, что в данном случае цветовая раскраска некоторых скриптов отличается, например, скрипт на удаление колонки fax отмечен красным цветом, а скрипт на добавление NotNull ограничения — желтым.

Оба скрипта являются потенциально опасными ввиду возможной потери данных, поэтому Amplicode старается обратить на них максимальное внимание со стороны разработчика.

Разрабатывая JPA модель, мы решили две бизнес-задачи: изменение сущности User и создание сущности Comment. Нам бы не хотелось складывать скрипты, относящиеся к разным бизнес-задачам, в один файл. Amplicode позволяет разнести скрипты в несколько файлов миграции, не покидая текущего окна.

Для этого необходимо:

  • Выбрать скрипты, относящиеся к изменению пользователя (1)

  • В верхней панели выбрать действие переноса скриптов в новый файл миграции (2):

  • Задать название каждому из файлов

Также обратите внимание, что для скрипта добавления NotNull ограничения Amplicode хочет предложить некоторые улучшения и сигнализирует нам об этом иконкой лампочки. Так как мы добавляем NotNull ограничение, мы должны быть уверены в том, что в таблице уже нет null значений.

Amplicode позволяет нам улучшить существующий скрипт, указав значение, которое будет проставлено для всех null значений перед добавлением ограничения.

Перед сохранением файлов миграции можем еще раз убедиться в их корректности в окне предпросмотра.

Кстати, есть еще один скрипт, который мы упустили. Это скрипт удаления индекса из таблицы Users в секции Ignored.

Amplicode автоматически разместил этот скрипт в данную секцию, т.к. создание индекса может быть довольно дорогостоящей операцией, а его добавление на уровне JPA модели не является довольно распространенным. Поэтому в реальной жизни удалять его из базы данных приходится крайне редко.

Но в случае необходимости всегда можно удалить скрипт из списка игнорируемых, либо наоборот расширить список игнорируемых скриптов, выбрав и перетащив ненужные скрипты в секцию Ignored прямо в окне предпросмотра или сконфигурировав такой список заранее в настройках.

Оба файла миграции успешно сгенерированы. Выполните их, используя команду flywayMigrate и проверьте статус исполнения командой flywayInfo.

Миграция прошла успешно.

Запуск Spring Boot приложения (анализ логов от Amplicode)

Хорошей практикой при разработке Spring Boot приложения и использовании системы версионирования баз данных является применение возможностей валидации соответствия JPA модели и схемы базы данных при помощи Hibernate. Путем использования свойства Hibernate spring.jpa.hibernate.ddl-auto со значением validate мы можем обеспечить соответствие JPA модели и схемы базы данных.

Внесите следующий код в файл application-properties:

spring.jpa.hibernate.ddl-auto=validate

Чтобы сделать это быстро, начните печатать ddl-auto и Amplicode предложит вам соответствующее свойство. Достаточно будет выбрать его из выпадающего списка и нажать Enter.

В случае обнаружения несоответствия Spring Boot приложение не запустится и выдаст ошибку.

Запустите приложение, чтобы убедиться, что в логах действительно отсутствуют исключения.

Если приложение запущено без проблем, лог должен выглядеть примерно следующим образом:

Столь же важно отметить, что в случае несоответствия JPA модели и схемы базы данных Amplicode сообщил бы о проблеме и дал бы возможность решить ее прямо из stacktrace.

Заключение

Подводя итог, все поставленные в данном гайде задачи успешно выполнены примерно за 20 минут.

Вы научились:

  1. Подключать и настраивать Flyway в проекте, написанном на Spring Boot с применением Amplicode

  2. Расширять JPA модель и писать скрипты миграции БД

  3. Валидировать JPA модель и БД

Подписывайтесь на наши Telegram и YouTube, чтобы не пропустить новые материалы про Amplicode, Spring и связанные с ним технологии!

А если вы хотите попробовать Amplicode в действии — то можете установить его абсолютно бесплатно уже сейчас, как в IntelliJ IDEA/GigaIDE, так и в VS Code.

Комментарии (3)


  1. vasyakolobok77
    24.07.2024 16:50

    Понятно, что статья больше для рекламы продукта и пиара компании, но давайте делать примеры с качественным кодом.

    Версионирование миграций вида V1__ V2__ сразу же сломается, когда 2 разработчика начнут делать фичи параллельно. Для обхода этого есть подход использовать таймштампа в версии https://www.jeremyjarrell.com/using-flyway-db-with-distributed-version-control/

    Генерация портянки кода вида

    Class<?> oEffectiveClass = o instanceof HibernateProxy
                    ? ((HibernateProxy) o).getHibernateLazyInitializer()
                    .getPersistentClass()
                    : o.getClass();
    Class<?> thisEffectiveClass = this instanceof HibernateProxy
                    ? ((HibernateProxy) this).getHibernateLazyInitializer()
                    .getPersistentClass()
                    : this.getClass();
    if (thisEffectiveClass != oEffectiveClass) return false;
    Comment comment = (Comment) o;

    видимо пошла из помогаторов вида jpa buddy. Неужели не проще и более читаемо делать проверку на instanceof? У нас классы расширяются cglib-ом через наследование и прокси-класс будет наследником класса entity. Как по мне более читаемо:

     if (!(o instanceof Comment comment)) return false;

    Непонятно, зачем задано ограничение на строку комментария varchar(255)? Во-первых, почему именно 255? Чем так привлекло это число, почему не varchar(4000)? Во-вторых, для текста в Postgres есть почти безлимитный text.

    Еще непонятно, почему для даты используется timestamp without time zone, который по смыслу хранит локальное время LocalDateTime? Если у нас 1 клиент БД с одной TZ, то вроде бы и ок. Но если у нас появится клиент (приложение) в другой TZ, то время будет уже неверным.


    1. alexander-shustanov
      24.07.2024 16:50
      +3

      1. В Amplicode можно настроить именование скриптов версионирования, в том числе и ваш вариант.

      2. Действительно, такие equals hashcode пришли из JPA Buddy (https://jpa-buddy.com/blog/hopefully-the-final-article-about-equals-and-hashcode-for-jpa-entities-with-db-generated-ids/), ведь его делала наша команда)

      3. Ограничение 255 не случайно, это дефолт, который использует JPA. Можно конечно его проигнорировать, но лучше, когда модель и бд согласованы, правда? Тем не менее, можно поменять настройки маппинга JPA типов на database типы

      4. Это модельное приложение, для него LocalDateTime вполне достаточен)


      1. honest_niceman Автор
        24.07.2024 16:50
        +2

        @vasyakolobok77 спасибо за развёрнутый комментарий! 

        Полностью согласен с @alexander-shustanov, лишь дополню:

        1) Да, всё это дело настраиваемое и каждый может настроить под свой проект

        2) На вашем проекте instanceof подходит, на других не подойдёт. В целом, мы можем посмотреть в сторону того, чтобы дать возможность генерировать несколько реализацией. Но нужно провести анализ и убедиться, что действительно не будет никаких сайд эффектов в случае использования этого подхода. С той реализацией которую мы генерируем, проблем точно не будет, хоть она и может выглядеть как "портянка" :)

        По 3 и 4 пунктам добавить нечего.