Skip to main content

Por qué no uso Triggers ni Procedures en SQL

Triggers/Procedures versus Observers

Llevo más de 30 años trabajando con SQL en sistemas UNIX, dando soporte a miles de clientes y actuando como bombero IT. Sí, bombero, porque apago fuegos técnicos que surgen como resultado de malas prácticas, o, lo que es peor, de seguir utilizando reliquias del pasado sin actualizarse.

Una de esas reliquias son los TRIGGERS y las PROCEDURES.

Qué bonitas eran en su día, qué problemáticas eran y qué problemáticas siguen siendo.

La cuestión es que un motor de bases de datos es lo que es, y en mi opinión, introducir lógicas de programación dentro del motor de bases de datos, a día de hoy, está obsoleto y es peligroso (siempre lo fue).

  • Un cambio de versión del motor SGBD.
  • Un cambio de SGBD.
  • Un problema que obliga a restaurar desde un backup y este es incompleto por error del comando usado en la copia de seguridad.

Y seguramente habrá más.

Lo cierto es que todo eso incumple los principios SOLID, al desplazar en un nivel superior, ya no en la aplicación, sino en el motor de datos, unas lógicas que son responsabilidad del programa. Al menos así lo veo en el año 2024.

Por ejemplo, podemos utilizar Observers a nivel de aplicación.

  • Fáciles de programar.
  • Dentro de la app.
  • Cumplen el objetivo de ser sólidos en nuestra aplicación, y dotan de la responsabilidad al modelo sobre el que se ejecutarían esos triggers.
  • Fácilmente testeables (o los tests, esa cosa que se sigue odiando en muchos lugares, quizás por la falta de... visión o ganas de actualizarse).

Anécdota

En el caso que me ocupa, un Magento 1.9, obsoleto, corriendo en un MariaDB 10.4, PHP 5.4, etc., y que está en producción con decenas de ventas al día.

No se puede trabajar en producción, y mucho menos si la aplicación se llama Magento, paradigma de los problemas en el comercio electrónico.

Así que cuando me enfrenté a ello, y pese a ser root, estar acostumbrado a lidiar con MySQL (y sus forks), me entró la pena negra al ver que había triggers heredados de un intento de migración con la herramienta oficial de Magento para tratar de pasar a Magento 2.

No era la primera vez. Alguna vez lo había visto en un restore de urgencia de otro cliente.

Algo que te rompe cuando estás con bases de datos de 5 a 10Gb, bajo presión, en producción,...

ERROR 1449 (HY000) at line 924: The user specified as a definer ('migration'@'99.99.99.99') does not exist

Bueno, como ya sabía de qué iba al ver la palabra migration y el contexto Magento, sabía que había más de un trigger.

Procesos de reparación

Obtener todos los triggers

mysql> SHOW TRIGGERS;
Trigger Event   Table   Statement       Timing  Created sql_mode        Definer character_set_client    collation_connection    Database Collation
trg_catalog_category_entity_after_insert        INSERT  catalog_category_entity BEGIN\nINSERT INTO m2_cl_catalog_category_entity (`entity_id`, `operation`) VALUES (NEW.entity_id, 'INSERT')ON DUPLICATE KEY UPDATE operation = 'INSERT';\nEND  AFTER   NULL            [email protected] utf8    utf8_general_ci latin1_swedish_ci
...
...

Script para eliminar triggers Mejor un script, que llamaremos drop_triggers.sh.

# Reemplaza los valores de `usuario`, `password`, y `nombre_de_tu_base` con tus propios valores.
MYSQL_USER="usuario"
MYSQL_PASS="password"
DATABASE_NAME="nombre_de_tu_base"

# Obtiene la lista de triggers y los elimina uno por uno
mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -D"$DATABASE_NAME" -e "SELECT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = '$DATABASE_NAME';" | grep -v TRIGGER_NAME | while read TRIGGER_NAME; do
    mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -D"$DATABASE_NAME" -e "DROP TRIGGER IF EXISTS \`$TRIGGER_NAME\`;";
done

Por favor, en serio. Este es algo a tener en cuenta. Backups y backups, y de los que sabes que funcionarán. Yo no soy responsable de tus actos (ni nadie).

Pruebas en docker

Bueno, si antes de hacer líos, necesitas trabajar en un entorno de desarrollo, con docker, te dejo unos pasos, ya que el otro puede ser algo más completo.

Obtener la lista

Puedes como alternativa obtener la lista de trigger de la base de datos, con TablePlus en formato CSV, y crear un fichero con la columna que define el nombre del trigger, por ejemplo triggers.txt.

Docker

Obtenemos el nombre del container de mysql

$ docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED        STATUS        PORTS                    NAMES
b49e62fa5b29   web4-apache-httpd   "httpd-foreground"       19 hours ago   Up 19 hours   0.0.0.0:8080->80/tcp     web4-apache-httpd-1
bfd56c6f6c65   web4-php-fpm        "docker-php-entrypoi…"   19 hours ago   Up 19 hours   9000/tcp                 web4-php-fpm-1
d804a906babd   mariadb:10.1        "docker-entrypoint.s…"   19 hours ago   Up 19 hours   0.0.0.0:3337->3306/tcp   web4-mysql-db-1

Subimos el fichero

docker cp triggers.sh web4-mysql-db-1:/root/

Y ejecutarlo en el container.

$ docker exec -it  web4-mysql-db-1 bash
root@d804a906babd:/# cd /root
# chmod 755  delete_triggers.sh
# ./delete_triggers.sh
Eliminando trigger: trg_catalog_category_entity_after_insert
Eliminando trigger: trg_catalog_category_entity_after_update
Eliminando trigger: trg_catalog_category_entity_after_delete
Eliminando trigger: trg_catalog_category_entity_datetime_after_insert
Eliminando trigger: trg_catalog_category_entity_datetime_after_update
Eliminando trigger: trg_catalog_category_entity_datetime_after_delete
Eliminando trigger: trg_catalog_category_entity_decimal_after_insert
Eliminando trigger: trg_catalog_category_entity_decimal_after_update
Eliminando trigger: trg_catalog_category_entity_decimal_after_delete
Eliminando trigger: trg_catalog_category_entity_int_after_insert
Eliminando trigger: trg_catalog_category_entity_int_after_update
Eliminando trigger: trg_catalog_category_entity_int_after_delete
Eliminando trigger: trg_catalog_category_entity_text_after_insert
Eliminando trigger: trg_catalog_category_entity_text_after_update
Eliminando trigger: trg_catalog_category_entity_text_after_delete
Eliminando trigger: trg_catalog_category_entity_varchar_after_insert
Eliminando trigger: trg_catalog_category_entity_varchar_after_update
Eliminando trigger: trg_catalog_category_entity_varchar_after_delete
Eliminando trigger: trg_catalog_category_product_after_insert
Eliminando trigger: trg_catalog_category_product_after_update
Eliminando trigger: trg_catalog_category_product_after_delete
Eliminando trigger: trg_catalog_compare_item_after_insert
Eliminando trigger: trg_catalog_compare_item_after_update
Eliminando trigger: trg_catalog_compare_item_after_delete
Eliminando trigger: trg_catalog_product_bundle_option_after_insert
Eliminando trigger: trg_catalog_product_bundle_option_after_update
Eliminando trigger: trg_catalog_product_bundle_option_after_delete
Eliminando trigger: trg_catalog_product_bundle_option_value_after_insert
Eliminando trigger: trg_catalog_product_bundle_option_value_after_update
Eliminando trigger: trg_catalog_product_bundle_option_value_after_delete
Eliminando trigger: trg_catalog_product_bundle_selection_after_insert
Eliminando trigger: trg_catalog_product_bundle_selection_after_update
Eliminando trigger: trg_catalog_product_bundle_selection_after_delete
Eliminando trigger: trg_catalog_product_bundle_selection_price_after_insert
Eliminando trigger: trg_catalog_product_bundle_selection_price_after_update
Eliminando trigger: trg_catalog_product_bundle_selection_price_after_delete
Eliminando trigger: trg_catalog_product_entity_after_insert
Eliminando trigger: trg_catalog_product_entity_after_update
Eliminando trigger: trg_catalog_product_entity_after_delete
Eliminando trigger: trg_catalog_product_entity_datetime_after_insert
Eliminando trigger: trg_catalog_product_entity_datetime_after_update
Eliminando trigger: trg_catalog_product_entity_datetime_after_delete
Eliminando trigger: trg_catalog_product_entity_decimal_after_insert
Eliminando trigger: trg_catalog_product_entity_decimal_after_update
Eliminando trigger: trg_catalog_product_entity_decimal_after_delete
Eliminando trigger: trg_catalog_product_entity_group_price_after_insert
Eliminando trigger: trg_catalog_product_entity_group_price_after_update
Eliminando trigger: trg_catalog_product_entity_group_price_after_delete

Toda una aventura, que en modo stress, no es buena para nadie.

Finales

Muchas veces oigo en mi trabajo, "Es que se puede hacer así".

Con mil cosas, caracteres extendidos, desarrollos con millones de ficheros en la misma carpeta, etc. Y la misma cantinela. "Ahora el límite..." "Se puede..."

La realidad es muy distinta. Y eso se aprende en el infierno de los sistemas, cuando llaman a nuestra puerta.

Una cosa es que se pueda y otra muy distinta, es que se deba.

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.