Trilha de Kotlin · parte 50 de 59
Migrations com Liquibase
Aula da fase 7 da trilha de Kotlin: Como versionar o schema com changesets, por que um changeset já aplicado nunca deve ser editado e como o `contexts` separa dados de teste?
intermediárioPublicada em · 13 min de leitura
Imagine um caderno de obra em que cada etapa é assinada e carimbada com a data. Ninguém reescreve uma etapa já assinada, porque a obra inteira depende dela: se alguém trocar o material de uma parede que já foi erguida, quem chega depois não sabe o que de fato está lá. O Liquibase trata o schema do banco de forma parecida. Cada mudança é um changeset com um identificador, e o Liquibase guarda na tabela DATABASECHANGELOG o registro de quais changesets já rodaram, junto com uma impressão digital do conteúdo de cada um.
Esta aula mostra como essa história funciona na prática: como o Liquibase decide o que rodar, por que um changeset já aplicado não pode ser editado e como o atributo context separa dados de teste dos de produção. A aula usa o PostgreSQL com o Spring Boot iniciando o Liquibase na subida do contexto.
o mapa da aula
A primeira parte explica o changeset e o que o identifica. A segunda mostra o caminho de decisão do Liquibase a cada inicialização, com o checksum e a tabela de controle. A terceira parte trata dos contextos, com três execuções reais. Depois vêm a edição indevida de um changeset já aplicado, as boas práticas e o que não foi verificado.
Versões desta aula: Spring Boot 4.1.1, com Spring Framework 7.0.9, Liquibase 5.0.3 (liquibase-core), o módulo spring-boot-liquibase do próprio Boot e PostgreSQL 17.11 em container. O Kotlin é o 2.3.21 do BOM, como nas aulas anteriores da fase. Esta aula não usa Flyway, jOOQ, Exposed nem R2DBC.
1. changeset, o caminho do changelog e a identidade
O arquivo que lista as mudanças se chama changelog. Dentro dele, cada mudança é um changeSet. A documentação do Liquibase define o changeset como “the basic unit of change in Liquibase”, e diz como ele é identificado: “A changeset is uniquely tagged by both the author and id attributes”, além do caminho do arquivo do changelog. Ou seja, a identidade de um changeset é a combinação de id, author e arquivo. Trocar qualquer um dos três cria, para o Liquibase, um changeset novo, mesmo que o conteúdo seja idêntico.
Para o Spring Boot, o Liquibase procura por padrão o arquivo classpath:/db/changelog/db.changelog-master.yaml, e o nome da propriedade para apontar outro arquivo é spring.liquibase.change-log. Os valores padrão foram conferidos lendo a classe LiquibaseProperties do próprio jar do Boot 4.1.1, com um programa de verificação. A página oficial de integração do Liquibase com Spring Boot descreve outro caminho padrão, um XML em src/main/resources/db/changelog, e um starter comercial, liquibase-secure-starter-all. Por isso a referência desta aula é o valor que o próprio Boot 4.1.1 usa, que é o que este projeto roda:
liquibase changeLog=classpath:/db/changelog/db.changelog-master.yaml contexts=null
O experimento desta aula aponta para classpath:aula64/changelog.yaml, com os dois changesets num único arquivo. O conteúdo está no código da seção de fontes.
Há um detalhe de classpath que custou uma tentativa na execução. Se o mesmo arquivo aparece em duas pastas do classpath, o Liquibase acusa o erro Found 2 files with the path 'classpath:aula64/changelog.yaml', e o contexto do Spring não sobe. A aula contornou isso removendo a cópia gerada em target/classes. Em um projeto em que o changelog existe só em src/main/resources, esse conflito não aparece, mas ele mostra que o Liquibase lê o arquivo pelo caminho dentro do classpath.
2. o que o Liquibase decide a cada inicialização
Quando o contexto do Spring sobe, o Liquibase percorre os changesets do changelog e decide, para cada um, se executa, pula ou falha. A decisão depende de duas coisas: se o changeset já está registrado na tabela DATABASECHANGELOG, e se o checksum gravado bate com o checksum atual do changeset. A documentação diz que o Liquibase faz essa comparação: “Liquibase compares the two checksums in order to check whether it has been modified since it was run.” Ele não calcula o checksum do arquivo inteiro. O checksum é do changeset em si, e por isso uma mudança em outro changeset não afeta o primeiro.
A figura mostra o caminho de decisão. Antes de qualquer checksum, o Liquibase pergunta se o changeset já está registrado. Se está, a comparação de checksum decide entre ignorar e falhar. Se não está, a pergunta é se o contexto do comando aceita o changeset, e a resposta decide entre executar e pular.
Os dois caminhos de falha são diferentes e merecem atenção. O caminho de checksum é o mais perigoso, porque o changeset já rodou no banco e alguém mudou o arquivo depois. O Liquibase para e mostra o erro. O caminho de contexto pula o changeset sem registrar nada, o que significa que ele pode rodar depois, em outro comando.
O experimento de checksum usou o mesmo changelog, com um único atributo alterado no changeset 1, varchar(100) virando varchar(120). Com contexts=prod, o resultado foi:
ValidationFailedException
Validation Failed:
1 changesets check sum
A mensagem não diz qual campo mudou. A documentação registra exatamente essa limitação: “Liquibase cannot identify what was changed.” Quem recebe o erro precisa comparar o arquivo com o que foi executado, ou procurar o md5sum gravado na tabela.
Nem toda edição no arquivo gera esse erro. A página de checksums diz que o arquivo pode ser reformatado sem que o checksum mude: “you may modify the file (such as to make formatting fixes) and still get the same checksum.” Para conferir, a aula trocou só espaços e a indentação de um changeset e rodou de novo com contexts=prod. A subida passou, e o md5sum continuou 9:7b7cb8e4cf1e10e889e9d382f15c20fd. A diferença entre uma reformatação e uma mudança de conteúdo é o que o checksum mede: a instrução depois de ser interpretada, e não o texto bruto do arquivo.
3. contextos: o mesmo changelog em três comandos
O atributo context restringe quando um changeset roda. A documentação resume a função dele assim: “You can specify Preconditions, Contexts, Labels, and other attributes in individual changesets to control exactly when they run.” No changelog desta aula, o changeset 2 tem context: test e insere uma linha de teste em cliente. O changeset 1 não tem contexto.
O comando de execução define quais contextos estão ativos. Foram três execuções reais, com o banco zerado antes de cada uma, e a tabela foi consultada depois. Em cada execução o Spring Boot recebeu a propriedade spring.liquibase.contexts com o valor do cenário.
Com contexts=prod, só o changeset 1 foi aplicado. A tabela ficou assim:
id | author | contexts | md5sum
----+--------+----------+------------------------------------
1 | jean | | 9:7b7cb8e4cf1e10e889e9d382f15c20fd
O changeset 2 não aparece na tabela. Ele foi pulado sem registro, e isso importa, porque um changeset pulado continua pendente e pode ser aplicado depois. Em seguida, com contexts=test, o mesmo banco recebeu o changeset 2, e a tabela passou a ter as duas linhas, com test na coluna contexts. O changeset 1 não rodou de novo, porque já estava registrado:
id | author | contexts | md5sum
----+--------+----------+------------------------------------
1 | jean | | 9:7b7cb8e4cf1e10e889e9d382f15c20fd
2 | jean | test | 9:f1d21b3e34796f9383a40f055034bb96
Sem nenhum contexto no comando, os dois changesets rodaram no banco zerado. A aula registra esse resultado como observado: quando o comando não informa contextos, nenhum changeset é filtrado. A figura reúne os três comandos.
Essa é a forma de separar dados de teste dos de produção sem manter dois changelogs. O risco é colocar um dado de teste em um changeset sem contexto, porque ele rodará em produção no primeiro comando que não filtra nada. A regra prática é marcar explicitamente todo changeset de dados com um contexto e fazer o comando de produção sempre informar o seu.
4. editar um changeset já aplicado
Este é o erro que o currículo chama de principal: alterar um changeset que já rodou em um ambiente compartilhado. O experimento fez isso de propósito. O banco tinha o changeset 1 registrado com o checksum original, e o arquivo foi editado para varchar(120). Com o comando contexts=prod, o Liquibase recusou a subida do contexto, e a tabela continuou com o checksum antigo. O erro aparece na inicialização do Spring, e o EntityManagerFactory depende do Liquibase, então a aplicação não sobe.
Há um caminho tentador para contornar o erro: editar o registro na tabela DATABASECHANGELOG à mão, ou rodar liquibase clear-checksums. Cada uma dessas saídas faz o Liquibase aceitar a mudança sem que ninguém tenha verificado se o banco ainda tem o schema esperado. A página de checksums descreve as duas: a edição manual precisa ser feita “for all environments where the changeset has been deployed”, e o clear-checksums “will clear the entire MD5SUM column of checksums in your DATABASECHANGELOG table”. A aula não testou a tag validCheckSum, por isso não a descreve aqui. A saída certa costuma ser outra: um changeset novo, com um id diferente, que faz a alteração desejada.
Esse é o motivo da regra. A tabela DATABASECHANGELOG é o registro do que de fato rodou em cada banco. Se o arquivo puder ser reescrito, o registro deixa de descrever o schema real, e a próxima pessoa a criar um ambiente a partir do changelog obtém um banco diferente do que está em produção.
5. erros comuns e boas práticas
O primeiro erro é editar um changeset antigo porque ele “ainda não foi para produção”. Às vezes ele já foi para o homologação, e a tabela de controle de homologação tem o checksum antigo. A regra é só editar um changeset que nunca rodou em lugar nenhum, o que é difícil de garantir em equipe.
O segundo erro é reutilizar um id com um author diferente, achando que o Liquibase vai encaixar a mudança no lugar certo. Como a identidade inclui o autor e o arquivo, o Liquibase trata o resultado como um changeset novo, que roda de novo no banco que já tinha o antigo.
O terceiro erro é omitir o contexto em um changeset de dados. A consequência aparece só quando o comando de produção não informa contextos, ou informa um que não existe no changeset, e o dado de teste entra em produção sem que ninguém tenha pedido.
O quarto erro é tratar a falha de checksum como ruído e forçar a subida do serviço. A falha existe para impedir que o código e o banco contem histórias diferentes. Forçar a subida transfere o problema para a primeira consulta que depender da coluna alterada.
A boa prática, em resumo, é escrever cada mudança como um changeset novo, com id único e author identificado, rodar o changelog em um banco limpo no pipeline de testes, marcar os dados de teste com contexto e exigir que cada comando de produção informe os seus contextos. A aula Spring Data MongoDB trata do lado MongoDB, que não tem Liquibase, mas tem o mesmo problema de evolução de dados.
6. exercícios
- Adicione um terceiro changeset que cria a tabela
pedido, comcontext: prod. Rode comcontexts=teste depois comcontexts=prod, e anote o que aparece na tabelaDATABASECHANGELOGem cada etapa. - Mude apenas o
authordo changeset 1 e rode o contexto de novo. Explique por que o Liquibase trata a mudança como um changeset novo, e o que acontece com a tabela. - Reformate um changeset com outra indentação, como fez a seção 2, e confira que o
md5sumnão muda. Depois altere o conteúdo de verdade, por exemplo o tipo de uma coluna, e confirme que a subida falha. Explique a diferença entre as duas mudanças com base no que a página de checksums diz. - Escreva em um arquivo de texto o procedimento que sua equipe usaria para corrigir um changeset já aplicado em produção. A resposta precisa dizer quem aprova, em que ordem os ambientes são atualizados e como o registro na tabela é preservado.
7. glossário
Changelog: arquivo que lista os changesets na ordem em que devem ser aplicados.
Changeset: unidade de mudança do Liquibase, identificada por id, author e caminho do arquivo.
DATABASECHANGELOG: tabela em que o Liquibase registra os changesets já executados, com o checksum de cada um.
Checksum: impressão digital do changeset, calculada pelo Liquibase para detectar edições depois da aplicação.
Contexto: rótulo que filtra quando um changeset roda, conforme os contextos ativos no comando.
Validation failed: erro que o Liquibase lança quando o checksum atual de um changeset já aplicado não bate com o gravado.
fontes
Consultadas em 20/10/2026:
-
Liquibase, conceito de changeset, com a definição e a identidade por
author,ide caminho do arquivo: https://docs.liquibase.com/concepts/changelogs/changeset.html -
Liquibase, checksums de changeset, com a comparação de checksums, a mensagem de validação, o aviso sobre
clear-checksumse a limpeza manual do campo: https://docs.liquibase.com/concepts/changelogs/changeset-checksums.html -
Liquibase, a mesma página de changeset, com o trecho sobre preconditions, contexts e labels.
-
Liquibase, integração com Spring Boot: https://docs.liquibase.com/tools-integrations/springboot/springboot.html. Essa página descreve o starter
liquibase-secure-starter-alle um caminho padrão em XML, diferente do que o Spring Boot 4.1.1 usa neste projeto. Por isso o nome da propriedade e os valores padrão vieram da classeLiquibasePropertiesdo próprio jar. -
Spring Boot 4.1.1, Liquibase: a classe
LiquibaseProperties(módulospring-boot-liquibase) é a referência do caminho padrão e decontexts, lida em tempo de execução.
Consultadas em 20/10/2026. As citações do Liquibase foram lidas com WebFetch nas URLs acima.
Verificações locais, feitas em 20/10/2026:
LiquibasePropertiesdospring-boot-liquibase4.1.1 foi instanciado sem argumentos, e os valores padrão dechangeLogecontextsforam lidos.- Os três cenários de contexto foram executados contra um PostgreSQL 17.11 com o Spring Boot 4.1.1 e o Liquibase 5.0.3. A tabela
DATABASECHANGELOGfoi consultada após cada execução, e omd5sumaparece como foi gravado. - O cenário de edição de changeset aplicado foi executado com a mudança do
varchar(100)paravarchar(120). A mensagem de erro é a que o Liquibase imprimiu. - A mensagem “Found 2 files” foi reproduzida com o mesmo arquivo em
target/classese emsrc/main/resourcesno classpath. - A reformatação com espaços foi executada sobre o banco já aplicado, e o
md5sumficou igual.
Código do experimento (arquivos aula64/Main.kt e aula64/changelog.yaml, este último em src/main/resources):
package aula64
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
// O Liquibase roda na inicializacao. As propriedades (change-log, contexts, datasource)
// entram por -D na linha de comando, para o mesmo jar servir a cada cenario.
@SpringBootApplication
class AulaLiquibaseApplication
fun main() {
runApplication<AulaLiquibaseApplication>().close()
}
databaseChangeLog:
- changeSet:
id: 1
author: jean
changes:
- createTable:
tableName: cliente
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
- column:
name: nome
type: varchar(100)
- changeSet:
id: 2
author: jean
context: test
changes:
- insert:
tableName: cliente
columns:
- column:
name: nome
value: Cliente de teste
Não verificado nesta aula: o comportamento com labels, preconditions e rollback, a tag validCheckSum, e o procedimento real de correção em produção, que depende da infraestrutura de cada equipe.
relacionados
- Trilha de Kotlin
- Curriculo de um Desenvolvedor Kotlin
- Starters e configuração por ambiente
- JPA, Hibernate e N+1
- Database Migrations com Goose
- Spring Data MongoDB: documentos e modelagem