Транзакции баз данных являются фундаментальной концепцией систем управления базами данных (СУБД), которые обеспечивают целостность и непротиворечивость данных в многопользовательской среде.

транзакция в MySQL – это последовательная группа операторов, запросов или операций, таких как выбор, вставка, обновление или удалить, чтобы выполнить как одну рабочую единицу, которую можно зафиксировать или отменить.

Транзакции описываются как ACID.

  1. Атомарность. Это свойство гарантирует, что транзакция будет рассматриваться как единая единица по принципу «все или ничего». Если какая-либо часть транзакции терпит неудачу, вся транзакция откатывается.
  2. Непротиворечивость. Транзакции переводят базу данных из одного согласованного состояния в другое. Это означает, что данные должны удовлетворять определенным предопределенным правилам или ограничениям как до, так и после транзакции.
  3. Изоляция. Транзакции выполняются изолированно друг от друга. Это означает, что операции одной транзакции не видны другим транзакциям, пока первая транзакция не будет зафиксирована.
  4. Долговечность. Как только транзакция зафиксирована, ее последствия становятся постоянными и сохранятся при любых последующих системных сбоях или сбоях. Зафиксированные данные хранятся таким образом, что их можно восстановить, даже если система выйдет из строя.

Теперь, если вы хотите использовать транзакции в MySQL, вам нужно хранилище движка InnoDB.

InnoDB является стандартным и наиболее широко используемым механизмом хранения в MySQL. Он обеспечивает ACID-совместимые транзакции, поддержку внешнего ключа и блокировку на уровне строк, что делает его пригодным для широкого круга приложений.

MySQL не поддерживает вложенные транзакции, но движок InnoDB поддерживает точки сохранения.

В Laravel вы можете использовать транзакции Closure DB, как показано ниже.

use Illuminate\Support\Facades\DB;
 
DB::transaction(function () {
    DB::insert('insert on orders');

    DB::update('update users set votes = 1');
 
    DB::delete('delete from posts');
}, 5); // <= This is used to handle Deadlocks, and is the number of tries.

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

Ручная обработка транзакций БД

Запуск транзакции
Чтобы начать транзакцию в Laravel, вы используете метод beginTransaction(), предоставляемый фасадом DB. После запуска транзакции все последующие операции с базой данных, выполняемые с использованием фасада DB или Eloquent ORM, будут включены в эту транзакцию до тех пор, пока она не будет зафиксирована или отменена.

Подтверждение транзакции.
После успешного выполнения всех операций с базой данных в рамках транзакции вы можете зафиксировать изменения в базе данных с помощью метода commit().

Откат транзакции.
Если во время какой-либо операции возникает исключение, вы можете использовать метод rollback() для отмены изменений, внесенных в транзакцию.

Полный пример транзакции БД:

use Illuminate\Support\Facades\DB;

try {
    DB::beginTransaction(); // <= Starting the transaction

    // Update user's balance
    DB::table('users')->where('id', 1)->decrement('balance', 100);
    
    // Insert a new order
    $orderID = DB::table('orders')->insertGetId([
        'user_id' => 1,
        'amount' => 100,
    ]);

    // Insert a new order history
    DB::table('order_history')->insert([
        'order_id' => $orderID,
        'status' => 'pending',
    ]);

    DB::commit(); // <= Commit the changes
} catch (\Exception $e) {
    report($e);
    
    DB::rollBack(); // <= Rollback in case of an exception
}

Как мы упоминали выше, Laravel использует точки сохранения (если поддерживается механизмом БД).

Черта, которая обрабатывает транзакции, находится по адресу: Illuminate/Database/Concerns/ManagesTransactions.php. В этой статье мы рассмотрим только beginTransaction(), commit() и rollBack().

/**
 * Start a new database transaction.
 *
 * @return void
 *
 * @throws \Throwable
 */
public function beginTransaction()
{
    $this->createTransaction(); // <== create transaction

    $this->transactions++;

    $this->transactionsManager?->begin(
        $this->getName(), $this->transactions
    );

    $this->fireConnectionEvent('beganTransaction');
}
/**
 * Create a transaction within the database.
 *
 * @return void
 *
 * @throws \Throwable
 */
protected function createTransaction()
{
    if ($this->transactions == 0) {
        $this->reconnectIfMissingConnection();

        try {
            $this->getPdo()->beginTransaction();
        } catch (Throwable $e) {
            $this->handleBeginTransactionException($e);
        }
    } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
        $this->createSavepoint(); // <= create savepoint
    }
}
/**
 * Commit the active database transaction.
 *
 * @return void
 *
 * @throws \Throwable
 */
public function commit()
{
    if ($this->transactionLevel() == 1) {
        $this->fireConnectionEvent('committing');
        $this->getPdo()->commit(); // <= if it's last transaction commit it
    }

    $this->transactions = max(0, $this->transactions - 1); // <= decrement transaction

    if ($this->afterCommitCallbacksShouldBeExecuted()) {
        $this->transactionsManager?->commit($this->getName());
    }

    $this->fireConnectionEvent('committed');
}
/**
 * Rollback the active database transaction.
 *
 * @param  int|null  $toLevel
 * @return void
 *
 * @throws \Throwable
 */
public function rollBack($toLevel = null)
{
    // We allow developers to rollback to a certain transaction level. We will verify
    // that this given transaction level is valid before attempting to rollback to
    // that level. If it's not we will just return out and not attempt anything.
    $toLevel = is_null($toLevel)
                ? $this->transactions - 1
                : $toLevel;

    if ($toLevel < 0 || $toLevel >= $this->transactions) {
        return;
    }

    // Next, we will actually perform this rollback within this database and fire the
    // rollback event. We will also set the current transaction level to the given
    // level that was passed into this method so it will be right from here out.
    try {
        $this->performRollBack($toLevel);
    } catch (Throwable $e) {
        $this->handleRollBackException($e);
    }

    $this->transactions = $toLevel;

    $this->transactionsManager?->rollback(
        $this->getName(), $this->transactions
    );

    $this->fireConnectionEvent('rollingBack');
}

Если вы посмотрите внимательно, DB::beginTransaction() создаст новую транзакцию, если ее нет или если в настоящее время есть одна существующая транзакция, увеличит ее и создаст точку сохранения (если поддерживается).

С другой стороны, commit() и rollBack() уменьшат счетчик транзакций (точка сохранения) и зафиксируют все запросы, если они будут последними.

Вот как работают вложенные транзакции в Laravel.

Использование транзакций внутри цикла.

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

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

Давайте рассмотрим сценарий, в котором у вас есть цикл, обрабатывающий набор элементов, и вам необходимо выполнить операцию с базой данных для каждого элемента в рамках транзакции.

Что мы делаем, так это проверяем, равно ли количество элемента нулю , и если да, мы пропускаем его обработку и переходим к следующему элементу в массиве. Если он не равен нулю, мы обновляем запас и фиксируем транзакцию.

Приведенный выше код представляет проблему 😱.

Предположим, у нас есть массив из 3 элементов.

$item = [
    [
        'id' => 1,
        'quantity' => 8,
    ],
    [
        'id' => 2,
        'quantity' =>  0,
    ],
    [
        'id' => 3,
        'quantity' =>  2,
    ],
];
  1. При первом входе в цикл мы начинаем новую транзакцию, DB::beginTransaction();, таким образом, счетчик транзакций увеличивается до 1, поскольку количество не равно нулю (элемент с идентификатором => 1), $item[‘quantity’] === 0 мы продолжаем с обновление запаса, а затем мы фиксируем эту транзакцию, используя DB::commit();. Это уменьшает счетчик транзакций до 0.
  2. Теперь переходим к следующему элементу (элемент с id =› 2). Мы начинаем транзакцию, и это увеличивает количество транзакций до 1. Теперь, поскольку количество этого элемента равно нулю, $item[‘quantity’] === 0 мы пропускаем этот шаг и переходим к следующей итерации. Помните, поскольку мы не сделали commit() или rollBack(), количество транзакций по-прежнему равно 1.
  3. Начать обработку следующего элемента (элемент с id =› 3). Мы запускаем транзакцию, и счетчик транзакций увеличивается до 2 (одна для предыдущего элемента и одна для текущего элемента). Это происходит потому, что на предыдущем шаге мы не завершили транзакцию вызовом DB::commit() или DB::rollBack(). После того, как мы проверим количество, которое не равно 0, мы приступим к обновлению запаса, а затем зафиксируем транзакцию, но следующая фиксация не будет выполнять фактическую фиксацию в базе данных, она просто уменьшит счетчик транзакция от 2 => 1.

Если количество транзакций и количество фиксаций/откатов не синхронизированы, никакие изменения в БД вноситься не будут.

Имейте в виду, что каждая открытая транзакция должна быть завершена с помощью commit() или rollback(), особенно внутри циклов.

Не стесняйтесь Подписаться, чтобы получить больше подобного контента 🔔, аплодировать 👏🏻 , комментировать💬 и поделиться статьей с кем хочешь

И, как всегда, я ценю вашу поддержку и спасибо за чтение.