Database-Design

使用硬刪除後向數據庫添加軟刪除

  • February 9, 2022

介紹

我的應用程序從一個集中的來源收集數據,許多不同的使用者可以在其中送出有關其組織和員工的數據。以前,當使用者數據與事實來源不再相關時,我們只是硬刪除使用者數據,因為它曾經是可靠的。

但是對客戶使用的某些軟體進行更改,一切都會變得混亂。現在,他們每月在送出數據時會多次刪除所有數據。這是錯誤的,並且由於設計糟糕。這意味著他們失去了我們系統中使用者的數據,並且必須重新輸入其中的一部分。

他們使用的軟體很頑固,不會改變行為。我們已經嘗試教育使用者如何使用它,但他們沒有學習。所以現在最後一個選項是軟刪除一段時間內的數據。

看過網路上的多個 Stack Overflow 文章和部落格後,我真的不喜歡任何選項,IE。在需要軟刪除的表中添加一列。我開始尋找是因為這也是我的第一直覺,但我並不真正喜歡它及其含義。

我想知道你是否可以就不同的想法給我一些回饋。我沒有維護軟刪除的經驗,我不知道我的想法是否很糟糕。

圖表和關係 顯示一些關係的簡單圖表

有一個使用者,他們的唯一標識符在多個組織中是相同的。每個使用者隸屬於一個組織,他們有一些使用者資訊,如姓名、頭銜等。在我們的系統中,他們有一個狀態行,因為無論他們選擇連接哪個組織,在我們的應用程序中都是相同的。

因此,如果我按照傳統方式添加用於軟刪除的列,我將不得不為每個包含使用者數據的唯一表添加一個,因為它們與某個組織的從屬關係可能會被刪除,但作為使用者,它們仍然存在我們的系統來自其他地方。

但是,為了解決所有這些額外的列,我的程式碼的實質內容似乎很麻煩,而且需要進行大量更改。

主意

在我看來,如果我添加一個包含以下內容的單獨表格會更簡單:

  • 唯一使用者標識符
  • 唯一組織標識符
  • 軟刪除日期

然後每當我的應用程序請求數據時,api 都會檢查新表;“這個人是不是被本組織軟刪除了?” 如果為真,它們只會阻止請求,直到它們在需要時被恢復,或者它們將一直被刪除,直到它們在軟刪除發生後的 x 小時內被硬刪除。

不必到處更改許多查詢和邏輯。

附加資訊

該 API 使用 EFCore 作為 ORM 來連接到數據庫,以防它有助於解決有關其功能集的任何其他智能修復。我曾考慮過創建自定義保存更改邏輯,但除了再次向所有表中添加一列之外,我想不出一個好主意。

如果您需要更多資訊,請告訴我。

更新

JD 告訴我行級安全性,這讓我環顧四周。它似乎非常有用,它讓我對我可以搜尋的內容有了更多的了解。

所以我遇到了 EFCore 的全域查詢過濾器,這似乎很有希望。它允許上下文對所有查詢進行過濾,當您實際上需要忽略此全域過濾器時,您可以簡單地逐個查詢地執行此操作。

如果您需要為基於連接使用者的全域過濾器使用某些東西,它允許依賴注入。我根據這些新資訊創建了一個答案

事實證明,我真正想要的是停用該行,直到最終啟動或硬刪除而不是軟刪除。我不知道表達自己的正確方式。

JD 在評論中提到了行級安全性,對於需要做我想做的事但只能使用 SQL 的人來說,這似乎很有希望。如果他們發布答案,我會為這個問題給予他們信任。

儘管可以在 EFCore 中創建允許在部署數據庫時執行自定義 SQL 的遷移,但感覺是被迫的。但仍然可能是一個有效的解決方案

MS docs 自定義遷移功能

MS docs 原始遷移 SQL

JD 的 ROW 級安全連結

根據我詞彙表中的新術語,我從 EFCore 中找到了全域查詢過濾器,然後我就去了。

微軟自己的文件

儘管您確實需要小心設置它的方式。

使用所需的導航來訪問定義了全域查詢過濾器的實體可能會導致意外結果。

但從本質上講,它歸結為在程式碼優先配置中添加這樣的內容。

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));

部落格討論一個真實世界的例子,他們討論將使用者 JWT 令牌資訊注入上下文以允許對個人進行個人過濾。

public class Context : DbContext
{
   private readonly IClaimsProvider _claimsProvider;

   private int UserId => _claimsProvider.UserId;
   private IEnumerable<int> AccessibleClientIds => _claimsProvider.AccessibleClientIds;

   public Context(DbContextOptions<Context> options, IClaimsProvider claimsProvider) : base(options)
   {
       _claimsProvider = claimsProvider;
   }
   ...
}


modelBuilder.Entity<Client>(entity =>
   {
       entity.HasQueryFilter(x => AccessibleClientIds.Contains(x.Id));

       entity.HasKey(x => x.Id);
       entity.HasMany(x => x.UserClientAccess)
             .WithOne(x => x.Client)
             .HasForeignKey(x => x.ClientId);
   });

       modelBuilder.Entity<UserOptions>(entity =>
    {
        entity.HasQueryFilter(x => x.UserId == UserId);

        entity.HasKey(x => x.Id);
    });

該部落格討論了將預設的 savechanges 功能覆蓋為始終軟刪除,但這不是我想要的,但有些人可能會覺得它很有幫助。

   public override int SaveChanges()
   {
       UpdateSoftDeleteStatuses();
       return base.SaveChanges();
   }

   public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
   {
           UpdateSoftDeleteStatuses();
           return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
   }

   private void UpdateSoftDeleteStatuses()
   {
       foreach (var entry in ChangeTracker.Entries())
       {
           switch (entry.State)
           {
               case EntityState.Added:
                   entry.CurrentValues["isDeleted"] = false;
                   break;
               case EntityState.Deleted:
                   entry.State = EntityState.Modified;
                   entry.CurrentValues["isDeleted"] = true;
                   break;
           }
       }
   }

最終實施

我有所有相關模型繼承自Deactivateable將屬性/列添加到數據庫。然後由於過濾器的簡單性,我只是在上下文中創建了這個配置:

foreach (var entity in modelBuilder.Model.GetEntityTypes())
       {
           if (entity.ClrType.IsSubclassOf(typeof(Deactivatable)))
           {
               var dateTimeOffsetDefaultValue = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0));
               modelBuilder.Entity(entity.ClrType, o =>
               {
                   o.Property(nameof(Deactivatable.DeactivatedDate)).HasDefaultValue(dateTimeOffsetDefaultValue);
               });

               var deactivatedDate = entity.FindProperty(nameof(Deactivatable.DeactivatedDate)).PropertyInfo;

               var parameterExpression = Expression.Parameter(entity.ClrType);
               var propertyExpression = Expression.Property(parameterExpression, deactivatedDate);
               var constExpression = Expression.Constant(dateTimeOffsetDefaultValue);

               var equalExpression = Expression.Equal(propertyExpression, constExpression);
               var filter = Expression.Lambda(equalExpression, parameterExpression);
               modelBuilder.Entity(entity.ClrType).HasQueryFilter(filter );
           }

       }

基於stevendarby發表的這個github 問題評論的靈感。我不知道它們是從哪裡來的,所以我只是選擇自己迭代模型並查看子類,基於我在相關 SO 文章中看到的一個想法,我再也找不到和參考了。.Model.FindLeastDerivedEntityTypes

引用自:https://dba.stackexchange.com/questions/306964