Когда пишешь что-то чуть сложнее «вывести список пользователей», рано или поздно ты сталкиваешься с тем, что база превращается в помойку. Сохранил юзера, но пост не создался. Деньги списались, но подписка не включилась. И дальше начинается весёлое расследование, кто накосячил — кодер, база или вселенная.

На самом деле виноват ты. Потому что забыл про транзакции.

Laravel, к счастью, даёт тебе инструмент под названием DB::transaction(). Это значит: "давай я заверну твои грязные запросы в пакет и либо всё сделаю, либо пошлю тебя нахрен". То есть либо база живёт в согласованном состоянии, либо ничего не меняется. И это единственный нормальный вариант, если не хочешь потом руками чинить разъехавшиеся данные.

Как это работает на самом деле

У Laravel есть метод:

DB::transaction(function () {
    DB::table('users')->update(['votes' => 1]);
    DB::table('posts')->delete();
});

Выглядит невинно. Но в реальности это твоя страховка: если на второй строке у тебя всё взорвётся, то первая тоже откатится.

А если не завернёшь в транзакцию? Получишь классическую картину: у юзеров уже votes = 1, а посты всё ещё на месте. И попробуй потом объяснить, почему бизнес-логика "голоса есть, постов нет" выглядит как глючный криптокошелёк 2017 года.

Когда надо ронять исключения

Иногда ты сам понимаешь, что всё идёт в задницу. Вот тогда нужно кидать исключение — Laravel честно откатит транзакцию.

DB::transaction(function () {
    DB::table('users')->update(['votes' => 1]);
    throw new Exception('Пошло не так — и я не собираюсь это чинить руками');
});

И всё, база осталась чистой. Да, ты облажался, но хотя бы без мусора.

Контроллеры и транзакции

Самый популярная ситуация — регистрация юзера + создание связанного ресурса. Ты ведь не хочешь, чтобы у тебя в таблице валялись "пустые" аккаунты без постов, правда?

public function store(Request $request)
{
    DB::transaction(function () use ($request) {
        $user = User::create($request->all());
        $post = new Post(['title' => 'Первый пост']);
        $user->posts()->save($post);
    });

    return redirect()->route('users.index');
}

Тут всё просто: если не сохранился пост, то и пользователь не появится. И наоборот. Либо вместе, либо никак.

Где тебя подстерегает засада

Идея транзакций проста, но часто про неё забывают. Люди пишут сложные сервисы, где на каждом шаге полсотни запросов к базе, и только потом начинают удивляться: «А что это у нас подписка без оплаты активировалась?». Ответ — потому что ты забыл завернуть код в транзакцию.

Ещё хуже, когда кто-то решает сделать "частичный откат":
— вот здесь сохраним,
— а вот тут посмотрим,
— а если не получится, ну и ладно.

Так база и превращается в рассадник багов.

Финалочка

Транзакции — это не серебряная пуля. Это ремень безопасности. Он не спасёт, если ты поехал 200 км/ч навстречу фуре, но зато не даст вылететь из машины, когда просто врежешься в бордюр.

Laravel сделал их удобными: кинул код в DB::transaction(), и забыл про половину головной боли. Дальше всё зависит от того, хватит ли у тебя мозгов не писать кашу из запросов без этого костыля.