عنوان:

‫مدیریت رفتارهای حذف در EF Core و انطباق آن با طراحی دامنه محور (DDD)


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۲/۳۰ ۱۰:۳۲
آدرس: www.dntips.ir
درک چگونگی مدیریت رفتارهای حذف و همسویی آن‌ها با مدل دامنه از اهمیت بالایی برخوردار است. این مقاله به بررسی نحوه عملکرد رفتارهای حذف در EF Core و ارتباط این گزینه‌ها با اصول DDD، به‌ویژه هنگام کار با مجموعه‌ها (Aggregates)، می‌پردازد. هدف از این بررسی، ارائه راهنمایی جامع برای توسعه‌دهندگان دات‌نت جهت اتخاذ استراتژی‌های حذف مناسب و منطبق با معماری نرم‌افزارشان است.

رفتارهای حذف در EF Core: یک مرور سریع
EF Core مجموعه‌ای از رفتارهای حذف را ارائه می‌دهد که تعیین می‌کنند هنگام حذف یک موجودیت اصلی (Principal Entity)، چه اتفاقی برای موجودیت‌های وابسته (Dependent Entities) رخ دهد. این رفتارها از طریق رابط برنامه‌نویسی Fluent API در متد OnModelCreating پیکربندی می‌شوند. درک این رفتارها برای حفظ یکپارچگی داده‌ها و اعمال منطق کسب‌وکار در سطح پایگاه داده ضروری است.

رفتارهای حذف کلیدی در EF Core عبارتند از:
  • حذف آبشاری (Cascade Deletes): هنگام حذف موجودیت اصلی، موجودیت‌های وابسته به‌طور خودکار حذف می‌شوند. این رفتار برای سناریوهایی مناسب است که موجودیت وابسته بدون موجودیت اصلی معنایی ندارد و چرخه حیات آن کاملاً به موجودیت اصلی گره خورده است.
  • محدود کردن (Restrict): از حذف موجودیت اصلی در صورتی که موجودیت‌های وابسته به آن وجود داشته باشند، جلوگیری می‌کند. این رفتار زمانی مفید است که حفظ موجودیت‌های وابسته پس از حذف احتمالی موجودیت اصلی ضروری باشد.
  • تهی‌سازی (SetNull): هنگام حذف موجودیت اصلی، کلید خارجی (Foreign Key | FK) در موجودیت‌های وابسته به مقدار null تنظیم می‌شود. این رفتار برای روابط اختیاری که در آن موجودیت وابسته می‌تواند بدون وجود موجودیت اصلی نیز وجود داشته باشد، مناسب است.
  • بدون اقدام (NoAction): رفتاری مشابه با Restrict دارد، اما رفتار نهایی ممکن است به ارائه‌دهنده پایگاه داده (Database Provider) بستگی داشته باشد.
  • تهی‌سازی در سمت کلاینت (ClientSetNull): کلید خارجی را در حافظه (برای روابط اختیاری) به مقدار null تنظیم می‌کند. این رفتار بیشتر در سناریوهایی کاربرد دارد که بررسی وجود موجودیت‌های وابسته در سطح پایگاه داده مطلوب نیست.

نحوه پیکربندی این رفتارها در متد OnModelCreating به شکل زیر است:
modelBuilder.Entity<Order>()
    .HasMany(o => o.OrderItems)
    .WithOne(i => i.Order)
    .HasForeignKey(i => i.OrderId)
    .OnDelete(DeleteBehavior.Cascade);
در این مثال، هنگام حذف یک شیء Order، تمام اشیاء OrderItem مرتبط با آن نیز به‌طور خودکار حذف خواهند شد.

مروری بر طراحی دامنه محور (DDD)
در DDD، واحد اصلی ساختاردهی، مجموعه (Aggregate) است. یک مجموعه، خوشه‌ای از اشیاء دامنه است که به عنوان یک واحد واحد در نظر گرفته می‌شوند. یکی از موجودیت‌های درون مجموعه به عنوان ریشه مجموعه (Aggregate Root) شناخته می‌شود و تمام تغییرات در درون مجموعه باید از طریق این ریشه اعمال شود. ریشه مجموعه، قواعد ثابت (Invariants) را اعمال کرده و رفتار فرزندان خود را کپسوله می‌کند.
به عنوان مثال، در یک سیستم تجارت الکترونیک، مجموعه Order می‌تواند شامل موجودیت ریشه Order و موجودیت‌های فرعی مانند OrderItem باشد که متعلق به Order هستند. در این ساختار، حذف یک Order باید منجر به حذف OrderItems مرتبط شود، اما این حذف باید بر اساس قوانین دامنه انجام شود، نه صرفاً از طریق عملیات خام EF Core.

DeleteBehavior.Cascadeو DDD
انتخاب رفتار حذف Cascade در DDD باید با دقت صورت گیرد.

چه زمانی از حذف آبشاری استفاده کنیم؟
  • زمانی که موجودیت فرزند بدون موجودیت والد هیچ وجود مستقلی نداشته باشد.
  • زمانی که موجودیت فرزند به‌طور کامل درون مجموعه کپسوله شده باشد.
  • زمانی که هیچ قانون دامنه یا اثر جانبی برای در نظر گرفتن در طول حذف وجود نداشته باشد.
این موارد به‌خوبی با اصول DDD همخوانی دارند، زیرا ریشه مجموعه (Order) مالک و کنترل‌کننده چرخه حیات فرزندان خود (OrderItems) است. حذف ریشه، حذف اجزای آن را توجیه می‌کند. یک قاعده سرانگشتی این است که اگر موجودیت فرزند مانند یک شیء ارزشی (Value Object-like Entity) باشد (یعنی هیچ چرخه حیاتی خارج از والد نداشته باشد)، حذف آبشاری قابل قبول است.

چه زمانی از حذف آبشاری استفاده نکنیم؟
  • زمانی که موجودیت فرزند توسط چندین مجموعه به اشتراک گذاشته می‌شود.
  • زمانی که در طول حذف نیاز به انتشار یک رویداد دامنه (Domain Event)، ثبت فعالیت (Logging)، یا انجام اثرات جانبی دیگری وجود دارد.
  • زمانی که از حذف نرم (Soft Deletes) یا استراتژی‌های بایگانی (Archival Strategies) استفاده می‌کنید.
  • زمانی که می‌خواهید سابقه رکورد را برای اهداف حسابرسی (Audit) یا تاریخچه حفظ کنید.

در این موارد، منطق دامنه باید به‌طور صریح حذف را مدیریت کند. ممکن است بخواهید از DeleteBehavior.Restrict برای جلوگیری از حذف خودکار توسط EF Core استفاده کنید.
modelBuilder.Entity<Invoice>()
    .HasMany(i => i.Payments)
    .WithOne(p => p.Invoice)
    .OnDelete(DeleteBehavior.Restrict); // اعمال مسئولیت دامنه

استراتژی حذف سازگار با DDD
به جای تکیه بر رفتار حذف EF Core، اجازه دهید ریشه مجموعه کنترل حذف را در دست بگیرد. برای مثال:
public class Order : AggregateRoot
{
    private readonly List<OrderItem> _items = new();

    public void Cancel()
    {
        if (_items.Any(i => i.IsShipped))
            throw new InvalidOperationException("Cannot delete an order with shipped items.");
        // انتشار رویداد دامنه
        this.AddDomainEvent(new OrderCancelledEvent(this.Id));

        _items.Clear(); // یا علامت‌گذاری به عنوان حذف شده
    }
}
سپس در لایه کاربرد (Application Layer)، یک متد repository را فراخوانی کنید:
var order = _orderRepository.GetById(id);
order.Cancel();
_orderRepository.Delete(order);
این رویکرد به مرزهای DDD احترام می‌گذارد و رفتار را درون دامنه حفظ می‌کند.

حذف نرم و EF Core
اگر دامنه شما به حذف نرم نیاز دارد (علامت‌گذاری موجودیت‌ها به عنوان حذف شده بدون حذف فیزیکی آن‌ها)، حذف آبشاری آن‌طور که انتظار می‌رود کار نخواهد کرد. در این سناریو، از یک فیلد IsDeleted استفاده کنید و EF Core را برای اعمال یک فیلتر سراسری پرس و جو (Global Query Filter) پیکربندی کنید:
modelBuilder.Entity<Order>()
    .HasQueryFilter(o => !o.IsDeleted);
و منطق حذف را در مدل دامنه مدیریت کنید:
public void MarkAsDeleted() => IsDeleted = true;

نتیجه‌گیری
انتخاب دقیق رفتارهای حذف در EF Core و همسو کردن آن‌ها با مدل دامنه، امری حیاتی برای اطمینان از یکپارچگی داده‌ها، قابلیت نگهداری سیستم و پایبندی واقعی به اصول طراحی دامنه محور است.
  • موجودیت‌های ساده و متعلق: استفاده از Cascade مناسب است.
  • منطق دامنه پیچیده: از Cascade اجتناب کنید و از متدهای دامنه استفاده کنید.
  • موجودیت‌های به اشتراک گذاشته شده: از Cascade اجتناب کنید و Restrict را ترجیح دهید.
  • حذف نرم: از Cascade اجتناب کنید و از فیلد IsDeleted استفاده کنید.
  • نیاز به رویدادهای دامنه: از حذف صریح در لایه دامنه استفاده کنید.

با در نظر گرفتن دقیق این موارد، توسعه‌دهندگان می‌توانند استراتژی‌های حذف مناسبی را در برنامه‌های دات‌نت خود پیاده‌سازی کنند که با معماری DDD آن‌ها همخوانی داشته باشد.


مشاهده مطلب اصلی