مدیریت تراکنشها در EF Core به روش درست: راهنمای جامع
تراکنش چیست و چرا اهمیت دارد؟
تراکنش مجموعهای از عملیات است که باید به صورت واحد (Atomic) انجام شوند؛ یعنی یا تمام عملیات با موفقیت اجرا میشوند، یا در صورت بروز هرگونه خطا، هیچکدام از آنها اعمال نمیشوند و سیستم به حالت اولیه خود بازمیگردد. این ویژگی با چهار اصل کلیدی به نام ACID تعریف میشود:
-
Atomicity (اتمی بودن): تمام یا هیچ. یک تراکنش به صورت یک واحد کامل اجرا میشود.
-
Consistency (پایداری): تراکنش باید پایگاه داده را از یک حالت پایدار به یک حالت پایدار دیگر منتقل کند و قوانین و محدودیتهای پایگاه داده را نقض نکند.
-
Isolation (انزوا): تراکنشهای همزمان نباید بر روی یکدیگر تأثیر منفی بگذارند. هر تراکنش باید طوری اجرا شود که گویی به تنهایی در سیستم در حال اجراست.
-
Durability (ماندگاری): پس از اینکه تراکنش با موفقیت Commit شد، تغییرات آن باید دائمی باشد و حتی در صورت قطعی برق یا خرابی سیستم از بین نرود.
اهمیت تراکنش زمانی مشخص میشود که چندین عملیات وابسته به هم دارید. برای مثال، در یک سیستم بانکی، انتقال پول از حساب A به حساب B شامل دو عملیات است: کسر مبلغ از حساب A و افزودن همان مبلغ به حساب B. اگر عملیات اول موفق باشد اما عملیات دوم به هر دلیلی (مانند قطعی شبکه) با شکست مواجه شود، پول از حساب اول کم شده اما به حساب دوم واریز نشده است! اینجاست که تراکنش وارد عمل میشود و تضمین میکند که هر دو عملیات با هم موفق شوند یا با هم شکست بخورند.
مدیریت خودکار تراکنشها: SaveChanges
سادهترین و رایجترین روش مدیریت تراکنش در EF Core، استفاده از متد SaveChanges یا SaveChangesAsync است. به طور پیشفرض، هر فراخوانی متد SaveChanges در داخل یک تراکنش محصور میشود.
این یعنی وقتی شما چندین تغییر (افزودن، ویرایش یا حذف موجودیتها) را در DbContext خود انجام میدهید و سپس SaveChanges را فراخوانی میکنید، EF Core به صورت خودکار یک تراکنش را شروع میکند، تمام دستورات SQL مربوط به تغییرات شما را اجرا کرده و در صورت موفقیتآمیز بودن همه آنها، تراکنش را Commit میکند. اگر در حین اجرای هر یک از این دستورات خطایی رخ دهد، تراکنش به صورت خودکار Rollback شده و هیچیک از تغییرات در پایگاه داده اعمال نخواهد شد.
مثال:
public async Task CreateOrder(Order newOrder, OrderItem newItem)
{
// هر دو موجودیت به context اضافه میشوند
_context.Orders.Add(newOrder);
_context.OrderItems.Add(newItem);
// SaveChanges یک تراکنش را شروع کرده و هر دو دستور INSERT را اجرا میکند
// اگر یکی از آنها شکست بخورد، دیگری نیز Rollback میشود
await _context.SaveChangesAsync();
}
این روش برای اکثر سناریوهای رایج کافی و بسیار کارآمد است، زیرا بار مدیریت دستی تراکنش را از دوش توسعهدهنده برمیدارد.
نکته کلیدی: تا زمانی که تمام عملیات شما در یک فراخوانی SaveChanges خلاصه میشود، نیازی به مدیریت دستی تراکنش ندارید. EF Core به بهترین شکل این کار را برای شما انجام میدهد.
مدیریت دستی تراکنشها: BeginTransaction
گاهی اوقات نیاز داریم کنترل بیشتری بر روی محدوده و چرخه حیات یک تراکنش داشته باشیم. این نیاز معمولاً در شرایط زیر به وجود میآید:
-
عملیات چندگانه SaveChanges: زمانی که چندین عملیات SaveChanges باید در قالب یک واحد کاری (Unit of Work) انجام شوند.
-
ترکیب با عملیات خارج از EF Core: وقتی نیاز دارید عملیات EF Core را با سایر عملیات پایگاه داده (مانند استفاده از ADO.NET یا Dapper) در یک تراکنش واحد ترکیب کنید.
-
کنترل سطح انزوا (Isolation Level): برای تعیین دقیقتر نحوه تعامل تراکنشها با یکدیگر.
در این موارد، باید از تراکنشهای صریح یا دستی (Explicit Transactions) استفاده کنیم.

چگونه از تراکنش دستی استفاده کنیم؟
الگوی استفاده از تراکنش دستی بسیار ساده و شبیه به ساختار try-catch-finally است و با استفاده از بلوک using میتوان آن را به شکل ایمن پیادهسازی کرد.
public async Task TransferFunds(int fromAccountId, int toAccountId, decimal amount)
{
// شروع یک تراکنش دستی
using (var transaction = await _context.Database.BeginTransactionAsync())
{
try
{
var fromAccount = await _context.Accounts.FindAsync(fromAccountId);
if (fromAccount == null || fromAccount.Balance < amount)
{
throw new InvalidOperationException("موجودی کافی نیست.");
}
fromAccount.Balance -= amount;
// اولین SaveChanges در محدوده تراکنش
await _context.SaveChangesAsync();
var toAccount = await _context.Accounts.FindAsync(toAccountId);
if (toAccount == null)
{
throw new InvalidOperationException("حساب مقصد یافت نشد.");
}
toAccount.Balance += amount;
// دومین SaveChanges نیز در همان تراکنش
await _context.SaveChangesAsync();
// اگر همه چیز موفقیتآمیز بود، تراکنش را Commit میکنیم
await transaction.CommitAsync();
}
catch (Exception ex)
{
// در صورت بروز هرگونه خطا، تمام تغییرات را Rollback میکنیم
await transaction.RollbackAsync();
// لاگ کردن خطا یا ارسال آن به لایههای بالاتر
throw;
}
}
}
در مثال بالا، هر دو فراخوانی SaveChangesAsync درون یک تراکنش واحد اجرا میشوند. اگر خطایی بین این دو فراخوانی یا در حین اجرای آنها رخ دهد، بلوک catch اجرا شده و transaction.RollbackAsync() تمام تغییرات را به حالت اولیه بازمیگرداند. تنها در صورتی که هر دو عملیات با موفقیت انجام شوند، transaction.CommitAsync() فراخوانی شده و تغییرات در پایگاه داده دائمی میشوند.
سطوح انزوا (Isolation Levels)
سطح انزوا تعیین میکند که یک تراکنش تا چه حد از تغییرات ایجاد شده توسط سایر تراکنشهای همزمان تأثیر میپذیرد. انتخاب سطح انزوای مناسب، مصالحهای بین سازگاری دادهها و عملکرد سیستم است. EF Core به شما اجازه میدهد سطح انزوای مورد نظر خود را هنگام شروع یک تراکنش دستی مشخص کنید.
سطوح انزوای استاندارد عبارتند از:
-
ReadUncommitted: پایینترین سطح. یک تراکنش میتواند دادههایی را بخواند که توسط تراکنش دیگری تغییر یافته ولی هنوز Commit نشدهاند (معروف به Dirty Read). این سطح سریعترین عملکرد را دارد اما کمترین سازگاری را تضمین میکند.
-
ReadCommitted: سطح پیشفرض اکثر پایگاههای داده (مانند SQL Server). یک تراکنش فقط میتواند دادههایی را بخواند که Commit شدهاند. این سطح از Dirty Read جلوگیری میکند.
-
RepeatableRead: تضمین میکند که اگر یک تراکنش دادهای را چندین بار در طول حیات خود بخواند، همیشه نتیجه یکسانی دریافت خواهد کرد (مگر اینکه خودش آن را تغییر دهد). این سطح از Non-Repeatable Read جلوگیری میکند.
-
Serializable: بالاترین و سختگیرانهترین سطح. این سطح تضمین میکند که نتیجه اجرای همزمان تراکنشها با نتیجه اجرای آنها به صورت سریالی (یکی پس از دیگری) یکسان باشد. این سطح از Phantom Read جلوگیری میکند اما میتواند به شدت بر عملکرد و همروندی (Concurrency) تأثیر منفی بگذارد.
-
Snapshot: (مخصوص برخی پایگاههای داده مانند SQL Server) در این سطح، تراکنش یک "عکس فوری" از دادهها در زمان شروع خود میگیرد و تمام عملیات خواندن را بر روی آن نسخه انجام میدهد. این کار مانع از قفل شدن رکوردها برای خواندن میشود و همروندی را به شدت بهبود میبخشد.
چگونه سطح انزوا را تعیین کنیم؟
using (var transaction = await _context.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable))
{
// ... عملیات تراکنش
}
انتخاب درست: همیشه از پایینترین سطح انزوایی استفاده کنید که نیازهای کسبوکار شما را برآورده میکند. برای اکثر برنامهها، سطح پیشفرض ReadCommitted کافی است. تنها زمانی به سراغ سطوح بالاتر بروید که دقیقاً بدانید چرا به آنها نیاز دارید و از تأثیرات عملکردی آن آگاه باشید.
تراکنشهای توزیعشده (Distributed Transactions)
گاهی اوقات نیاز دارید عملیاتی را که چندین منبع داده مختلف (مانند دو پایگاه داده SQL متفاوت، یا یک پایگاه داده و یک Message Queue) را درگیر میکند، در قالب یک تراکنش واحد هماهنگ کنید. این کار از طریق تراکنشهای توزیعشده انجام میشود.
در داتنت، این کار معمولاً با استفاده از TransactionScope انجام میشود. EF Core به خوبی با TransactionScope ادغام میشود.
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
try
{
// عملیات روی context اول
await _context1.Database.SaveChangesAsync();
// عملیات روی context دوم
await _context2.Database.SaveChangesAsync();
// اگر همه چیز موفق بود، تراکنش کامل میشود
scope.Complete();
}
catch (Exception)
{
// در صورت خطا، تراکنش به صورت خودکار Rollback میشود
throw;
}
}
توجه: تراکنشهای توزیعشده پیچیدگی و سربار قابل توجهی دارند و نیازمند یک هماهنگکننده تراکنش مانند MSDTC (Microsoft Distributed Transaction Coordinator) هستند. قبل از استفاده از آنها، مطمئن شوید که جایگزین سادهتری مانند الگوی Saga وجود ندارد.
جمعبندی و بهترین شیوهها
-
به پیشفرض اعتماد کنید: برای ۹۰٪ موارد، تراکنش خودکار SaveChanges کافی، بهینه و ایمن است. تا زمانی که مجبور نشدهاید، از آن استفاده کنید.
-
تراکنشهای دستی برای عملیات پیچیده: تنها زمانی از BeginTransaction استفاده کنید که نیاز به اجرای چندین SaveChanges یا ترکیب عملیات EF Core با کدهای دیگر در یک واحد کاری دارید.
-
از بلوک using استفاده کنید: همیشه IDbContextTransaction را درون یک بلوک using قرار دهید تا از آزادسازی (Dispose) صحیح منابع اطمینان حاصل کنید.
-
کد را در try-catch محصور کنید: برای مدیریت Rollback در صورت بروز خطا، منطق تراکنش خود را همیشه در یک بلوک try-catch بنویسید.
-
سطح انزوا را هوشمندانه انتخاب کنید: با سطح پیشفرض (ReadCommitted) شروع کنید و تنها در صورت نیاز واقعی آن را افزایش دهید. از تأثیرات عملکردی هر سطح آگاه باشید.
-
کوتاه و سریع: تراکنشها، بهویژه آنهایی که سطح انزوای بالایی دارند، میتوانند منابع پایگاه داده را قفل کنند. سعی کنید منطق داخل تراکنش را تا حد امکان کوتاه و سریع نگه دارید و از انجام عملیات طولانی مانند تماسهای شبکهای یا پردازشهای سنگین در داخل آن خودداری کنید.
مدیریت صحیح تراکنشها یکی از ستونهای اصلی ساخت برنامههای قابل اعتماد و پایدار است. EF Core ابزارهای لازم برای انجام این کار را به شکلی ساده و در عین حال قدرتمند در اختیار ما قرار میدهد. با درک عمیق این ابزارها و استفاده از آنها در جای مناسب، میتوانید از یکپارچگی دادههای خود در هر شرایطی اطمینان حاصل کنید.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.