1. معرفی تراکنش‌ها و سازگاری داده‌ها

تراکنش یک واحد منطقی در فرایند اجرای یک سیستم مدیریت پایگاه داده است که شامل یک سری عملیات می‌باشد. این عملیات یا همه موفق می‌شوند یا همه ناکام می‌مانند و به عنوان یک مجموعه ناقسمند در نظر گرفته می‌شوند. ویژگی‌های کلیدی یک تراکنش به صورت ACID می‌تواند خلاصه شود:

  • اتمیت: همه عملیات در یک تراکنش یا به طور کامل انجام می‌شوند یا در کل انجام نمی‌شوند؛ انجام جزئی غیر ممکن است.
  • سازگاری: یک تراکنش باید پایگاه داده را از یک وضعیت سازگار به وضعیت دیگری سازگار منتقل کند.
  • تعزیز: اجرای یک تراکنش باید از مزاحمت توسط تراکنش‌های دیگر محافظت شود و داده‌ها بین تراکنش‌های همزمان چندگانه باید جدا شوند.
  • پایداری: یکباری تراکنش تایید شود، اصلاحات انجام شده توسط آن در پایگاه داده باقی خواهد ماند.

سازگاری داده به حفظ وضعیت داده صحیح و معتبر در یک پایگاه داده پس از یک سری عملیات اشاره دارد. در صورتی که در شرایط دسترسی همزمان یا خرابی‌های سیستم، سازگاری داده واقعاً مهم است و تراکنش‌ها یک مکانیزم را فراهم می‌کنند تا تضمین کنند که سازگاری داده حتی در صورت وقوع خطاها یا تضادها به خطر نیفتد.

2. مروری بر چارچوب ent

ent یک چارچوب موجودیت است که از طریق تولید کدهای برنامه‌نویسی Go، یک API ایمن از نوع را برای عملیات با پایگاه داده فراهم می‌کند. این امکان را فراهم می‌کند که عملیات پایگاه داده به صورت بیشتری حسی و ایمن انجام شود و از این طریق از مشکلات امنیتی مانند تزریق SQL جلوگیری شود. از نظر پردازش تراکنش، چارچوب ent پشتیبانی قوی فراهم می‌کند که برنامه‌نویسان بتوانند عملیات‌های پیچیده تراکنش را با کد مختصر انجام داده و اطمینان حاصل کنند که ویژگی‌های ACID تراکنش رعایت می‌شود.

3. شروع یک تراکنش

3.1 چگونگی شروع یک تراکنش در ent

در چارچوب ent، با استفاده از متد client.Tx، می‌توان به آسانی یک تراکنش جدید را در یک زمینه داده‌شده آغاز نمود که یک شی Tx را بر می‌گرداند. کد نمونه به صورت زیر است:

tx, err := client.Tx(ctx)
if err != nil {
    // اگر خطا در هنگام شروع تراکنش رخ داد، با آن برخورد شود
    return fmt.Errorf("هنگام شروع تراکنش خطایی رخ داد: %w", err)
}
// انجام عملیات‌های بعدی با استفاده از tx...

3.2 انجام عملیات در یک تراکنش

هنگامی که شی Tx با موفقیت ایجاد شود، می‌توان از آن برای انجام عملیات پایگاه داده استفاده نمود. همه عملیات‌های ایجاد، حذف، به‌روزرسانی و پرس‌وجوی انجام شده بر روی شی Tx قسمتی از تراکنش خواهد شد. مثال زیر یک سری عملیات را نشان می‌دهد:

hub, err := tx.Group.
    Create().
    SetName("Github").
    Save(ctx)
if err != nil {
    // اگر یک خطا رخ دهد، تراکنش را برگردانی کنید
    return rollback(tx, fmt.Errorf("خطا در ایجاد گروه: %w", err))
}
// عملیات‌های اضافی می‌تواند اینجا اضافه شود...
// تایید تراکنش
tx.Commit()

4. رسیدگی به خطا و برگشت در تراکنش‌ها

4.1 اهمیت رسیدگی به خطا

در هنگام کار با پایگاه‌های داده، انواع مختلفی از خطاها مانند مسائل شبکه، تضاد داده یا نقض‌های محدودیت می‌توانند در هر زمان اتفاق بیفتند. رسیدگی مناسب به این خطاها امری حیاتی برای حفظ سازگاری داده است. در یک تراکنش، اگر یک عملیات شکست خورد، تراکنش باید به منظور اطمینان از اینکه عملیات‌هایی که تا به نیمه انجام شده‌اند و ممکن است سازگاری پایگاه داده را به خطر بیاندازند، به برگشت درآید.

4.2 چگونگی پیاده‌سازی برگشت

در چارچوب ent، می‌توانید از متد Tx.Rollback() برای برگشت کامل تراکنش استفاده کنید. به طور معمول، یک تابع کمکی با نام rollback تعریف می‌شود تا برگشت و خطاها را رسیدگی نماید، همانطور که در زیر نشان داده شده است:

func rollback(tx *ent.Tx, err error) error {
    if rerr := tx.Rollback(); rerr != nil {
        // اگر برگشت ناموفق باشد، خطای اصلی و خطای برگشت را در کنار هم باز گردانید
        err = fmt.Errorf("%w: خطا در هنگام برگشت تراکنش: %v", err, rerr)
    }
    return err
}

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

5. استفاده از کلاینت تراکنشی

در برنامه‌های عملی، ممکن است سناریوهایی وجود داشته باشد که نیاز داریم کد غیر تراکنشی را به سریع کد تراکنشی تبدیل کنیم. برای اینگونه موارد، می‌توانیم از یک کلاینت تراکنشی استفاده کنیم تا کد را به صورت بی‌درنگ منتقل کنیم. در زیر مثالی از چگونگی تبدیل کد غیر تراکنشی موجود به پشتیبانی از تراکنش‌ها آورده شده است:

// در این مثال، ما تابع اصلی Gen را در یک تراکنش شامل کرده‌ایم.
func WrapGen(ctx context.Context, client *ent.Client) error {
    // ابتدا، یک تراکنش ایجاد کنید
    tx, err := client.Tx(ctx)
    if err != nil {
        return err
    }
    // از کلاینت تراکنشی برای تراکنش استفاده کنید
    txClient := tx.Client()
    // تابع Gen را با استفاده از کلاینت تراکنشی اجرا کنید بدون تغییر کد اصلی Gen
    if err := Gen(ctx, txClient); err != nil {
        // در صورت بروز خطا، تراکنش را بازگردانید
        return rollback(tx, err)
    }
    // در صورت موفقیت، تراکنش را تایید کنید
    return tx.Commit()
}

در کد فوق، از کلاینت تراکنشی tx.Client() استفاده می‌شود که به تابع اصلی Gen اجازه می‌دهد تحت تضمین تراکنش اجرا شود. این رویکرد به ما امکان می‌دهد با تأثیر کمی بر روی منطق اصلی، به راحتی کد غیر تراکنشی موجود را به کد تراکنشی تبدیل کنیم.

6. بهترین روش‌های تراکنش

6.1 مدیریت تراکنش با توابع بازخوانی

وقتی منطق کد ما پیچیده می‌شود و شامل چند عملیات پایگاه داده می‌شود، مدیریت متمرکز این عملیات در یک تراکنش ویژگی بسیار مهمی می‌شود. در زیر مثالی از مدیریت تراکنش از طریق توابع بازخوانی آورده شده است:

func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
    tx, err := client.Tx(ctx)
    if err != nil {
        return err
    }
    // از defer و recover برای رسیدگی به سناریوهای حالت شدید استفاده کنید
    defer func() {
        if v := recover(); v != nil {
            tx.Rollback()
            panic(v)
        }
    }()
    // تابع فراهم شده بازخوانی را فراخوانی کنید تا منطق تجاری اجرا شود
    if err := fn(tx); err != nil {
        // در صورت بروز خطا، تراکنش را بازگردانید
        if rerr := tx.Rollback(); rerr != nil {
            err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr)
        }
        return err
    }
    // اگر منطق تجاری بدون خطا باشد، تراکنش را تایید کنید
    return tx.Commit()
}

با استفاده از تابع WithTx برای پوشش منطق تجاری، می‌توانیم تضمین کنیم که حتی اگر خطاها یا استثناها در داخل منطق تجاری رخ دهد، تراکنش به درستی رسیدگی خواهد شد (با تایید یا بازگردانی).

6.2 استفاده از هوک‌های تراکنش

مانند هوک‌های طرح‌ها و هوک‌های زمان اجرا، ما همچنین می‌توانیم هوک‌ها را در یک تراکنش فعال (Tx) ثبت کنیم که در هنگام Tx.Commit یا Tx.Rollback فعال می‌شود:

func Do(ctx context.Context, client *ent.Client) error {
    tx, err := client.Tx(ctx)
    if err != nil {
        return err
    }
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // منطق قبل از تایید تراکنش
            err := next.Commit(ctx, tx)
            // منطق بعد از تایید تراکنش
            return err
        })
    })
    tx.OnRollback(func(next ent.Rollbacker) ent.Rollbacker {
        return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error {
            // منطق قبل از بازگردانی تراکنش
            err := next.Rollback(ctx, tx)
            // منطق بعد از بازگردانی تراکنش
            return err
        })
    })
    // اجرای منطق تجاری دیگر
    //
    // 
    //
    return err
}

با اضافه کردن هوک‌ها در هنگام تایید و بازگردانی تراکنش، می‌توانیم منطق اضافی مانند ورودی یا پاک‌سازی منابع را رسیدگی کنیم.

7. درک سطوح مختلف عزل تراکنش

در یک سیستم پایگاه داده، تنظیم سطح عزل تراکنش برای جلوگیری از مشکلات همزمانی مختلف (مانند خواندن داده‌های غیرقطعی، خواندن غیرتکراری و خیالی) بسیار حیاتی است. در اینجا چندین سطح عزل استاندارد و نحوه تنظیم آن‌ها در چارچوب ent آورده شده است:

  • READ UNCOMMITTED: پایین‌ترین سطح، اجازه می‌دهد داده‌هایی که هنوز تأیید نشده‌اند را بخواند که ممکن است منجر به خواندن داده‌های غیرقطعی، غیرتکراری و خیالی شود.
  • READ COMMITTED: اجازه می‌دهد داده‌ها را بخواند و تأیید کند، جلوگیری از خواندن داده‌های غیرقطعی، اما می‌تواند باعث شود خواندن‌های غیرتکراری و خیالی رخ دهد.
  • REPEATABLE READ: اطمینان می‌دهد که خواندن دوبار یکسان داده در یک تراکنش تکراری نتایج پایداری تولید می‌کند که جلوگیری از خواندن‌های غیرتکراری دارد، اما ممکن است باعث شود خواندن‌های خیالی رخ دهد.
  • SERIALIZABLE: سخت‌گیرانه‌ترین سطح عزل، سعی می‌کند با قفل کردن داده‌های مرتبط جلوی خواندن‌های غیرقطعی، غیرتکراری و خیالی را بگیرد.

در ent، اگر درایور پایگاه داده امکان تنظیم سطح عزل تراکنش را داشته باشد، می‌توان آن را به‌صورت زیر تنظیم کرد:

// تنظیم سطح عزل تراکنش به تکراری خواندن
tx, err := client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})

درک سطوح عزل تراکنش و کاربرد آن‌ها در پایگاه داده‌ها بسیار حیاتی است برای اطمینان از هماهنگی داده و پایداری سیستم. توسعه‌دهندگان باید بر اساس نیازهای خاص برنامه، یک سطح عزل مناسب را انتخاب کنند تا بهترین روش برای اطمینان از امنیت داده و بهینه‌سازی عملکرد را داشته باشند.