Y se llama Liquibase!


Hey vos!

Si, vos!

  • Querés tener control sobre los esquemas de tu DB?
  • Querés un historial de los cambios aplicados?
  • Y también accountability?
  • Y por qué no también la habilidad de testear facilmente nuevos cambios?
  • Y hacer rollbacks?

Entonces Liquibase es lo que necesitás.

Qué es LQB?

LiquiBase es un proyecto Open Source creado en 20061. Permite a los usuarios planificar, desarrollar y almacenar cambios a la DB en archivos de texto, permitiendo así mantener y documentar fácilmente “qué pasa cuando”. LQB soporta muchas bases de datos.

Estos archivos de texto que mencionamos se llaman ChangeLogs. Y ya que es lindo tener algún tipo de control de versiones, sería ideal almacenar estos archivos en un source control.

Los ChangeLogs contienen ChangeSets. Cada ChangeSet contiene una cantidad de Statements, que al ser aplicados a la base de datos, hacen cosas como:

  • Crear una tabla
  • Crear una vista
  • Crear una clave primaria
  • Configurar alguna constraint en una columna
  • Dropear un índice

Y más.

Lo lindo es su simpleza, y a pesar de que tiene muchas otras funciones por ahora quedémonos con lo básico.

Empecemos por crear una tabla.

Mirá mamá! Sin SQL!

Crear una tabla es algo simple.

Primero, creamos un archivo. Pongamosle de nombre lqb-changelog-1.xml.

Los archivos se puede escribir en 4 lenguajes distintos: XML, YAML, JSON y por su puesto SQL. Voy a usar XML ahora mismo, pero pueden ver las otras versiones en el repo de Gitlab (cuando las suba).

Los changelogs XML se definen con el tag databaseChangeLog. En la documentación, el tag se define de esta manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?xml version="1.0" encoding="UTF-8"?>  

<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"  
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd
    http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">

</databaseChangeLog>

Feo? Si. Pero es un copy-paste, así que no es un gran problema.

Luego, definamos un changeset vacío. Necesita tener un ID y un autor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>  

<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"  
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd
    http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">    
    
    <changeSet id="1" author="manucastro">

    </changeSet>
</databaseChangeLog>

Dentro del changeset, definimos una tabla. Pongamosle de nombre lqb_test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>  

<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"  
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd
    http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
    
    <changeSet id="1" author="manucastro">
        <createTable tableName="lqb_test">
            
        </createTable>
    </changeSet>
</databaseChangeLog>

Pero ya que las tablas sin columnas son inútiles, agreguemos 2: id y value

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>  

<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"  
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd
    http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">    
    
    <changeSet id="1" author="manucastro">
        <createTable tableName="lqb_test">
            <column name="id" type="integer">
            
            </column>
            <column name="value" type="varchar(32)">
            
            </column>
        </createTable>
    </changeSet>
</databaseChangeLog>

Liquibase también nos permite agregar constraints. Definamos la constraint “NOT NULL” en cada columna.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>  

<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"  
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd
    http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">

    <changeSet id="1" author="manucastro">
        <createTable tableName="lqb_test">
            <column name="id" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="value" type="varchar(32)">
                <constraints nullable="false" />
            </column>
        </createTable>
    </changeSet>
</databaseChangeLog>

Esto acá es un ejemplo básico de lo que se puede definir un changeset, lean la documntación para ver más cositas.

Docker images

Ahora podemos hacer algunos tests básicos. Los vamos a correr sobre containers de Docker2. Me gusta postgres así que voy a usarlo, pero siéntanse libres de usar la DB que más les guste. A LQB no le importa.3

Y además obviamente necesitamos Liquibase. Voy a usar su imagen de docker también

Para resumir, voy a usar las siguientes imágenes.

  • postgres:12.1-alpine
  • liquibase/liquibase

Configurando la DB

En tu terminal favorita, arrancá la db:

1
~# docker run --rm --net host --name db -d postgres:12.1-alpine

Podés, por ejemplo, ver si la DB esta funcionando con alguna query.

1
2
3
4
5
~# docker exec --user postgres -i db psql -c "select now();"
              now              
-------------------------------
 2020-05-23 23:45:59.784746+00
(1 row)

Creemos una DB, testdb.

1
~# docker exec --user postgres -i db createdb -T template0 testdb

(Y un usuario, solamente porque sí)

1
~# docker exec --user postgres -i db psql -c "create user testuser with superuser password 'testuser';"

Teniendo una base de datos usable, ahora probemos Liquibase.

Usando Liquibase

Nececesitamos especificar algunos parámetros obligatorios:

  • –changeLogFile=<path and filename> - Archivo de la migración
  • –username=<value> - Username
  • –password=<value> - Password
  • –url=<value> - URL de la DB

En este caso vamos a montar los changelogs en /liquibase/changelog, por lo que los argumentos serían una cosa así:

1
2
3
4
--changeLogFile /liquibase/changelog/lqb-changelog-1.xml
--username testuser
--password testuser
--url "jdbc:postgresql://localhost:5432/testdb"

Ya con todo configurado, podemos iniciar el container de LQB.

Comandos

Vamos a probar 3 comandos:

  • validate: verifica si hay errores en el changelog.
  • updateSQL: imprime el SQL que va a ejecutar en la DB.
  • update: aplica los cambios en la DB.

Notas:

  • Vamos a correr ambos containers en la red host
  • Montamos el contenido de la carpeta que contiene los changelogs en /liquibase/changelog
  • Le decimos a liquibase que use los changelogs en ese path
  • Y los demás parámetros, ya vimos que hacen.

Validate

1
2
3
4
~# docker run --rm --net host -v $PWD:/liquibase/changelog --name lqb -i liquibase/liquibase --changeLogFile /liquibase/changelog/lqb-changelog-1.xml --username testuser --password testuser --url "jdbc:postgresql://localhost:5432/testdb" validate
Liquibase Community 3.8.9 by Datical
No validation errors found.
Liquibase command 'validate' was executed successfully.

Valida bien! Esto significa que el changelog no tiene errores.


UpdateSQL

Verifiquemos que va a hacer Liquibase en nuestra preciada DB de producción.

 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
26
27
28
29
30
31
32
~# docker run --rm --net host -v $PWD:/liquibase/changelog --name lqb -i liquibase/liquibase --changeLogFile /liquibase/changelog/lqb-changelog-1.xml --username testuser --password testuser --url "jdbc:postgresql://localhost:5432/testdb" updateSQL
Liquibase Community 3.8.9 by Datical
-- *********************************************************************
-- Update Database Script
-- *********************************************************************
-- Change Log: /liquibase/changelog/lqb-changelog-1.xml
-- Ran at: 5/24/20, 3:07 AM
-- Against: testuser@jdbc:postgresql://localhost:5432/testdb
-- Liquibase version: 3.8.9
-- *********************************************************************

-- Create Database Lock Table
CREATE TABLE public.databasechangeloglock (ID INTEGER NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP WITHOUT TIME ZONE, LOCKEDBY VARCHAR(255), CONSTRAINT DATABASECHANGELOGLOCK_PKEY PRIMARY KEY (ID));

-- Initialize Database Lock Table
DELETE FROM public.databasechangeloglock;

INSERT INTO public.databasechangeloglock (ID, LOCKED) VALUES (1, FALSE);

-- Lock Database
UPDATE public.databasechangeloglock SET LOCKED = TRUE, LOCKEDBY = '192.168.0.13 (192.168.0.13)', LOCKGRANTED = '2020-05-24 03:07:34.454' WHERE ID = 1 AND LOCKED = FALSE;

-- Create Database Change Log Table
CREATE TABLE public.databasechangelog (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED TIMESTAMP WITHOUT TIME ZONE NOT NULL, ORDEREXECUTED INTEGER NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35), DESCRIPTION VARCHAR(255), COMMENTS VARCHAR(255), TAG VARCHAR(255), LIQUIBASE VARCHAR(20), CONTEXTS VARCHAR(255), LABELS VARCHAR(255), DEPLOYMENT_ID VARCHAR(10));

-- Changeset /liquibase/changelog/lqb-changelog-1.xml::1::manucastro
CREATE TABLE public.lqb_test (id INTEGER NOT NULL, value VARCHAR(32) NOT NULL);

INSERT INTO public.databasechangelog (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, DESCRIPTION, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('1', 'manucastro', '/liquibase/changelog/lqb-changelog-1.xml', NOW(), 1, '8:71bf69f8a204a5959d96fbb8bf870354', 'createTable tableName=lqb_test', '', 'EXECUTED', NULL, NULL, '3.8.9', '0289656372');

-- Release Database Lock
UPDATE public.databasechangeloglock SET LOCKED = FALSE, LOCKEDBY = NULL, LOCKGRANTED = NULL WHERE ID = 1;

Peraaa un minuto Manu, qué es toda esa basura?

Muy brevemente, liquibase:

  • Crea la tabla
  • Y hace Tareas mágicas

La tabla DatabaseChangeLog y DatabaseChangeLogLock son la manera que tiene LQB para trackear la version de cada tabla de la db, y que cambios se aplicaron en cada una. Pueden leer la docu al respecto acá

Ahora que ya calmamos la paranoia y sabemos que no hay ningun DROP DATABASE escondido por ningún lugar, podemos proceder y actualizar la DB.


Update

1
2
3
~# docker run --rm --net host -v $PWD:/liquibase/changelog --name lqb -i liquibase/liquibase --changeLogFile /liquibase/changelog/lqb-changelog-1.xml --username testuser --password testuser --url "jdbc:postgresql://localhost:5432/testdb" update
Liquibase Community 3.8.9 by Datical
Liquibase: Update has been successful.

El comando funciona bién, y podemos ver que ahora hay una tabla lqb_test, con 2 columnas, que no pueden ser NULL.

1
2
3
4
5
6
~# docker exec --user postgres -i db psql testdb testuser -c "\d lqb_test"
                     Table "public.lqb_test"
 Column |         Type          | Collation | Nullable | Default 
--------+-----------------------+-----------+----------+---------
 id     | integer               |           | not null | 
 value  | character varying(32) |           | not null | 
1
2
3
4
[manu-T480 changelog]# docker exec --user postgres -i db psql testdb testuser -c "select * from lqb_test"
 id | value 
----+-------
(0 rows)

Bien. Funciona.

Fin.

PERO POR QUE NECESITO HACER TODO ESTO SI PUEDO ESCRIBIR UN SCRIPT DE SQL!!?!?!11

Bueno, sí, podés. Pero para qué necesitamos containers? Por que querríamos tener un orquestador? Y métricas? Y monitoreo?

Porque ahora son un dolor de cabeza para implementar, y una “pérdida de tiempo”, pero A LARGO PLAZO van a hacer que nuestra vida SEA MUCHO MAS FELIZ.

No digo que necesitamos implementar esto SÍ O SÍ, ni que tampoco necesitamos ninguna otra herramienta específica para tener un proceso funcional y performante. Solamente a veces es lindo dar un paso atras, mirar el proceso completo de punta a punta, y ver que QUIZÁS haya lugares que les vendría bien un poco de WD-40.


Ahí está. Otro post después de un par de meses.

“Hurra!”, a superar la procrastinación.


  1. Parece como que viví bajo una roca durante 14 años. [return]
  2. docker es bueno, algún día vendrá un artículo sobre él. [return]
  3. Bueno, un poco le importa. Más sobre eso acá [return]