Migração de banco de dados com Flyway no Spring Boot
1. Visão geral
Quando a aplicação muda, o schema do banco de dados também deve mudar. Se você executar manualmente cada comando ALTER TABLE em cada ambiente, mais cedo ou mais tarde dev, staging e production ficarão desalinhados. O Flyway resolve esse problema: cada mudança de schema é um arquivo de migration com versão, aplicado automaticamente e na ordem correta.
Neste artigo, você vai configurar o Flyway para uma aplicação Spring Boot 3, escrever migrations em SQL e Java, testá-los com JUnit 5, e aprender como lidar com erros comuns como checksum mismatch.
2. Instalando dependências
O Spring Boot já gerencia a versão do Flyway, então você não precisa especificá-la. O exemplo abaixo usa PostgreSQL:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
A partir do Flyway 10 (que acompanha o Spring Boot 3.3+), o suporte para cada tipo de banco de dados foi separado em módulos distintos. Se o flyway-database-postgresql estiver faltando, a aplicação não iniciará e reportará um erro:
org.flywaydb.core.api.FlywayException: Unsupported Database: PostgreSQL 16.4
H2 ainda está incluído no flyway-core, então o lado dos testes não precisa de módulos adicionais.
3. Configuração básica
Com apenas flyway-core no classpath, o Spring Boot criará automaticamente um bean Flyway e executará as migrations antes de EntityManagerFactory ser inicializado. Configuração mínima em application.properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/shop
spring.datasource.username=shop
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=validate
spring.flyway.locations=classpath:db/migration
spring.flyway.validate-on-migrate=true
As linhas importantes:
spring.jpa.hibernate.ddl-auto=validate: O Hibernate apenas verifica se as entidades correspondem ao schema. Ele não altera o schema automaticamente. Flyway é a única fonte de verdade para o schema.spring.flyway.locations: o diretório que contém as migrations. O valor padrão já éclasspath:db/migration. Aqui foi especificado para maior clareza.spring.flyway.validate-on-migrate=true: antes de migrar, o Flyway verifica o checksum das migrations já executadas. Este também é o valor padrão.
Nota: Não use
ddl-auto=updateoucreatejunto com o Flyway. Hibernate e Flyway alterarão o schema ao mesmo tempo, e o resultado será imprevisível.
4. Escrevendo migrations em SQL
4.1. Regras de nomenclatura
O Flyway identifica as migrations pelo nome do arquivo:
V1__create_customer_table.sql
│ │ └── descrição (o underscore é interpretado como espaço)
│ └── dois underscores separam
└── V = migration com versão, seguido pelo número da versão
A versão pode ser 1, 2, 1.1 ou 20261016.1. O Flyway compara versões por valor numérico, não alfabético. Portanto, V10 executa após V9.
4.2. A primeira migration
Crie o arquivo src/main/resources/db/migration/V1__create_customer_table.sql:
CREATE TABLE customer (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
full_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Em seguida, V2__create_orders_table.sql:
CREATE TABLE orders (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customer (id),
total NUMERIC(12, 2) NOT NULL,
status VARCHAR(20) NOT NULL
);
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
Ao iniciar a aplicação, o log terá a seguinte aparência:
Flyway Community Edition 10.x by Redgate
Database: jdbc:postgresql://localhost:5432/shop (PostgreSQL 16.4)
Successfully validated 2 migrations (execution time 00:00.012s)
Creating Schema History table "public"."flyway_schema_history" ...
Current version of schema "public": << Empty Schema >>
Migrating schema "public" to version "1 - create customer table"
Migrating schema "public" to version "2 - create orders table"
Successfully applied 2 migrations to schema "public", now at version v2
4.3. Tabela flyway_schema_history
O Flyway registra todas as migrations executadas na tabela flyway_schema_history. Você pode consultá-la diretamente:
SELECT installed_rank, version, description, checksum, success
FROM flyway_schema_history
ORDER BY installed_rank;
installed_rank | version | description | checksum | success
----------------+---------+-----------------------+-------------+---------
1 | 1 | create customer table | -1253721480 | t
2 | 2 | create orders table | 873416502 | t
Na próxima inicialização, o Flyway verá que o schema está na versão 2 e não executará nada novamente. A coluna checksum é a chave para a seção 7.
4.4. Migration repeatable
Arquivos que começam com R__ são migrations repeatable. Elas não possuem versão. O Flyway executa novamente esse arquivo sempre que o checksum muda, e sempre após todas as migrations com versão. Esse tipo é ideal para views, functions ou stored procedures:
-- R__order_summary_view.sql
CREATE OR REPLACE VIEW order_summary AS
SELECT c.id AS customer_id, c.full_name, COUNT(o.id) AS order_count
FROM customer c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.full_name;
5. Migrations em Java
Algumas mudanças são difíceis de escrever em SQL puro, como backfill de dados que precisam de lógica de processamento de strings. Nesses casos, você escreve a migration em Java, herdando de BaseJavaMigration.
Primeiro, adicione a coluna em SQL em V3__add_customer_email.sql:
ALTER TABLE customer ADD COLUMN email VARCHAR(150);
Depois, coloque a classe abaixo no pacote db.migration, em src/main/java/db/migration/V4__Backfill_customer_email.java:
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import java.text.Normalizer;
public class V4__Backfill_customer_email extends BaseJavaMigration {
@Override
public void migrate(Context context) {
var jdbc = new JdbcTemplate(
new SingleConnectionDataSource(context.getConnection(), true));
jdbc.query("SELECT id, full_name FROM customer WHERE email IS NULL", rs -> {
long id = rs.getLong("id");
String email = toSlug(rs.getString("full_name")) + "." + id + "@example.com";
jdbc.update("UPDATE customer SET email = ? WHERE id = ?", email, id);
});
}
private static String toSlug(String name) {
String noAccent = Normalizer.normalize(name, Normalizer.Form.NFD)
.replaceAll("\\p{M}", "")
.replace('đ', 'd').replace('Đ', 'D');
return noAccent.toLowerCase().trim().replaceAll("\\s+", ".");
}
}
Explicação:
- O nome da classe segue a mesma regra dos arquivos SQL:
V4__Backfill_customer_email. context.getConnection()retorna a conexão que o Flyway está usando, dentro da mesma transação da migration. O parâmetrotruedeSingleConnectionDataSourceimpede queJdbcTemplatefeche essa conexão.- O Flyway verifica o pacote
db.migrationporque ele corresponde aclasspath:db/migration.
Para um cliente chamado Nguyễn Văn An com id 1, o resultado seria nguyen.van.an.1@example.com.
O Spring Boot também registra automaticamente todos os beans do tipo JavaMigration no Flyway. Por isso, você pode declarar uma migration como @Component. Porém, você não deve injetar repositórios ou serviços na migration. O código de negócio muda com o tempo, enquanto as migrations devem ser executadas da mesma forma para sempre.
6. Testando migrations
Você deve ter pelo menos um teste que confirme que todas as migrations podem ser executadas a partir de um schema vazio e que o Hibernate valida com sucesso. Crie src/test/resources/application-test.properties:
spring.datasource.url=jdbc:h2:mem:shop;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=
Teste:
import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ActiveProfiles("test")
class FlywayMigrationTest {
@Autowired
private Flyway flyway;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void whenContextStarts_thenAllMigrationsAreApplied() {
var current = flyway.info().current();
assertThat(current).isNotNull();
assertThat(current.getVersion().getVersion()).isEqualTo("4");
assertThat(flyway.info().pending()).isEmpty();
}
@Test
void givenNewCustomer_whenInsert_thenEmailColumnExists() {
jdbcTemplate.update(
"INSERT INTO customer (full_name, email) VALUES (?, ?)",
"Trần Thị Bình", "binh@example.com");
String email = jdbcTemplate.queryForObject(
"SELECT email FROM customer WHERE full_name = ?",
String.class, "Trần Thị Bình");
assertThat(email).isEqualTo("binh@example.com");
}
}
O primeiro teste verifica que o schema está na versão 4 e não há nenhuma migration aguardando execução. Se um arquivo SQL tiver um erro de sintaxe, o contexto não iniciará e o teste falhará imediatamente. O segundo teste confirma que a coluna email da V3 realmente existe.
Nota: O H2 em
MODE=PostgreSQLapenas emula parte da sintaxe do PostgreSQL. Se as migrations usarem recursos específicos comoJSONB, índices parciais ouCREATE INDEX CONCURRENTLY, você deve executar os testes no PostgreSQL real usando o Testcontainers.
7. Erros comuns e como tratá-los
7.1. Modificando migrations já executadas
Este é o erro mais comum. Suponha que você modifique V2__create_orders_table.sql depois que ele já foi executado no staging. Na próxima inicialização, você verá:
Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 2
-> Applied to database : 873416502
-> Resolved locally : -402981177
A regra é: nunca modifique uma migration que já foi executada em qualquer ambiente compartilhado. Se você precisa fazer uma mudança, crie uma nova migration, por exemplo V5__alter_orders_status_length.sql.
Se a mudança for apenas um comentário ou espaçamento em branco e você tiver certeza de que o schema não mudou, você pode atualizar o checksum usando o comando repair. Por exemplo, com o plugin Maven do Flyway:
mvn flyway:repair -Dflyway.url=jdbc:postgresql://localhost:5432/shop \
-Dflyway.user=shop -Dflyway.password=secret
repair também remove registros de migrations falhadas da flyway_schema_history. Isso é importante com MySQL, pois MySQL não pode fazer rollback de DDL dentro de uma transação.
7.2. Versões duplicadas ao trabalhar em equipe
Se dois desenvolvedores criarem V5__... em duas branches diferentes, após o merge verão o erro Found more than one migration with version 5. Existem duas maneiras de evitar isso:
- Use versões com timestamp, por exemplo
V20261016_0930__add_order_note.sql. - Coordene as versões “reservando” números antes de criar os arquivos.
Há outro caso: a branch A faz merge de V7 primeiro, depois a branch B faz merge de V6. O Flyway reportará que V6 foi ignorado. Você pode ativar spring.flyway.out-of-order=true, mas é melhor renomear o arquivo para ter uma versão maior que a versão atual.
7.3. Aplicando Flyway a um banco de dados existente
Se o banco de dados de produção já possui tabelas mas não tem flyway_schema_history, o Flyway se recusará a executar porque o schema não está vazio. Você usa o baseline:
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1
O Flyway marca o schema atual como versão 1. Depois, ele apenas executa migrations com versão maior que 1. Neste caso, V1 deve ser um dump do schema atual, para que novos ambientes ainda possam ser construídos do zero.
Nota: Ative
baseline-on-migrateapenas durante a primeira migração. Se deixar ativado por muito tempo e acidentalmente apontar para um banco de dados que já tem tabelas, o Flyway fará o baseline automaticamente em vez de reportar um erro.
7.4. O comando clean
flyway clean exclui todos os objetos no schema. O Spring Boot 3 define spring.flyway.clean-disabled=true por padrão. Não desative essa flag em qualquer perfil que possa acessar a produção.
7.5. Migrations que executam lentamente
O Spring Boot executa as migrations no início. Um comando ALTER TABLE em uma tabela com dezenas de milhões de linhas pode bloquear a tabela e fazer o health check falhar. Neste caso, você deve:
- Dividir a mudança em várias etapas: adicionar coluna nullable, fazer backfill em lotes e depois adicionar
NOT NULL. - Desativar auto-migrate com
spring.flyway.enabled=falsee executar o Flyway como uma etapa separada no pipeline de deploy, antes do rollout da aplicação.
8. Quando não usar o Flyway do modo padrão
O Flyway é muito adequado para a maioria das aplicações Spring Boot. Porém, a abordagem “migrar ao iniciar” tem limitações:
- Executar múltiplas instâncias simultaneamente: o Flyway usa lock no banco de dados, então não executa migrations duplicadas. Mas as outras instâncias têm que esperar, e o tempo de inicialização aumenta.
- Conta da aplicação não tem permissão DDL: este é um requisito de segurança comum em produção. Nesse caso, você executa o Flyway com uma conta separada no pipeline.
- Precisa de rollback automático: a edição Community não possui undo de migrations. Você deve escrever migrations “para frente” para desfazer mudanças.
9. Conclusão
Neste artigo, você integrou o Flyway ao Spring Boot 3. Você aprendeu a escrever migrations com versão, migrations repeatable e migrations em Java, testá-los com JUnit 5 e AssertJ, e como lidar com erros comuns como checksum mismatch, versões duplicadas ou baseline de bancos de dados antigos.
Há três princípios que você deve lembrar:
- O Flyway é o único lugar onde o schema é alterado, enquanto o Hibernate apenas faz
validate. - Nunca modifique uma migration que já foi executada.
- Sempre tenha um teste que execute todas as migrations a partir de um schema vazio.