Делает ли управление транзакциями в плохой практике контроллера?
Я работаю над PHP/MySQL-приложением, используя структуру Yii.
Я столкнулся со следующей ситуацией:
В моем VideoController
у меня есть actionCreate
, который создает новое видео и actionPrivacy
, которое устанавливает конфиденциальность в Видео. Проблема в том, что во время actionCreate
вызывается метод setPrivacy
модели Video
, который в настоящее время имеет транзакцию. Я хотел бы, чтобы создание видео также было в транзакции, что приводит к ошибке, так как транзакция уже активна.
В комментарии этот ответ, пишет Билл Карвин
Таким образом, нет необходимости делать классы классов домена или классы DAO транзакции - просто сделайте это на уровне контроллера
и этот ответ:
Поскольку вы используете PHP, объем ваших транзакций не более один запрос. Таким образом, вы должны просто использовать транзакции, управляемые контейнерами, не транса. То есть, начните транзакцию с самого начала обработки запроса и фиксации (или откат) по завершении обрабатывая запрос.
Если я управляю транзакциями в контроллере, у меня будет куча кода, который выглядит так:
public function actionCreate() {
$trans = Yii::app()->getDb()->beginTransaction();
...action code...
$trans->commit();
}
Это приводит к дублированию кода во многих местах, где мне нужны транзакции для действия.
Или я мог бы реорганизовать его в методы beforeAction()
и afterAction()
родительского класса Controller
, который затем автоматически создавал транзакции для каждого выполняемого действия.
Были ли проблемы с этим методом? Что такое хорошая практика для управления транзакциями для приложения PHP?
Ответы
Ответ 1
Причина, по которой я говорю, что транзакции не относятся к слою модели, в основном такова:
Модели могут вызывать методы в других моделях.
Если модель пытается начать транзакцию, но она не знает, начал ли ее вызывающий транзакцию транзакцией, тогда модель должна условно начать транзакцию, как показано в примере кода в @Bubba. Методы модели должны принимать флаг, чтобы вызывающий мог сказать ему, разрешено ли ему начать свою собственную транзакцию или нет. Или же модель должна иметь возможность запросить свой вызывающий объект в состоянии транзакции.
public function setPrivacy($privacy, $caller){
if (! $caller->isInTransaction() ) $this->beginTransaction();
$this->privacy = $privacy;
// ...action code..
if (! $caller->isInTransaction() ) $this->commit();
}
Что делать, если вызывающий объект не является объектом? В PHP это может быть статический метод или просто не-объектно-ориентированный код. Это становится очень грязным и приводит к большому количеству повторяющихся кодов в моделях.
Это также пример Control Coupling, который считается неудачным, потому что вызывающий должен знать что-то о внутренних функциях вызываемого объекта, Например, некоторые из методов вашей модели могут иметь параметр транзакции $, но другие методы могут не иметь этого параметра. Как зонд должен знать, когда параметр имеет значение?
// I need to override method attempt to commit
$video->setPrivacy($privacy, false);
// But I have no idea if this method might attempt to commit
$video->setFormat($format);
Другое решение, которое я видел (или даже реализованное в некоторых фреймворках, таких как Propel), заключается в том, чтобы сделать beginTransaction()
и commit()
no-ops, когда DBAL знает об этом уже в транзакции. Но это может привести к аномалиям, если ваша модель пытается зафиксировать и обнаруживает, что ее на самом деле не совершает. Или пытается откат, и этот запрос игнорируется. Я уже писал об этих аномалиях.
Компромисс, который я предложил, заключается в том, что Модели не знают о транзакциях. Модель не знает, является ли ее запрос на setPrivacy()
тем, что он должен совершить немедленно или является частью более крупного изображения, более сложной серии изменений, которые связаны с несколькими моделями, и должны быть совершены только в том случае, если все эти изменения будут успешными. Это точка транзакций.
Итак, если модели не знают, могут ли они или должны начинаться и совершать собственную транзакцию, то кто это делает? GRASP включает шаблон контроллера, который является не-UI-классом для прецедента, и ему назначается ответственность за создание и контроль всех частей для выполнения этого варианта использования. Контроллеры знают о транзакциях, поскольку доступ к информации о том, является ли полный вариант использования сложным, требует нескольких изменений, которые должны выполняться в моделях, в течение одной транзакции (или, возможно, нескольких транзакций).
Пример, о котором я писал ранее, то есть начать транзакцию в методе beforeAction()
контроллера MVC и зафиксировать его в методе afterAction()
, это упрощение. Контроллер должен иметь возможность запускать и фиксировать столько транзакций, сколько логически требуется для завершения текущего действия. Или иногда Контроллер может воздерживаться от явного управления транзакциями и разрешать авторам автоматически изменять каждое изменение.
Но дело в том, что информация о том, какие трансакции нужны, - это то, что Модели не знают - им нужно сказать (в виде $transactional parameter) или запросить его у своих вызывающий, который в любом случае должен был делегировать вопрос вплоть до действия контроллера.
Вы также можете создать уровень обслуживания классов, каждый из которых знает, как выполнять такие сложные варианты использования, и включать ли все изменения в одной транзакции. Таким образом, вы избегаете много повторного кода. Но для PHP-приложений нередко есть отдельный уровень обслуживания; действие контроллера обычно совпадает с уровнем обслуживания.
Ответ 2
Лучшая практика: Поместите транзакции в модель, не помещайте транзакции в контроллер.
Основным преимуществом шаблона проектирования MVC является следующее: MVC делает классы моделей повторно используемыми без изменений. Упростите техническое обслуживание и внедрение новых функций.
Например, предположительно вы в первую очередь разрабатываете браузер, где пользователь вводит один набор данных за раз, и вы перемещаете манипуляции с данными в контроллер. Позже вы понимаете, что вам необходимо поддерживать возможность загрузки большого количества коллекций данных, которые будут импортированы на сервере из командной строки.
Если все манипуляции с данными были в модели, вы могли бы просто сломать данные и передать их в обрабатываемую модель. Если в контроллере есть необходимая (транзакционная) функциональность, вам придется реплицировать это в свой CLI script.
С другой стороны, возможно, вы получаете другой контроллер, который должен выполнять одну и ту же функциональность, с другой точки. Вам также понадобится повторить код в этом другом контроллере.
С этой целью вам просто нужно решить проблемы транзакций в модели.
Предполагая, что у вас есть класс (модель) Video с методом setPrivacy(), который уже имеет транзакционную сборку; и вы хотите вызвать его из другого метода persist(), который также должен обернуть свою функциональность в транзакции большего размера, вы можете просто изменить setPrivacy() для выполнения условной транзакции.
Возможно, что-то вроде этого.
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();
}
}
В конце концов, ваши инстинкты верны (re: Это приводит к дублированию кода во многих местах, где мне нужны транзакции для действия.). Архитектор ваших моделей для поддержки множества транзакционных потребностей, которые у вас есть, и пусть контроллер просто определит, какую точку входа (метод) он будет использовать в своем собственном контексте.
Ответ 3
Нет, вы правы. Транзакция делегируется методом "create", который должен выполнять контроллер. Ваше предложение использовать "обертку", как beforeAction(), - это путь. Просто заставьте контроллер расширить или реализовать этот класс. Похоже, вы ищете шаблон типа Observer или реализацию factory.
Ответ 4
Ну, один из недостатков этих широких транзакций (по всему запросу) заключается в том, что вы ограничиваете возможности concurrency вашего механизма базы данных, а также увеличиваете вероятность блокировок. С этой точки зрения, это может окупиться, чтобы делать транзакции только там, где они вам нужны, и позволить им покрывать только тот код, который должен быть покрыт.
Если возможно, я бы определенно пошел на размещение транзакции в моделях. Проблема с перекрывающимися транзакциями может быть решена путем введения BaseModel (предков всех моделей) и переменной transactionLock в этой модели. Затем вы просто переносите свои директивы транзакции begin/commit в методы BaseModel, которые уважают эту переменную.