Codeigniter, Transacciones

Para el que nunca lo haya visto, un transacción en base de datos es una serie de operaciones que se ejecutan como si fuesen una sola, y si una de estas falla, todos los cambios realizados hasta ese punto volverán atrás. Esto se aplica para aquellas operaciones en base de datos que realizan cambios en las tablas; un INSERT, un UPDATE o un DELETE; a diferencia de los SELECT que no realizan alteraciones en las filas.

Supongamos esta situación: hay una operación bancaria en donde una persona va a hacer una transferencia de dinero de su cuenta a otra. Esto llevaría una serie de pasos, se me ocurre: restar el dinero en la cuenta emisora, generar el registro de dicha transferencia y por último sumar el monto a la cuenta receptora. Pero por alguna razón uno de estas operaciones no funciona correctamente. Por ejemplo la cuenta receptora no puede recibir transferencias, entonces todo volvería atrás y el dinero que iba a transferir el emisor obviamente no se resta.

Se dice que las transacciones son el todo o nada, porque o se ejecutan todos los procesos de forma correcta o todos se cancelan.

Ahora, llevando esto a la práctica, en Codeigniter, una transacción puede realizarse de modo muy sencillo. Utilizando el objeto db, el cual como sabemos trabaja con todo lo relacionado a la base de datos.

Iniciamos la transacción:

$this->db->trans_begin();

Todo lo que se haga a partir de aquí puede guardarse o cancelarse por completo.

Después de una serie de procesos, verificamos si hubo algún error:

if ($this->db->trans_status() === FALSE)

Si la transacción falló por algún motivo, volvemos todo atrás con rollback:

$this->db->trans_rollback();

Si no hubo errores y todo salió bien, entonces podemos hacer un commit:

$this->db->trans_commit();

Ejemplo

Supongamos que tenemos una base de datos en donde los clientes se registran e indican cuáles son los servicios que desean adquirir.

Vamos a crear tres tablas con el siguiente código:

Una tabla para guardar los servicios:

CREATE TABLE servicios(
   servicio_id tinyint(2) primary key not null,
   nombre varchar(100) not null
)ENGINE=innoDB;

Otra para los clientes:

CREATE TABLE clientes(
   cliente_id int(10) unsigned not null auto_increment primary key,
   nombre varchar(100) not null,
   apellido varchar(100) not null,
   domicilio varchar(100) not null,
   telefono varchar(100) not null,
   email varchar(100) unique not null,
   fecha_registro datetime not null
)ENGINE=innoDB;

Y finalmente la relación entra ambas:

CREATE TABLE servicios_clientes(
   servicio_id tinyint(2) not null,
   cliente_id int(10) unsigned not null,
   primary key(servicio_id, cliente_id),
   foreign key(servicio_id)
   references servicios(servicio_id),
   foreign key(cliente_id)
   references clientes(cliente_id)
)ENGINE=innoDB;

Además vamos a insertar cuáles son los servicios disponibles con su respectivo código:

INSERT INTO servicios(servicio_id, nombre)
VALUES(11, 'Banda ancha');
INSERT INTO servicios(servicio_id, nombre)
VALUES(22, 'Televisión por cable');
INSERT INTO servicios(servicio_id, nombre)
VALUES(33, 'Teléfono');

Ahora bien, debemos detenernos en ciertos puntos. En primer lugar una restricción de la base de datos es que el email del usuario debe ser único:

email varchar(100) unique not null,

Por tanto, si otro usuario intenta registrarse con una dirección de correo electrónico ya existente, debería devolver un error.

Además los únicos servicios que hay tienen los siguientes código: 11, 22, 33. Al intentar insertar una relación servicios-clientes con otro id, también sería incorrecto.

Y por último, en la tabla que relaciona a los servicios y los clientes, cada registro tendrá como clave primaria el id del cliente y el id del servicio, como un identificador múltiple:

primary key(servicio_id, cliente_id)

Por tanto, sólo podrá haber una única combinación. Si se intenta insertar un segundo registro con un id y un servicio, en donde ambos se relacionan en otra fila, también debería dar un error.

Para representar a la entidad clientes vamos a crear un modelo llamado Cliente_model.php, con el siguiente código:

<?php
class Cliente_model extends CI_Model {
   public function __construct()
   {
      parent::__construct();
   }
   public function registrar($data)
   {
      //Iniciamos la transacción.    
      $this->db->trans_begin();    
      //Intenta insertar un cliente.    
      $this->db->insert('clientes', array(      
         'nombre' => $data['nombre'],      
         'apellido' => $data['apellido'],      
         'domicilio' => $data['domicilio'],       
         'telefono' => $data['telefono'],       
         'email' => $data['email'],      
         'fecha_registro' => date('Y-m-d H:i:s')    
      ));    
      //Recuperamos el id del cliente registrado.    
      $cliente_id = $this->db->insert_id();    
      //Insertamos los servicios que desea adquirir el cliente.    
      foreach($data['servicios'] as $servicio_id){        
         $this->db->insert('servicios_clientes', array(      
            'servicio_id' => $servicio_id,      
            'cliente_id' => $cliente_id      
         ));    
      }    
      if ($this->db->trans_status() === FALSE){      
         //Hubo errores en la consulta, entonces se cancela la transacción.   
         $this->db->trans_rollback();      
         return false;    
      }else{      
         //Todas las consultas se hicieron correctamente.  
         $this->db->trans_commit();    
         return true;    
      }  
   }
}

Simplemente lo que hace el método es insertar un cliente, y luego los servicios que desea adquirir éste.

Ahora, vamos a crear un controlador cualquier para incluir nuestro modelo y probar de forma rápida el funcionamiento de este método con transacciones.

Caso 1: Ok

$this->load->model('cliente_model');
$resultado = $this->cliente_model->registrar(array(
 'nombre' => 'Juan',
 'apellido' => 'Pérez',
 'domicilio' => 'Calle Falsa 1234', 
 'telefono' => '55555555', 
 'email' => 'juan_perez@mail.com', 
 'servicios' => array(11, 22) 
)); 
if($resultado){ 
   echo 'El cliente fue registrado correctamente'; 
}else{ 
   echo 'El cliente NO fue registrado, porque hubo errores'; 
}

El cliente se inserta, pero también lo hacen los servicios. No hubo ningún error, por eso se puede hacer commit.

Caso 2: Columna única

$this->load->model('cliente_model');
$resultado = $this->cliente_model->registrar(array(
 'nombre' => 'Cosme',
 'apellido' => 'Fulanito',
 'domicilio' => 'Calle sin número',
 'telefono' => '55555555',
 'email' => 'juan_perez@mail.com',
 'servicios' => array(11, 22) 
)); 
if($resultado){ 
   echo 'El cliente fue registrado correctamente'; 
}else{
   echo 'El cliente NO fue registrado, porque hubo errores';
}

Se intenta insertar un cliente con un correo que ya le pertenece a otro, se produce el primer error. Los cambios se vuelven para atrás con rollback. No se inserta el cliente y tampoco los servicios.

Caso 3: Datos duplicados

$this->load->model('cliente_model'); 
$resultado = $this->cliente_model->registrar(array(
 'nombre' => 'Cosme',
 'apellido' => 'Fulanito',
 'domicilio' => 'Calle sin número',
 'telefono' => '55555555',
 'email' => 'cosme_fulanito@mail.com',
 'servicios' => array(11, 22, 33, 22) 
)); 
if($resultado){ 
   echo 'El cliente fue registrado correctamente'; 
}else{ 
   echo 'El cliente NO fue registrado, porque hubo errores'; 
}

Se intenta insertar con un cliente dos veces el mismo servicio, lo cual nuestra base de datos no nos permite. Esto no sólo hace que no se inserten los servicios, sino que se cancela el paso previo de registrar al cliente, a pesar de que los datos eran correctos.

Caso 4: Clave foránea de un registro que no existe

$this->load->model('cliente_model'); 
$resultado = $this->cliente_model->registrar(array(
 'nombre' => 'Cosme',
 'apellido' => 'Fulanito',
 'domicilio' => 'Calle sin número',
 'telefono' => '55555555',
 'email' => 'cosme_fulanito@mail.com',
 'servicios' => array(55) 
)); 
if($resultado){
   echo 'El cliente fue registrado correctamente'; 
}else{
   echo 'El cliente NO fue registrado, porque hubo errores'; 
}

En este último caso, al igual que el anterior, no hay errores en los datos del cliente, pero se está intentando insertar un en la tabla ‘servicios_clientes’ una clave externa de un registro que no existe en la tabla servicios. La transacción devuelve errores, y el paso anterior se elimina también.

Nota: Si estás en modo ‘development’, seguro te va a mostrar los errores de SQL por pantalla. Al cambiarlo a modo ‘production’, no te mostrará los errores, y el método registrar() te devolverá false, debido al fallo.


One Reply to “Codeigniter, Transacciones”

  1. interesante, nunca me habia puesto a pensar en estas cosas tal como lo explicas, un punto a tener en cuenta a futuro.