Where to place transactions?

Is there a general recommendation regarding where to place transactions? E.g. only in controllersl, only in models…

Currently, I have this example situation: If I create one item (let’s say ‘car’), in the overriden afterSave method some other related items will be created as well (‘wheels’). Of course, I’d like to do this in a transaction. Now, I myself know that an transaction is needed and I would put it in the controller.

However, the needed transaction for the model is not obvious. So ideally I could override save (of ‘car’) as well and start the transaction there, instead of in the controller. Another developer could just use save and be sure that the wheels will be created as well. But then again it is not obvious that save uses an transaction internally. The other developer that uses the model could start a transaction in the controller before, so two transactions would run then? I don’t know what happens then but I think it shouldn’t be like that.

How could I deal with transactions (in Yii 2)? Is there a general straight forward rule so I never get a problem anymore? Or is method/class documentation the key?

I’ve used this sort of pattern in models in Yii 1.1 before:




public function doSomething()

{

    $transaction = self::model()->getDbConnection()->getCurrentTransaction();

    $inExternalTransaction = $transaction !== null;


    // Start a new transaction if one is not active.

    if (!$inExternalTransaction)

        $transaction = self::model()->getDbConnection()->beginTransaction();


    try

    {

        //

        // Attempt your updates here. If they fail, throw an exception.

        //


        // Commit if the transaction was created in this method.

        if (!$inExternalTransaction)

            $transaction->commit();

    }

    catch (Exception $ex)

    {

        // Roll back if the transaction was created in this method.

        if (!$inExternalTransaction)

            $transaction->rollback();


        throw $ex; // Rethrow the exception so calling code knows saving failed.

    }

}



Your calling code can choose whether to start its own transaction or just let the single method run in a transaction. You’ll need to wrap it up in a try-catch block as an exception is thrown if saving fails, regardless of whether a transaction was already running or not.

I suspect that a modified version of this pattern would work in Yii 2, although there might be a better way now.

Thanks for your inspiration. This will surely work, though it bloats code a bit.

If you extended the transaction class and used it throughout your application, you might be able to make it so that it only ever starts one transaction, but counts up every time a request to begin a transaction is made. Each time a commit or roll back request is sent, the transaction class decrements the counter until it hits 0, at which point it performs a commit or roll back as requested.

You could avoid a bit of the boilerplate code then, and just use:




public function doSomething()

{

    // This line would either use an existing transaction (and update a counter) or create a new one.

    $transaction = self::model()->getDbConnection()->beginTransaction();


    try

    {

        //

        // Attempt your updates here. If they fail, throw an exception.

        //


        // Commit only succeeds if new transaction created above. Otherwise counter decremented.

        $transaction->commit(); 

    }

    catch (Exception $ex)

    {

        // Roll back only succeeds if new transaction created above. Otherwise counter decremented.

        $transaction->rollback();


        throw $ex; // Rethrow the exception so calling code knows saving failed.

    }

}



I’m sure there’d be some edge cases to consider, but this would integrate nicely with the existing framework code. You’d just need to update the database connection config to use your customised transaction class.

Hmm… could be part of Yii itself. Thanks again.