Administrando esquemas de bases de datos en grandes equipos de desarrollo con Liquibase y PostgreSQL

Hola con tod@s, una vez mas continuamos con un nuevo post y esperamos que sea de importancia en tu diario andar por el mundo tecnológico. En esta ocasión les traemos un tema interesante que es de mucha utilidad sobre todo cuando trabajamos con grandes grupos de desarrollo que continuamente estan actualizando los esquemas de la base de datos (creando, modificando o eliminando tablas y/o columnas) manteniendo varias versiones del sistema en producción con varias versiones de la base de datos cuya instalación, migración e implementación se requiere sea de manera fácil y eficiente.

Imagina por un momento que administras un sistema de backend donde normalmente desarrollan 100 o más personas que contínuamente están modificando la estructura de la base de datos y hay que implementar varias versiones del sistema en producción, cada uno con una versión distinta de la misma base de datos. Si, es un escenario muy común en grandes grupos de trabajo y que supondría un dolor de cabeza si no se adecúa las herramientas necesarias para facilitar esta tarea. Es por ello que hablaremos de liquibase una herramienta muy útil para superar estas complicaciones de manera fácil y elegante.

Liquibase

Liquibase es una librería que nos permite administrar migraciones de esquemas de bases de datos que es una tarea necesaria en cualquier proyecto de software. Esta librería tiene una versión community y Open Source y otra versión de pago. Para los objetivos de este post, vamos a utilizar la versión community de esta librería.

Si deseas averiguar más sobre liquibase, visita su web oficial: https://www.liquibase.org/

Para empezar con la utilización de esta librería, vamos a introducir unos breves conceptos para entender como se utiliza y que contiene este software. Vamos a conocer que son y en se utilizan el ChangeLog, ChangeSet, Rollback, Context, Configuration File y a su vez lo haremos con el apoyo de docker.

ChangeLog

Los cambios que los desarrolladores aplican sobre la base de datos se deben centralizar en un archivo demoninado ChangeLog en forma ordenada secuencialmente. Este archivo puede ser escrito en varios formatos como XML, SQL, YAML, JSON y otros más; sin embargo, la sintáxis de este archivo deberá ser verificada dentro de las especificaciones de liquibase (por ello, personalmente prefiero escribirlo en xml que puede ser verificado y autocompletado gracias a los esquemas que provee liquibase sin necesidad de utilizar herramientas adicionales como en el caso de otros formatos). Este archivo, como ya veremos más adelante, contiene un conjunto de cambios (ChangeSet) que los desarrolladores aplicarán sobre la base de datos en forma secuencial.

Un ejemplo de un esquema básico de un archivo de changelog (escrito en xml) es:

<?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"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
        http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
    .
    .
    .
</databaseChangeLog>

Para mayor información, visita https://docs.liquibase.com/concepts/basic/changelog.html

ChangeSet

Un changeset es una unidad de cambio que liquibase ejecutará sobre la base de datos en orden secuencial y el cual contiene en su interior los cambios que se van a realizar sobre el esquema de la base de datos. Estos changesets son identificados por un id único y el autor del mismo. Un conjunto de changesets constituye un archivo de changelog. Estas unidades de cambio son las que nos van a guiar sobre la migración de un esquema de una base de datos y a su vez éstas pueden ser reversadas mediante un proceso denominado rollback. Existen changesets con rollback automáticos y otros donde obligadamente deberemos escribir un rollback personalizado.

Un ejemplo de un changeset (escrito en xml) es:

    <changeSet id="SEQ-00001" author="Developer 1">
        .
        .
        .
    </changeSet>

Estos cambios deben estar dentro del tag \<databaseChangeLog>.

Para mayor información, visita https://docs.liquibase.com/concepts/basic/changeset.html

Rollback

Uno de los puntos más importantes de administrar adecuadamente los esquemas de las bases de datos es poder movernos fácilmente dentro de los esquemas y versiones que tengamos sin mucho problema; para ello, un punto necesario es poder reversar cualquier changeset que se haya aplicado a una base de datos. El rollback es una operación inversa a cualquier changeset que se aplique.

Algunos changeset vienen predefinidos con autorollback es decir que por su propia definición liquibase ya conoce como reversar dicho cambio. Por ejemplo: si un changeset define una operación de create table esta puede ser fácilmente reversada con un drop table sin necesidad de conocer más detalles que el nombre de la tabla (liquibase lo hace de manera automática por nosotros); sin embargo, si un changeset define un drop table no hay forma de conocer su operación inversa porque ello implicaría conocer los campos y tipos de datos que esa tabla contenía; en ese caso liquibase nos indica que dicha operacion no tiene un reverso automático y la debemos escribir por nuestra propia cuenta mediante la etiqueta \<rollback> dentro del changeset.

Para poder conocer las operaciones que liquibase nos provee de autorollback, podemos buscar fácilmente en la documentación oficial (https://docs.liquibase.com/change-types/community/home.html) en donde podremos consultar que tipos de cambio son soportados, cuáles tienen autorollback y cuáles de ellos son compatibles con la base de datos que utilizemos.

Recuerda que una migración adecuada debe poder retornar a cualquier versión de la base de datos en un punto de tiempo específico que deseemos.

Context

Cuando trabajamos en grupos de desarrollo, siempre va ser necesario desplegar varias versiones del sistema para distintos propósitos; por ejemplo, tendremos un ambiente destinado para developers donde se apliquen y prueben los últimos cambios, podemos tener un ambiente para calidad de software donde los testers pueden probar una versión específica del sistema a ser lanzada y así mismo ambientes específicos para distintos fines (sin contar los numerosos ambientes que podemos tener en producción) y cada uno de ellos con una versión distinta de la misma base de datos. Así mismo, es frecuente siempre encontrarnos con escenarios donde va a haber cambios específicos en la base de datos que solo deben ser desplegados en una versión o ambiente específico y que no deben estar en los demás (por diversas razones).

Para ello, liquibase nos provee de un atributo dentro de la etiqueta changeset que se llama context. Este atributo puede ser utilizado para indicar en que contexto va a ser aplicado este cambio y en cuales no. Por ejemplo si se define un context ="qa" se puede desplegar una version de la base de datos que cumpla con esa etiqueta. Adicional, y para dotar de mayor versatilidad, se puede incluir operaciones lógicas dentro de la etiqueta tales como !, AND y OR, pudiendo incluso ocupar varias de ellas en la misma etiqueta de la siguiente manera:

context="!qa"
context="v1.0 or v1.1"
context="v1.0 or qa"
context="qa, development"
context="!qa and !development"

De esta forma, cuando se ejecute liquibase, podremos enviarle los parámetros exactos para desplegar una versión específica de la base de datos.

Configuration File

Un vez que hayamos escrito el archivo de changelog tenemos que ejecutar liquibase para aplicar los cambios sobre la base de datos, indicando cuál la ruta del archivo de changhelog, la url y las credenciales de la base de datos, el tipo de base de datos y el driver para ser utilizado en la conexión de la base de datos. Estos parámetros pueden llamarse mediante una terminal ejecutando el siguiente comando (más adelante veremos cómo el último parámetro puede ser cambiado para las distintas acciones que queremos ejecutar):

liquibase --driver=oracle.jdbc.OracleDriver --classpath="/my-path/liquibase/lib" \
      --changeLogFile="/my_project/db/changelogs/changelog_db.xml" \
      --url="jdbc:oracle://{host}:{port}/{database_name}" \
      --username={db_username} --password={db_password} update

Sin embargo, no es un buen enfoque hacerlo de esta manera; para ello podemos escribir un archivo de configuración donde contenga todos estos atributos y pasarle como parámetro a liquibase para ser ejecutado. Por ejemplo, crearemos un archivo de configuración que se denominará liquibase.properties y contendra los siguientes campos:

classpath: /my-path/liquibase/lib
changeLogFile: ./liquibase-changeLog.xml
url: jdbc:mysql://localhost:3306/{database_name}
username: {db_user}
password: {db_password}
driver: com.mysql.jdbc.Driver
liquibaseProLicenseKey: {your_licence_key_if_you_have_one}

Para mayor información, visita https://docs.liquibase.com/workflows/liquibase-community/creating-config-properties.html

Una vez que hemos revisado los conceptos básicos y las ventajas que liquibase ofrece, vamos a hacer un pequeño ejemplo usando una imagen de docker de liquibase y una de postgreSQL.

Liquibase sobre docker

Para poder instalar liquibase sobre docker solo necesitamos ejecutar el siguiente comando:

docker pull liquibase/liquibase

En este caso no debemos crear el container de docker porque lo haremos en cada ejecución como lo veremos más adelante. Así mismo, en la documentación de liquibase nos indica que esta imagen ya viene incluído por defecto un driver de postgreSQL por lo que no necesitamos despues especificarlo.

PostgreSQL sobre docker

Para este ejemplo, vamos a utilizar una base de datos en postgreSQL. Podemos instalar una instancia de la base de datos de manera rápida usando docker. Para hacerlo, sigue nuestro anterior post https://blog.nano-bytes.com/2020/06/10/postgresql-en-docker-para-pruebas-desarrollo/

Ejemplo de uso de liquibase

Una vez introducidos los conceptos básicos y con nuestras imagenes de docker de liquibase y postgreSQL listas, vamos a empezar.

Primero necesitamos una carpeta donde vamos a contener nuestros archivos (en un paquete de desarrollo puede ir dentro de la carpeta resources). En esta carpeta vamos a crear nuestro Configuration File que se llamará liquidbase.properties y contendrá:

classpath: /liquibase/changelog/
changeLogFile: ./dbchangelog.xml
url: jdbc:postgresql://{ip_host}:{port}/{database_name}
username: {db_user} 
password: {db_password}

Unos puntos a distinguir en el archivo de configuración anterior son:

  • Ocuparemos la version community de liquibase por lo que no especificaremos el atributo liquibaseProLicenseKey.
  • La imagen de docker de liquibase contiene un classpath por defecto: /liquibase/changelog/ que lo mapearemos con nuestra carpeta resources de nuestro proyecto de desarrollo el momento de ejecutar liquibase a través de docker.
  • Recordemos que este archivo, al ser ejecutado desde la imagen de docker, contendrá una llamada a la base de datos colocando la ip de la interfaz de red de nuestra máquina en lugar de localhost.

Ahora, vamos a definir el archivo de changelog con los siguientes changesets:

<?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"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
        http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

    <changeSet id="SEQ-00001" author="developer 1" context="qa, develop">
        <createTable tableName="table_test">
            <column name="id" type="int" autoIncrement="true"/>
            <column name="description" type="varchar(255)"/>
        </createTable>
    </changeSet>

    <changeSet id="SEQ-00002" author="developer 1" context="qa, develop">
        <addPrimaryKey tableName="table_test" columnNames="id" />
    </changeSet>

    <changeSet id="SEQ-00003" author="developer 2" context="qa, develop">
        <addColumn tableName="table_test">
            <column name="json_value" type="json"/>
        </addColumn>
    </changeSet>

    <changeSet id="SEQ-00004" author="developer 3" context="develop">
        <modifyDataType tableName="table_test" columnName="json_value" newDataType="text"/>
        <rollback>
            <modifyDataType tableName="table_test" columnName="json_value" newDataType="json"/>
        </rollback>
    </changeSet>

</databaseChangeLog>

Ahora nos queda ejecutar estos cambios con algunos parámetros para comprobar el funcionamiento y las ventajas de liquibase.

Ejecutar todas las actualizaciones

Para poder ejecutar todas las actualizaciones en orden secuencial (independientemente del context de las mismas), ejecutamos el siguiente comando:

docker run --rm -v /{local_path}/resources:/liquibase/changelog liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties update

Una vez finalizada la ejecución, veremos el mensaje Liquibase: Update has been successful. y los resultados sobre la base de datos serán:

  • Creación de la tabla table_test con todos los cambios solicitados:
my_database-# \d table_test                                    
Table "public.table_test"    
  Column    |          Type          | Collation | Nullable |             Default               
------------+------------------------+-----------+----------+----------------------------------  
id          | integer                |           | not null | generated by default as identity  
description | character varying(255) |           |          |   
json_value  | text                   |           |          |  
Indexes:     "table_test_pkey" PRIMARY KEY, btree (id)
  • Creación de una tabla denominada databasechangelog que es la tabla que usa liquibase para controlar versiones, cambios, contextos, sumas de verificación, fechas de ejecución, etc. Esta tabla es la que debemos consultar para conocer que versión de esquema de base de datos tenemos. En nuestro caso la veremos de esta manera:
my_database-# SELECT * FROM databasechangelog;
     id     |   author    |                 filename                  |        dateexecuted        | orderexecuted | exectype |               md5sum               |                        description                         | comments | tag | liquibase |   contexts    | labels | deployment_id
 -----------+-------------+-------------------------------------------+----------------------------+---------------+----------+------------------------------------+------------------------------------------------------------+----------+-----+-----------+---------------+--------+---------------
  SEQ-00001 | developer 1 |             ./dbchangelog.xml             | 2021-02-03 20:27:25.268992 |             1 | EXECUTED | 8:a8bca97eefd0e3644ac3182a82ef7221 | createTable tableName=table_test                           |          |     | 4.2.2     | (qa, develop) |        | 2384045200  
  SEQ-00002 | developer 1 |             ./dbchangelog.xml             | 2021-02-03 20:27:25.282638 |             2 | EXECUTED | 8:f97c53c9d34000a5553f37be80fff4bd | addPrimaryKey tableName=table_test                         |          |     | 4.2.2     | (qa, develop) |        | 2384045200  
  SEQ-00003 | developer 2 |             ./dbchangelog.xml             | 2021-02-03 20:27:25.292486 |             3 | EXECUTED | 8:bfedabe1bb03131fe7ade4d7ae2f8101 | addColumn tableName=table_test                             |          |     | 4.2.2     | (qa, develop) |        | 2384045200  
  SEQ-00004 | developer 3 |             ./dbchangelog.xml             | 2021-02-03 20:27:25.31296  |             4 | EXECUTED | 8:c6a18109568c81a1415fd6fbbb614501 | modifyDataType columnName=json_value, tableName=table_test |          |     | 4.2.2     | develop       |        | 2384045200 
(4 rows)

Esta tabla contiene una lista de los cambios aplicados en el esquema de la base de datos.

Rollback para un número específico de changesets

Ahora vamos a ejecutar un rollback para volver la base de datos a la versión del changeset SEQ-00003 es decir vamos a reversar el último cambio. Con ello, el campo denominado json_value volverá a ser del tipo json. Ejecutamos el siguiente comando:

docker run --rm -v /{local_path}/resources:/liquibase/changelog liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties rollbackCount 1

Obtenemos un mensaje de este tipo en la consola: Liquibase: Rollback has been successful. con el siguiente resultado:

my_database-# \d table_test                                    
Table "public.table_test"    
   Column    |          Type          | Collation | Nullable |             Default               
-------------+------------------------+-----------+----------+----------------------------------  
 id          | integer                |           | not null | generated by default as identity  
 description | character varying(255) |           |          |   
 json_value  | json                   |           |          |  
 Indexes:     "table_test_pkey" PRIMARY KEY, btree (id)

El campo json_value volvió a ser del tipo json. La tabla databasechangelog nos indica que la base de datos volvió a la versión del SEQ-00003:

 my_database-# SELECT * FROM databasechangelog;     
     id     |   author    |                 filename                  |        dateexecuted        | orderexecuted | exectype |               md5sum               |            description             | comments | tag | liquibase |   contexts    | labels | deployment_id  
 -----------+-------------+-------------------------------------------+----------------------------+---------------+----------+------------------------------------+------------------------------------+----------+-----+-----------+---------------+--------+---------------  
  SEQ-00001 | developer 1 |             ./dbchangelog.xml             | 2021-02-03 20:27:25.268992 |             1 | EXECUTED | 8:a8bca97eefd0e3644ac3182a82ef7221 | createTable tableName=table_test   |          |     | 4.2.2     | (qa, develop) |        | 2384045200  
  SEQ-00002 | developer 1 |             ./dbchangelog.xml             | 2021-02-03 20:27:25.282638 |             2 | EXECUTED | 8:f97c53c9d34000a5553f37be80fff4bd | addPrimaryKey tableName=table_test |          |     | 4.2.2     | (qa, develop) |        | 2384045200  
  SEQ-00003 | developer 2 |             ./dbchangelog.xml             | 2021-02-03 20:27:25.292486 |             3 | EXECUTED | 8:bfedabe1bb03131fe7ade4d7ae2f8101 | addColumn tableName=table_test     |          |     | 4.2.2     | (qa, develop) |        | 2384045200 
  (3 rows)

Colocar una marca de tiempo específica

Ahora, supongamos que estos 3 cambios pertenecen al release 1.0.0 y el cuarto changeset será parte de una versión posterior. En ese caso nos interesaría colocar una marca que nos indique hasta cuál changeset pertenece al release mencionado. En este caso, añadimos un changeset con una etiqueta denominada tagDatabase despues del tercer changeset, teniendo finalmente un changelog file de la siguiente manera:

<?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"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
        http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

    <changeSet id="SEQ-00001" author="developer 1" context="qa, develop">
        <createTable tableName="table_test">
            <column name="id" type="int" autoIncrement="true"/>
            <column name="description" type="varchar(255)"/>
        </createTable>
    </changeSet>

    <changeSet id="SEQ-00002" author="developer 1" context="qa, develop">
        <addPrimaryKey tableName="table_test" columnNames="id" />
    </changeSet>

    <changeSet id="SEQ-00003" author="developer 2" context="qa, develop">
        <addColumn tableName="table_test">
            <column name="json_value" type="json"/>
        </addColumn>
    </changeSet>

    <changeSet id="release-v1.0.0" author="developer team">
        <tagDatabase tag="version_1.0.0"/>
    </changeSet>

    <changeSet id="SEQ-00004" author="developer 3" context="develop">
        <modifyDataType tableName="table_test" columnName="json_value" newDataType="text"/>
        <rollback>
            <modifyDataType tableName="table_test" columnName="json_value" newDataType="json"/>
        </rollback>
    </changeSet>

</databaseChangeLog>

Primero, aplicamos un rollbackCount 3 para tener el databasechangelog limpio y ningún changeset ejecutado. Luego vamos a actualizar la base de datos con la versión de base necesaria para la versión 1.0.0 ejecutando el siguiente comando:

docker run --rm -v /{local_path}/resources:/liquibase/changelog liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties updateToTag version_1.0.0

Obteniendo un mensaje: Liquibase command 'updateToTag' was executed successfully. con el siguiente resultado:

my_database-# SELECT * FROM databasechangelog;        
       id       |     author     |                 filename                  |        dateexecuted        | orderexecuted | exectype |               md5sum               |            description             | comments |      tag      | liquibase |   contexts    | labels | deployment_id  
----------------+----------------+-------------------------------------------+----------------------------+---------------+----------+------------------------------------+------------------------------------+----------+---------------+-----------+---------------+--------+---------------  
 SEQ-00001      | developer 1    |             ./dbchangelog.xml             | 2021-02-03 22:26:09.814642 |             1 | EXECUTED | 8:a8bca97eefd0e3644ac3182a82ef7221 | createTable tableName=table_test   |          |               | 4.2.2     | (qa, develop) |        | 2391169743  
 SEQ-00002      | developer 1    |             ./dbchangelog.xml             | 2021-02-03 22:26:09.826229 |             2 | EXECUTED | 8:f97c53c9d34000a5553f37be80fff4bd | addPrimaryKey tableName=table_test |          |               | 4.2.2     | (qa, develop) |        | 2391169743  
 SEQ-00003      | developer 2    |             ./dbchangelog.xml             | 2021-02-03 22:26:09.835959 |             3 | EXECUTED | 8:bfedabe1bb03131fe7ade4d7ae2f8101 | addColumn tableName=table_test     |          |               | 4.2.2     | (qa, develop) |        | 2391169743  
 release-v1.0.0 | developer team |             ./dbchangelog.xml             | 2021-02-03 22:26:09.839866 |             4 | EXECUTED | 8:c1cc30f1cbc414cdfeade1172c5e7a91 | tagDatabase                        |          | version_1.0.0 | 4.2.2     |               |        | 2391169743 (4 rows)

De la misma manera, si a futuro se aplican más cambios a la base de datos, podemos volver a tener la versión 1.0.0 de la base de datos de manera simple. Como tenemos nuestra marca de tiempo, sólo es cuestión de ejecutar el siguiente comando y tendremos la base de datos lista:

docker run --rm -v /{local_path}/resources:/liquibase/changelog liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties rollback version_1.0.0

Sumas de verificación

Dentro del registro de cada changeset en la base de datos, existe un campo llamado md5sum. Este campo es una suma de verificación que liquibase realiza sobre cada changeset y cuyo objetivo es verificar que un changeset no sea modificado posterior a su ejecución, que por obvias razones no debe ser modificado. Cualquier modificación que se realice sobre la base de datos se deberá incluír en un nuevo changeset. A su vez, vale aclarar que la etiqueta rollback que pertenece a cada changeset no es considerado el momento del cálculo de la suma de verificación (esto se debe a que muchas veces necesitamos cambiar los rollbacks posterior a haber aplicado un changeset).

Si un changeset fue cambiado y una suma de verificación no corresponde con la encontrada en cada changeset aplicado en la base de datos, liquibase detiene la ejecución de todos los cambios informando de este error. Por ello una forma de verificar si un archivo de changelog esta correcto y sus sumas de verificación son correctas, podemos ejecutar el siguiente comando:

docker run --rm -v /{local_path}/resources:/liquibase/changelog liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties validate

En el caso de ser correcto el archivo y sus sumas de verificación, veremos el siguiente resultado:

No validation errors found.
Liquibase command 'validate' was executed successfully.

Este comando no realiza ningún cambio sobre la base de datos.

Con este pequeño ejemplo que hemos realizado podemos comprobar la utilidad de una herramienta de este tipo, sobre todo cuando tenemos varios ambientes, varios entornos, cantidad de developers y testers que puede llegar a acomplejar el panorama.

A su vez, liquibase tiene una serie de comandos de bastante utilidad como: probar rollbacks, generar los scripts SQL de una versión definida, generar los scripts SQL para un rollback definido, exportar cambios, y muchos otros más. Te recomiendo que revises la web oficial de liquibase para más detalles.

Recuerden escribir si tienen alguna duda o comentario.

Hasta un próximo post!

You may also like...