Подтвердить что ты не робот

Делает ли управление транзакциями в плохой практике контроллера?

Я работаю над 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?

4b9b3361

Ответ 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, которые уважают эту переменную.