php - ¿Está haciendo la gestión de transacciones en la mala práctica del controlador?
mysql design-patterns (4)
Estoy trabajando en una aplicación PHP / MySQL usando el framework Yii.
Me he encontrado con la siguiente situación:
En mi VideoController
, tengo una actionCreate
que crea un nuevo video y actionPrivacy
que establece la privacidad en el video. El problema es que durante la actionCreate
setPrivacy
método setPrivacy
del modelo de Video
que actualmente tiene una transacción. Me gustaría que la creación del video también esté en una transacción, lo que genera un error ya que una transacción ya está activa.
En el comentario sobre esta respuesta , Bill Karwin escribe
Por lo tanto, no es necesario hacer que las clases del Modelo de dominio o las clases DAO administren las transacciones, simplemente hágalo en el nivel Controlador
y en esta respuesta :
Como usa PHP, el alcance de sus transacciones es como máximo una sola solicitud. Por lo tanto, solo debe usar transacciones administradas por contenedor, no transacciones de capa de servicio. Es decir, comience la transacción al inicio de la gestión de la solicitud y comprométala (o retrotúyala) cuando termine de gestionar la solicitud.
Si administro las transacciones en el controlador, tendré un montón de código que se ve así:
public function actionCreate() {
$trans = Yii::app()->getDb()->beginTransaction();
...action code...
$trans->commit();
}
Eso lleva a código duplicado en muchos lugares donde necesito transacciones para la acción.
O podría refactorizarlo en los beforeAction()
y afterAction()
de la clase Controller
principal que crearía automáticamente transacciones para cada acción que se realiza.
¿Habría algún problema con este método? ¿Cuál es una buena práctica para la gestión de transacciones para una aplicación PHP?
Bueno, una desventaja de estas transacciones amplias (en toda la solicitud) es que limita las capacidades de concurrencia de su motor de base de datos y también aumenta la probabilidad de bloqueos. Desde este punto de vista, podría ser rentable colocar las transacciones solo donde las necesita y permitirles cubrir solo el código que debe cubrirse.
Si es posible, definitivamente iría por colocar transacciones en modelos. El problema con las transacciones superpuestas se puede resolver introduciendo BaseModel (ancestros de todos los modelos) y variable transactionLock en ese modelo. Luego, simplemente ajusta sus directivas de transacción begin / commit en métodos BaseModel que respeten esta variable.
Mejores prácticas: coloque las transacciones en el modelo, no coloque las transacciones en el controlador.
La principal ventaja del patrón de diseño MVC es este: MVC hace que las clases modelo sean reutilizables sin modificaciones. Facilita el mantenimiento y la implementación de nuevas características.
Por ejemplo, presumiblemente, usted se está desarrollando principalmente para un navegador donde un usuario ingresa una colección de datos a la vez, y usted mueve la manipulación de datos al controlador. Más tarde, se dará cuenta de que debe admitir que el usuario pueda cargar una gran cantidad de colecciones de datos para importar en el servidor desde la línea de comandos.
Si toda la manipulación de datos estaba en el modelo, simplemente podría sorber los datos y pasarlos al modelo para que los maneje. Si hay una funcionalidad necesaria (transaccional) en el controlador, tendría que replicar eso en su script CLI.
Por otro lado, tal vez termines con otro controlador que necesita realizar la misma funcionalidad desde un punto diferente. Tendrá que replicar el código en ese otro controlador también ahora.
Para ello, solo necesita resolver los desafíos de transacción en el modelo.
Suponiendo que tiene una clase de video (modelo) con el método setPrivacy () que ya tiene la compilación de transacciones; y desea llamarlo desde otro método persist () que también necesita ajustar su funcionalidad en una transacción más grande, simplemente puede modificar setPrivacy () para realizar una transacción condicional.
Quizás algo como esto.
class Video{
private $privacy;
private $transaction;
public function __construct($privacy){
$this->privacy = $privacy;
}
public function persist(){
$this->beginTransaction();
// ...action code...
$this->setPrivacy($this->privacy, false);
// ...action code...
$this->commit();
}
public function setPrivacy($privacy, $transactional = true){
if ($transactional) $this->beginTransaction();
$this->privacy = $privacy;
// ...action code..
if ($transactional) $this->commit();
}
private function beginTransaction(){
$this->transaction = Yii::app()->getDb()->beginTransaction();
}
private function commit(){
$this->transaction->commit();
}
}
Al final, tus instintos son correctos (re: eso lleva a códigos duplicados en muchos lugares donde necesito transacciones para la acción ). Arme sus modelos para soportar la gran cantidad de necesidades transaccionales que tiene, y permita que el controlador simplemente determine qué punto de entrada (método) usará en su propio contexto.
No, tienes razón. La transacción se delega mediante el método "crear", que es lo que se supone que debe hacer un controlador. Su sugerencia de usar un ''envoltorio'' como beforeAction () es el camino a seguir. Simplemente haga que el controlador extienda o implemente esta clase. Parece que está buscando un patrón de tipo Observer o una implementación similar a la de fábrica.
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 tiene conocimiento de si su interlocutor ya inició una transacción, entonces el modelo tiene que iniciar una transacción de manera condicional , como se muestra en el ejemplo del código en la respuesta de @ Bubba . Los métodos del modelo tienen que aceptar una bandera para que la persona que llama pueda decirle si está permitido iniciar su propia transacción o no. De lo contrario, el modelo debe tener la capacidad de consultar el estado "en una transacción" de su interlocutor.
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 desordenado, y conduce a una gran cantidad de código repetido en los modelos.
También es un ejemplo de Control de acoplamiento , 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 debe saber cuándo importa 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 frameworks como Propel) es hacer beginTransaction()
y commit()
no-ops cuando el DBAL sabe que ya está en una transacción. Pero esto puede provocar anomalías si su modelo intenta comprometerse y descubre que realmente no se compromete. O intenta deshacer y tiene esa solicitud ignorada. 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 para establecer setPrivacy()
es algo que debe comprometerse de inmediato o si es parte de una imagen más grande, una serie más compleja de cambios que involucran múltiples Modelos y solo debe comprometerse si todos estos cambios tienen éxito. Ese es el punto de las transacciones.
Entonces, si los Modelos no saben si pueden o deben comenzar y comprometer su propia transacción, entonces ¿quién lo hace? GRASP incluye un patrón de controlador que no es una clase 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 acerca de si el caso de uso completo es complejo y requiere que se realicen varios cambios en Modelos, dentro de una transacción (o tal vez dentro de varias transacciones).
El ejemplo sobre el que he escrito antes, es decir, iniciar una transacción en el método beforeAction()
de un controlador MVC y confirmarlo en el método afterAction()
, es una simplificación . El Controlador debe poder iniciar y comprometer tantas transacciones como lógicamente requiera para completar la acción actual. O a veces el Controlador podría abstenerse de un control explícito de la transacción y permitir que los Modelos se autocommitan cada cambio.
Pero el punto es que la información sobre qué tranasction (s) son necesarias es algo que los Modelos no saben - tienen que ser contados (en la forma de un parámetro $ transaccional) o si no lo consultan desde su llamador, que tendría que delegar la pregunta de todos modos hasta la acción del Controlador.
También puede crear una Capa de servicio de clases que cada uno sepa cómo ejecutar casos de uso tan complejos y si debe incluir todos los cambios en una sola transacción. De esta forma evitas muchos códigos repetidos. 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.