La razón por la que digo que las transacciones no pertenecen a la capa del modelo es básicamente esta:
Los modelos pueden llamar a métodos en otros modelos.
Si un modelo intenta iniciar una transacción, pero no sabe si la persona que llama ya inició una transacción, entonces el modelo tiene que condicionalmente iniciar una transacción, como se muestra en el ejemplo de código en respuesta de @Bubba . Los métodos del modelo tienen que aceptar una bandera para que la persona que llama pueda decirle si tiene permiso para iniciar su propia transacción o no. O bien, el modelo debe tener la capacidad de consultar el estado "en una transacción" de la persona que llama.
public function setPrivacy($privacy, $caller){
if (! $caller->isInTransaction() ) $this->beginTransaction();
$this->privacy = $privacy;
// ...action code..
if (! $caller->isInTransaction() ) $this->commit();
}
¿Qué pasa si la persona que llama no es un objeto? En PHP, podría ser un método estático o simplemente un código no orientado a objetos. Esto se vuelve muy complicado y conduce a una gran cantidad de código repetido en los modelos.
También es un ejemplo de Acoplamiento de control , que se considera malo porque la persona que llama tiene que saber algo sobre el funcionamiento interno del objeto llamado. Por ejemplo, algunos de los métodos de su Modelo pueden tener un parámetro $ transaccional, pero otros métodos pueden no tener ese parámetro. ¿Cómo se supone que la persona que llama sepa cuándo es importante el parámetro?
// I need to override method's attempt to commit
$video->setPrivacy($privacy, false);
// But I have no idea if this method might attempt to commit
$video->setFormat($format);
La otra solución que he visto sugerida (o incluso implementada en algunos marcos como Propel) es hacer beginTransaction()
y commit()
sin operaciones cuando el DBAL sabe que ya está en una transacción. Pero esto puede generar anomalías si su modelo intenta comprometerse y descubre que realmente no se compromete. O intenta retroceder y se ignora esa solicitud. He escrito sobre estas anomalías antes.
El compromiso que he sugerido es que los modelos no conocen las transacciones . El modelo no sabe si su solicitud a setPrivacy()
es algo que debe comprometerse de inmediato o es parte de un panorama más amplio, una serie más compleja de cambios que involucran múltiples Modelos y debe solo comprometerse si todos estos cambios tienen éxito. Ese es el objetivo de las transacciones.
Entonces, si los modelos no saben si pueden o deben comenzar y realizar su propia transacción, ¿quién lo sabe? GRASP incluye un Patrón de controlador que es una clase que no es de interfaz de usuario para un caso de uso, y se le asigna la responsabilidad de crear y controlar todas las piezas para lograr ese caso de uso. Los controladores conocen las transacciones porque ese es el lugar donde se puede acceder a toda la información sobre si el caso de uso completo es complejo y requiere que se realicen múltiples cambios en Modelos, dentro de una transacción (o quizás dentro de varias transacciones).
El ejemplo sobre el que he escrito antes, es iniciar una transacción en beforeAction()
método de un controlador MVC y confirmarlo en afterAction()
método, es una simplificación . El controlador debe tener la libertad de iniciar y realizar tantas transacciones como sea necesario para completar la acción actual. O, a veces, el Controlador podría abstenerse de un control explícito de transacciones y permitir que los Modelos confirmen automáticamente cada cambio.
Pero el punto es que la información sobre qué transacción(es) son necesarias es algo que las Modelos no saben:se les debe informar (en forma de un parámetro $transaccional) o consultarlo a la persona que llama, lo que tendría que delegar la pregunta hasta la acción del controlador de todos modos.
También puede crear una Service Layer de clases, cada una de las cuales sabe cómo ejecutar casos de uso tan complejos y si incluir todos los cambios en una sola transacción. De esa manera evitas mucho código repetido. Pero no es común que las aplicaciones PHP incluyan una capa de servicio distinta; la acción del controlador suele coincidir con una capa de servicio.