یکی از روشهایی که در اکثر پروژههای بزرگ استفاده میشود، بحث استفاده از حذف منطقی (soft delete) بجای حذف فیزیکی رکورد میباشد (اکثرا در برنامههایی که با بخش مالی (پول) در ارتباط هستند) و از آنجاییکه هیچ برنامهای بدون باگ نمیباشد، حذف منطقی بجای حذف فیزیکی پیشنهاد میشود. در واقع داشتن و حفظ دیتا، یک امتیاز مثبت میباشد؛ به علاوه استرس از دست دادن داده به صورت اتفاقی (سهل انگاری کاربر) را هم نخواهیم داشت. لازم به ذکر است کاربران نهایی استفاده کننده از نرم افزار، خبری از نوع حذف منطقی ندارند.
علاوه بر مواردی که ذکر شد، حذف منطقی میتواند به عنوان روشی برای حذف مطرح شود؛ به این صورت که حذف یک رکورد، در دو مرحله صورت گیرد:
- مرحله اول، حذف منطقی: کاربر اقدام به حذف رکورد مورد نظر را میکند. بعد از حذف، خبری از نمایش رکورد مربوطه نخواهد بود .
- مرحله دوم، حذف فیزیکی: مدیر اصلی میتواند تصمیم بگیرد که رکوردهای حذف منطقی شده واقعا حذف شوند یا خیر. فقط مدیر اصلی و سایر افردای که دسترسی حذف مرحله دوم را داشته باشند، از روند دو مرحلهای حذف با خبر هستند.
در ادامه قصد داریم به مزایا و معایب حذف منطقی و روشهایی برای مدیریت آن بپردازیم.
پیشهاد میکنم سری مقالات مفید
تحلیل سیستم مدیریت محتوا DTNCMS را حتما مطالعه نمایید.
برای اینکه بخواهیم حذف منطقی را پیاده سازی نماییم، نیاز داریم به هر رکورد، فیلدی اضافه شود تا از طریق آن مشخص نماییم که آیا رکورد حذف شده است یا خیر. برای این منظور باید فیلدی از نوع boolean به تمام کلاسها (جداول) اضافه شود. میتوانیم این فیلد را به صورت زیر تعریف کنیم:
public bool IsDeleted { get; set; }
برای جلوگیری از تکرار کد فوق پیشنهاد میکنم اقدام به ایجاد یک اینترفیس به صورت زیر نمایید:
public interface ISoftDelete
{
bool IsDeleted { get; set; }
}
و در کلاسی که قصد حذف منطقی را در آن دارید، فقط کافیست از اینترفیس فوق ارث بری نماید:
public class Post :ISoftDelete
{
}
بعد از افزودن فیلد فوق، نیاز داریم تا در تمام کوئریها شرطی را اضافه نماییم تا فقط رکوردهایی را از دیتابیس واکشی کند که حذف نشدهاند. یعنی فیلد فوق برابر False باشد. در ادامه روشهایی برای این هدف بیان خواهند شد.
روش هایی برای فیلتر رکوردهای حذف شده
1- افزودن فیلتر زیر در تمامی کوئریها:
where (IsDeleted=false && ...)
در روش فوق نیاز است در تمامی کوئری هایمان شرط فوق را اضافه کنیم. همانطور که حدس زدهاید، در این روش احتمال فراموش شدن شرط فوق وجود دارد و از طرفی یک کد را در همه کوئریها تکرار کردهایم.
2- نوشتن یک متد الحاقی:
برای جلوگیری از تکرار شرط فوق میتوان یک متد الحاقی را به صورت زیر پیاده سازی نمود و در تمامی شرطها، آن را فراخوانی کرد:
public static class EntityFrameworkExtentions
{
public static ObservableCollection<TEntity> Alive<TEntity>(this DbSet<TEntity> set)
where TEntity : class, ISoftDelete
{
var data = set.Where(e => e.IsDeleted == false);
return new ObservableCollection<TEntity>(data);
}
}
3-استفاده از کتابخانه
EntityFramework.DynamicFilte
ابتدا اقدام به نصب بسته آن نمایید:
Install-Package EntityFramework.DynamicFilters
و در کلاس Context خود فیلتر زیر را قرار دهید :
modelBuilder.Filter("IsDeleted", (ISoftDelete d) => d.IsDeleted, false);
مشکلاتی پیرامون حذف منطقی
- کلاس User و Post را در نظر بگیرد که یک User چندین Post دارد. حال اگر حذف، فیزیکی باشد و کاربر اقدام به حذف User مورد نظر کند، با خطای زیر مواجه میشود:
The DELETE statement conflicted with the REFERENCE constraint ....
اما در حذف منطقی چطور؟ در حذف منطقی چون رکوردی حذف نمیشود و فقط فیلد IsDeleted به روز رسانی میشود، خطای فوق رخ نخواهد داد و همین مورد باعث بروز مشکل میشود؛ چون رکوردی که دارای کلید اصلی بوده حذف شده، ولی رکوردهای وابسته به آن هنوز داخل سیستم موجود میباشند. پس برای این مورد نیاز هست ابتدا تمامی Navigation propertyهای رکورد مورد نظر یافت شوند و در صورتیکه مقداری وجود نداشت، رکورد مورد نظر حذف فیزیکی شود. برای این منظور دو راه حل پیشنهاد میشود:
1- در سرویسهای مربوط به کلاسهایی که از ISoftDelet ارث بری کردهاند، متدی تحت عنوان CanDelete، به صورت زیر تعریف شود:
public bool CanDelete(user model)
{
return !model.posts.Any() && ! model.news.Any();
}
در متد Candelete، خصوصیات مورد نیاز کلاس را بررسی کرده و در صورتیکه هیچ رکوردی وجود نداشت، کاربر میتواند رکورد مورد نظر را حذف نماید.
2- برای جلوگیری از تکرار قطعه کد فوق، میتوان از روش زیر استفاده کرد:
- یک Attribute سفارشی را به صورت زیر تعریف نمایید:
[AttributeUsage(AttributeTargets.Property)]
public class MustBeEmptyToDeleteAttribute : Attribute { }
- به تمامی خصوصیات مورد نیاز که قصد بررسی آنها را داریم، آنرا به صورت زیر اضافه میکنیم:
public class User
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
[MustBeEmptyToDelete] public virtual ICollection<Post> Posts { get; set; }
[MustBeEmptyToDelete] public virtual ICollection<File> Files { get; set; }
// etc...
}
و در پایان متد الحاقی زیر، برای بررسی رکوردهای وابسته میباشد:
public static class EntityExtensions
{
public static bool CanDelete(this object entity)
{
return entity.GetType().GetProperties()
.Where(x => x.IsDefined(typeof(MustBeEmptyToDeleteAttribute)))
.Select(x => x.GetValue(entity))
.OfType<IEnumerable<object>>()
.All(x => !x.Any());
}
همانطور که بیان شد، در حذف منطقی فقط رکورد مورد نظر به روز رسانی میشود. برای این منظور میتوان دو متد را همانند زیر در نظر گرفت و هر کدام که مورد نیاز بود، فراخوانی شود:
public void MarkAsSoftDeleted<TEntity>(TEntity entity) where TEntity : ISoftDelete
{
Entry(entity).State = EntityState.Modified;
// set IsDelete=true
// here you can set other logs like who deleted ,when ,...
}
public void MarkAsDeleted<TEntity>(TEntity entity) where TEntity : class
{
Entry(entity).State = EntityState.Deleted;
}
پیشنهادها
- اگر از حذف منطقی استفاده میکنید، امکانی را در سیستم قرار دهید تا در صورت تمایل رکوردهای حذف منطقی را بتوان حذف کرد (تهیه backup و حذف)، حذف منطقی در دراز مدت حجم دیتابیس را بالا میبرد.
- تا حد امکان به کاربران استفاده کننده، وجود امکان حذف منطقی را اطلاع ندهید. اطلاع از این امر شاید باعث عدم دقت افراد استفاده کننده شود.