leanmind logo leanmind text logo

Blog

BDD

Behaviour-driven Development es una técnica para tomar mejores requisitos de producto.

Migrando bases de datos con Liquibase

Por José Luis Rodríguez Alonso

Cuando trabajas en un proyecto desde 0, siempre suele surgir la necesidad de guardar los cambios en el esquema de tu base datos como migraciones, de forma que, cuando la aplicación esté en producción, no tengas que ejecutar SQL a mano antes de publicar una nueva versión.

De las opciones que existen para Java, he probado Flyway y Liquibase . Las dos funcionan de una manera similar, crean una tabla en tu base de datos. donde almacenan el histórico de cambios y se encargan de ejecutar las migraciones que no aparezcan en ella.

Entre Flyway y Liquibase, me quedo con Liquibase por algunas características que lo hacen más versátil a la larga. Una de ellas es que nos permite escribir nuestras migraciones en formatos independientes de la base de datos, de forma que si por el motivo que sea decidimos cambiar de una base de datos a otra, solo es necesario cambiar el driver de conexión.

Configuración

Como Spring Boot tiene soporte para Liquibase, lo único que debemos hacer es añadir la dependencia a nuestro pom.xml e indicar la ruta donde almacenamos las migraciones:

1
2
3
4
<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
</dependency>

Nota: En este caso no es necesario indicar la versión, porque Spring Boot ya lo hace por nosotros, gracias al spring-boot-starter-parent

Ahora en el archivo de configuración application.yaml indicamos la ruta de las migraciones:

1
2
3
spring:
  liquibase:
    change-log: classpath:database/liquibase-changelog.xml

Por defecto, Spring Boot configura Liquibase con el archivo db/changelog/db.changelog-master.yaml, pero personalmente prefiero hacerlo en XML, porque el autocompletado en IntelliJ permite crearlos pulsando apenas un par de teclas.

Migraciones

Con Liquibase, podemos escribir nuestras migraciones en varios formatos, sql, yaml, json, xml… Como dije antes, prefiero usar el xml, porque aunque es mucho más verboso, el autocompletado en IntelliJ es una maravilla.

El archivo liquibase-changelog.xml que definimos antes, contiene los ficheros que contienen nuestras migraciones. Tiene este formato:

    <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                       xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                                           http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
    
        <include file="changelog/changelog-v0.0.1.xml" relativeToChangelogFile="true"/>
    
    </databaseChangeLog>

Aunque no es estrictamente necesario, yo prefiero organizar las migraciones en distintos ficheros, uno para cada versión de nuestra aplicación. Al principio, puede parecer innecesario, pero a la larga tener esto organizado es una ventaja.

Los archivos de las migraciones tienen esta forma:

    <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                       xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                                           http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
    
        <changeSet id="create-products-table" author="jrodalo">
            <createTable tableName="products">
                <column name="id" type="integer">
                    <constraints primaryKey="true"/>
                </column>
                <column name="name" type="varchar(255)"/>
            </createTable>
        </changeSet>
    
    </databaseChangeLog>

Podemos añadir tantos changesets como necesitemos y cada changeset puede contener los cambios que quieras. En el ejemplo anterior, estaríamos creando una tabla products con un par de campos.

Ejecutando las migraciones

Las migraciones se ejecutan automáticamente al arrancar la aplicación. Spring Boot se encarga de ejecutar Liquibase y este se encarga de revisar el estado de las migraciones. Si existen cambios, los ejecutará por nosotros.

De esta forma, si necesitamos añadir una nueva tabla o campo a nuestra base de datos, solo debemos añadir la migración y desplegar los cambios.

Añadiendo Testcontainers al potaje

Para probar que esto funciona, vamos a tirar de TestContainers. En nuestro archivo de configuración local, añadimos la configuración para PostgreSQL:

1
2
3
 spring:
datasource:
url: jdbc:tc:postgresql:12.2:///test driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver 

Nota: driver-class-name solo es necesario para versiones de Spring Boot anteriores a la 2.3.0

Y la dependencia en nuestro pom.xml:

1
2
3
4
5
6
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <version>1.13.0</version>
        <scope>test</scope>
    </dependency>

Lo siguiente sería crear un test de integración con @SpringBootTest e intentar acceder a nuestra nueva tabla, algo como esto valdría:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 @SpringBootTest @ActiveProfiles("local")
class LiquibaseTest {

    private final NamedParameterJdbcTemplate jdbcTemplate;

    @Autowired
    LiquibaseApplicationTests(NamedParameterJdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Test
    void createsDatabaseTablesUsingMigrations() {
        var product = Product.builder()
                .id(1L)
                .name("Test Product")
                .build();

        insert(product);

        assertEquals(product, selectProduct(1L));
    }

    [...]

} 

Si ejecutamos el test y revisamos la consola, veremos que Spring Boot inicia la aplicación y, después de que Testcontainers arranque nuestra base de datos, Liquibase se encarga de crear nuestro esquema.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 INFO --- [main]
org.testcontainers.DockerClientFactory   : Checking the system... INFO --- [main]
🐳 [postgres:12.2]                       : Creating container for image: postgres:12.2 INFO --- [main]
🐳 [postgres:12.2]                       : Starting container with ID: 968e249bb73f1b82ed01c6ba INFO --- [main]
🐳 [postgres:12.2]                       : Container postgres:12.2 is starting: 968e249bb73f1b82ed01c6ba INFO --- [main]
🐳 [postgres:12.2]                       : Container postgres:12.2 started in PT4.032473S INFO --- [main]
l.c.StandardChangeLogHistoryService      : Creating database history table with name: public.databasechangelog INFO
--- [main] liquibase.executor.jvm.JdbcExecutor      : CREATE TABLE public.products (id INTEGER NOT NULL, name VARCHAR(
255), CONSTRAINT PRODUCTS_PKEY PRIMARY KEY (id))
INFO --- [main] liquibase.changelog.ChangeSet            : Table products created 

Cambiando de base de datos

Aunque es poco probable que en un proyecto se decida cambiar la base de datos, es posible que necesitemos realizar una app que el cliente pueda personalizar y desplegar en sus propios servidores. Con Liquibase, eso lo podemos hacer simplemente cambiando el driver de conexión en nuestro pom.xml. Si cambiamos el driver de PostreSQL por el de MariaDB, Liquibase se encargará de traducir nuestras migraciones al SQL específico para esa base de datos.

Publicado el 18/08/2021 por
José Luis image

José Luis Rodríguez Alonso

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers