El problema con las transacciones anidadas en Ruby on Rails

Muchas veces tenemos que realizar varias operaciones en la BD y queremos que esas operaciones se comporten como si fuesen una sola. Es decir, si falla una de esas operaciones, queremos que se deshagan todas. Para lograr esto, podemos usar ✨transacciones✨ que nos facilitan la vida.

El tema es que a veces, sobre todo a medida que se complejiza el código, podemos tener transacciones anidadas y eso puede traernos algunos problemitas sin que lo notemos.

Veamos un ejemplo: imaginemos que tenemos que crear un producto con variantes (distinto talle, tamaño, etc).
Para eso, podemos usar un código como el siguiente:

Ahora, imaginemos que la implementación de create_variants! es así 👇 y que explota, lanzando una excepción porque las variantes tienen el mismo kind:

Como en ningún lado se rescata la excepción, se propaga hasta el método create_product! y se rollbackea todo. Es decir, no se crea ni el producto, ni las variantes 👏👏. ¡Todo joya!

Ahora, modifiquemos el código para que se rescate la excepción en create_variants!:

En este caso, la excepción es atrapada en create_variants! y al volver a create_product! la transacción original se commitea (😱) porque no hubo excepción que la frene. ¿Cual es el resultado? ¡Se crea el producto y las variantes! Es decir, la transacción de afuera es la que manda y hace que todo lo que pasó dentro de ella se commitee.

Quizás el ejemplo es demasiado burdo porque está todo en un mismo archivo, pero muchas veces invocamos servicios que llevan a cabo transacciones sin que nos demos cuenta.

¿Cómo podríamos salvar esto? Les dejo dos opciones muy sencillas:
1) No rescatar la excepción en create_variants!, como vimos en el primer ejemplo
2) Retornar el resultado de create_variants y actuar según el resultado:

Este es solo uno de los problemas que nos podemos cruzar al trabajar con transacciones. También existe el caso en el que queremos que la transacción de adentro sea independiente de la de afuera. Para esto podemos usar podrían usar la opción “requires_new: true (ver documentación).