Транзакции в Laravel

Транзакции в Ларавел

Иногда нужно выполнить ряд SQL-запросов, но они настолько взаимосвязаны между собой, что если у одного из них произойдет сбой, то поломается всё.

Представьте себе такое:

/**
 * Complete the registration of the User
 *
 * @param  \Illuminate\Http\Request $request
 * @return \Illuminate\Http\Response
 */
public function completeRegister(Request $request)
{
    $validated = $request->validate([
        // Куча правил
    ]);

    $user = User::create($validated);

    $this->createDefaultPreferences($user);

    // Получить ключи API из внешней службы через Интернет 
    $keys = ExternalService::getKeysForUser($user->id);

    $user->keys()->create([ 'keys' => $keys ]);

    return response()->view('user-created', ['user' => $user]);
}

И, предположим, что внешняя служба падает и получить ключи невозможно. Из-за этой ошибки пользователь регистрируется без ключей и не может пользоваться сайтом. Теперь нужно получить их вручную или создать механизм для их получения.

Вы могли бы сказать: «Парень, сначала получи ключи, а потом уже создавай пользователя», «сначала пропингуй службу» или «сделай, чтобы ключи были необязательными», — все это абсолютно справедливо, но я просто хочу показать пример, что иногда что-то может пойти не так. Сломаться может даже база данных.

Для подобных проблем и используются транзакции.

Транзакции: сохранение данных одним махом

Транзакции — это невероятная особенностью СУБД SQL, таких как MySQL, MariaDB, PostgreSQL и SQL Server.
Они позволяет базе данных «откатывать» (rollback) целый набор запросов. Тут лучше меня скажет документация:

Вы можете использовать метод transaction фасада DB для запуска набора запросов внутри транзакции базы данных. Если в замыкании транзакции выбрасывается исключение, то транзакция автоматически откатывается. Если замыкание выполняется успешно, то транзакция автоматически фиксируется (commit). Нет необходимости беспокоиться о ручном откате или фиксации при использовании метода transaction.

Чтож, давайте решим нашу проблему с ключами при помощью транзакции.

DB::transaction(function() use ($validated) {

    $user = User::create($validated);

    $this->createDefaultPreferences($user);

    // Получить ключи API из внешней службы
    $keys = ExternalService::getKeysForUser($user->id);

    $user->keys()->create([ 'keys' => $keys ]);

});

Если ExternalService, выполняющий http-запрос, не сможет выполнить то, что должен, то будет выброшено исключение. Когда это произойдет, то вся транзакция будет откачена назад, как будто ничего и не было.

Это происходит потому, что метод transaction использует блок try catch. Если ловится исключение, то делается откат транзакции перед тем, как выдать само исключение.

Но я бы попытался еще X раз!

К счастью, транзакция может принять второй аргумент — количество попыток. Если внешняя служба не подключает из-за большой нагрузки, то мы можем попытаться еще 3 раза.

DB::transaction(function() {

    // ... снова и снова

}, 3);  // Повторить три раза, прежде чем признать неудачу

Что если внешняя служба МЕРТВА?

Предположим, что внешняя служба находится под DDoS-атакой, или кто-то не оплатил счет за электричество, потому что потратил всё на Dota 2, да просто из-за тех.обслуживания или любого другого события, которое блокирует нам доступ к службе. Мы исчерпали все свои попытки, что же делать? Мы можем поймать исключение!

try {
    DB::transaction(function() {

        // ... снова и снова

    }, 3);  // Повторить три раза, прежде чем признать неудачу
} catch (ExternalServiceException $exception) {

    return 'Извините, внешняя служба не работает, а вы не сможете завершить регистрацию без ключей от нее'.

}

Как уже говорилось выше, если транзакция завершится неудачно, то она вернется тот Throwable, который был брошен первым внутри замыкания.

Кстати, вам не нужно использовать блок try catch. Laravel имеет отличную обработку исключений, а методы report и render делают то, что и подразумевают их имена.

Например, если ваша внешняя служба бросает конкретное исключение, то вы можете вывести его так, чтобы пользователю было понятно, например: «Служба не работает, нажмите здесь, чтобы напомнить вам о регистрации, когда всё заработает», и, используя метод report, отправить себе отчёт по по электронной почте и увидеть, что там, черт возьми, произошло.

Так что, да, нет нужды городить в ваших контроллерах полотна кода.

Автор: Italo Baeza
Перевод: Demiurge Ash