آموزش کار با ترنزکشن (Transaction) در لاراول

Learning how to work with Transaction in Laravel

23 شهریور 1402
Learning how to work with Transaction in Laravel

تراکنش های پایگاه داده یا Database Transaction یک مفهوم اساسی در سیستم های مدیریت پایگاه داده (DBMS) است که یکپارچگی و سازگاری داده ها را در یک محیط چند کاربره تضمین می کند. در این مقاله میخواهیم درباره ترنزکشن در لاراول صحبت کنیم.

همینطور به شما توصیه می کنیم برای یادگیری صفر تا صد لاراول این دوره را مشاهده بفرمایید.

Transaction یا تراکنش یا ترنزکشن در MySQL یک گروه متوالی از عبارات، کوئری ها یا عملیاتی مانند SELECT، INSERT، UPDATE یا DELETE است تا به عنوان یک واحد کاری انجام شود.

تراکنش ها یا Transaction ها به عنوان ساختار ACID شرح داده می شوند. در ادامه مخفف عبارت ACID را توضیح می دهیم:

  1. Atomic (اتمی بودن): این ویژگی تضمین می کند که یک تراکنش به عنوان یک واحد، همه یا هیچ در نظر گرفته می شود. یعنی ابتدا تا انتهای انجام یک تراکنش در دیتابیس باید مشخص باشد و هیچ عملیاتی به صورت نصفه و نیمه ارائه نمی‌شود، بنابراین اگر هر بخشی از تراکنش با شکست مواجه شود، کل تراکنش برگردانده می شود یا به اصلاح Rollback می‌شود.
  2. 2. Consistency (سازگاری): تراکنش های پایگاه داده را از یک حالت سازگار به حالت دیگر تبدیل می کنند. به این معنی که داده ها باید قوانین یا محدودیت های از پیش تعریف شده خاصی را قبل و بعد از تراکنش رعایت کنند.
  3. 3. Isolation (ایزوله سازی): تراکنش ها جدا از یکدیگر انجام می شوند. به این معنی که عملیات یک تراکنش تا زمانی که اولین تراکنش انجام نشود برای سایر تراکنش ها قابل مشاهده نیست. بخواهیم بهتر توضیح بدهیم: هیچ تراکنشی از عملیات های سایر تراکنش ها مطلع نیست.
  4. 4. Durability (دوام): هنگامی که یک تراکنش انجام می شود، اثرات آن دائمی است و از هر گونه خرابی یا خرابی های بعدی سیستم محفوظ می ماند. داده ها به گونه ای ذخیره می شوند که حتی در صورت از کار افتادن سیستم قابل بازیابی هستند.

اکنون اگر می خواهید از تراکنش ها در MySQL استفاده کنید، به ذخیره سازی موتور InnoDB نیاز دارید.

InnoDB موتور پیش فرض و پرکاربردترین موتور ذخیره سازی در MySQL است. این  موتور تراکنش های سازگار با ACID، پشتیبانی از کلید خارجی و قفل در سطح ردیف را فراهم می کند و آن را برای طیف گسترده ای از برنامه ها مناسب می کند.

MySQL از تراکنش های تو در تو پشتیبانی نمی کند، اما موتور InnoDB از نقاط ذخیره مختلف پشتیبانی می کند.

با استفاده از DB Clouser ها می توانید ترنزکشن در لاراول را به صورت زیر پیاده سازی کنید.

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.

با استفاده از این روش، نیازی به نگرانی در مورد شروع تراکنش، commit کردن و بازگشت در صورت بروز مشکل ندارید، همه این کارها را به صورت خودکار انجام می دهد. شما همچنین می توانید به عنوان آرگومان دوم تعداد دفعات تکرار را که تعداد دفعاتی که یک تراکنش باید در زمان شکست در اجرا، انجام شود را مشخص کنید.

مدیریت DB Transaction در لاراول

شروع یک ترنزکشن در لاراول

برای شروع یک ترنزکشن در لاراول، از متد ()beginTransaction ارائه شده توسط ویوی DB استفاده کنید. هنگامی که یک تراکنش شروع شد، تمام عملیات های بعدی پایگاه داده که با استفاده از ویوی DB یا Eloquent ORM اجرا می شوند، در آن تراکنش گنجانده می شوند تا زمانی که commit شود یا برگشت داده شود.

انجام ترنزکشن در لاراول

پس از اجرای موفقیت آمیز تمامی عملیات پایگاه داده در یک تراکنش، می توانید با استفاده از متد ()commit تغییرات را در پایگاه داده انجام دهید.

بازگرداندن یک ترنزکشن در لاراول

اگر در طول هر عملیاتی استثنا رخ دهد، می توانید از متد ()rollback برای بی تاثیر کردن تغییرات ایجاد شده در تراکنش استفاده کنید.

نمونه کامل DB Transaction در لاراول:

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
}

همانطور که در بالا ذکر کردیم، لاراول از نقاط ذخیره یا save point ها (در صورت پشتیبانی توسط موتور دیتابیس) استفاده می کند. ویژگی ای که تراکنش ها را مدیریت می کند در آدرس زیر قرار دارد: Illuminate/Database/Concerns/ManagesTransactions.php. در این مقاله، ما فقط به ()startTransaction()، 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 تعداد تراکنش ها (savepoint) را کاهش می دهند و اگر آخرین مورد باشد، تمام کوئری ها را انجام می دهند.

تراکنش های تو در تو در لاراول اینگونه عمل می کنند

استفاده از Transaction لاراول داخل یک حلقه

استفاده از تراکنش های درون یک حلقه می تواند کمی پیچیده تر از استفاده از تراکنش ها برای عملیات های تکی باشد. هنگامی که با حلقه ها و تراکنش ها سر و کار دارید، باید مرزهای تراکنش را به دقت مدیریت کنید تا از ثبات و عملکرد داده ها اطمینان حاصل کنید.

بیایید حالتی را در نظر بگیریم که در آن شما یک حلقه دارید که مجموعه‌ای از آیتم‌ها را پردازش می‌کند و باید یک عملیات پایگاه داده برای هر آیتم در یک تراکنش انجام دهید.

کاری که ما انجام می دهیم این است که بررسی کنیم که آیا مقدار یک آیتم صفر است یا خیر، و اگر این طور است، از پردازش آن صرف نظر می کنیم و به آیتم بعدی در آرایه می رویم. اگر صفر نباشد، stock را تغییر می دهیم و تراکنش را انجام می دهیم.

اشتباه ترنزکشن در لاراول و حلقه‌ها

کد بالا یک مشکل را نشان می دهد. :(

فرض کنید یک آرایه با 3 عنصر داریم.

$item = [
    [
        'id' => 1,
        'quantity' => 8,
    ],
    [
        'id' => 2,
        'quantity' =>  0,
    ],
    [
        'id' => 3,
        'quantity' =>  2,
    ],
];

  • اولین باری که وارد حلقه می شویم، یک تراکنش جدید شروع می کنیم، DB::beginTransaction(); بنابراین این تعداد تراکنش را به 1 افزایش می دهد، زیرا مقدار آن صفر نیست (آیتم با id => 1$item[‘quantity’] === 0 . ما به به تغییر stock ادامه می دهیم و سپس این تراکنش را با استفاده از آن انجام می دهیم. DB::commit();. این تعداد تراکنش را به 0 کاهش می دهد.
  • حالا به سراغ آیتم بعدی می رویم (مورد با id => 2). ما تراکنش را شروع می کنیم و این تعداد تراکنش را به 1 افزایش می دهد. حالا چون مقدار این آیتم صفر است، $item[‘quantity’] === 0 از این مرحله می گذریم و به تکرار بعدی می رویم. به یاد داشته باشید، چون ما ()commit یا ()rollback را انجام ندادیم، تعداد تراکنش ها هنوز 1 است.
  • پردازش آیتم بعدی را شروع کنید (آیتم با id => 3). تراکنش را شروع می کنیم و این تعداد تراکنش را به 2 افزایش می دهد (یکی برای آیتم قبلی و یکی برای آیتم فعلی). این حالت به این دلیل اتفاق می‌افتد که در مرحله قبل، تراکنش را با فراخوانی ()DB::commit یا ()DB::rollBack تکمیل نکردیم. پس از بررسی مقداری که 0 نیست، تغییر stock و سپس انجام تراکنش ادامه می دهیم، اما commit تحت عنوان یک commit واقعی برای پایگاه داده انجام نمی شود، فقط counter تراکنش را از 2 به 1 کاهش می دهد.

هنگامی که تعداد تراکنش و تعداد commit/rollback برابر نشدند، هیچ تغییری در DB ایجاد نخواهد شد.

به خاطر داشته باشید که هر تراکنش باز شده باید با یک ()commit یا ()rollback، به خصوص در حلقه ها انجام شود.

رفع اشکال تراکنش لاراول در یک ناسازگاری حلقه
رفع اشکال تراکنش در یک ناسازگاری حلقه

جمع بندی

با توجه به مطالب ارائه شده در این مقاله، نکات کلیدی در مورد مدیریت ترنزکشن در لاراول به شرح زیر است:

  • تراکنش‌های پایگاه داده برای حفظ یکپارچگی و سازگاری داده‌ها در محیط‌های چندکاربره بسیار مهم هستند. لاراول روش‌های آسانی برای مدیریت تراکنش‌ها با استفاده از توابع DB فراهم کرده است.
  • برای شروع یک تراکنش از تابع DB::beginTransaction() استفاده می‌شود. تمام عملیات‌های بعدی پایگاه داده تا زمان commit یا rollback شدن، بخشی از آن تراکنش خواهند بود.
  • برای اعمال تغییرات تراکنش به پایگاه داده از تابع DB::commit() و برای باطل کردن تغییرات از DB::rollback() استفاده می‌شود.
  • تراکنش‌ها در لاراول مدل ACID (اتمی‌بودن، سازگاری، ایزوله سازی و دوام) را دنبال می‌کنند. موتور پایگاه داده باید از تراکنش‌ها پشتیبانی کند تا این ویژگی کار کند.
  • هنگام کار با حلقه‌ها، باید مراقب بود که هر تراکنش به درستی commit یا rollback شود، در غیر این صورت تراکنش‌های باز می‌توانند مشکل‌آفرین شوند.
  • لاراول تراکنش‌های تودرتو را به صورت خودکار با استفاده از نقاط ذخیره (savepoint) مدیریت می‌کند.
  • در مجموع، لاراول API ساده و شفافی برای مدیریت تراکنش‌های پایگاه داده فراهم کرده است. مدیریت صحیح تراکنش‌ها برای ساخت برنامه‌های قدرتمند پایگاه داده محور در لاراول بسیار مهم است.

نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.