web-dev-qa-db-ja.com

Entity Frameworkでソフト削除されたエンティティを自動的に除外するにはどうすればよいですか?

Entity Framework Code Firstを使用しています。 SaveChangesDbContextをオーバーライドして、「ソフト削除」を実行できるようにします。

if (item.State == EntityState.Deleted && typeof(ISoftDelete).IsAssignableFrom(type))
{
    item.State = EntityState.Modified;
    item.Entity.GetType().GetMethod("Delete")
        .Invoke(item.Entity, null);

    continue;
}

これは素晴らしいことなので、オブジェクトは自分自身をソフト削除としてマークする方法を知っています(この場合、IsDeletedtrueに設定するだけです)。

私の質問は、オブジェクトを取得するときにIsDeletedのあるものを無視するようにするにはどうすればよいですか?そのため、_db.Users.FirstOrDefault(UserId == id)と言った場合、そのユーザーがIsDeleted == trueそれは無視します。本質的にフィルタリングしたいですか?

注:単に&& IsDeleted == trueだからこそ、私はクラスがインターフェースでマークしているので、削除は「ただ動作する」方法を知っています。

33
Jordan

私はすべてのエンティティに対してソフト削除が機能し、ソフト削除されたアイテムは this answer によって提案された手法を使用してコンテキスト経由で取得されません。これには、ナビゲーションプロパティを介してエンティティにアクセスする場合も含まれます。

IsDeleted識別子を、ソフト削除できるすべてのエンティティに追加します。残念ながら、抽象クラスまたはインターフェイスから派生したエンティティに基づいてこのビットを実行する方法を考え出していません( EFマッピングは現在エンティティとしてインターフェイスをサポートしていません ):

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
   modelBuilder.Entity<Foo>().Map(m => m.Requires("IsDeleted").HasValue(false));
   modelBuilder.Entity<Bar>().Map(m => m.Requires("IsDeleted").HasValue(false));

   //It's more complicated if you have derived entities. 
   //Here 'Block' derives from 'Property'
   modelBuilder.Entity<Property>()
            .Map<Property>(m =>
            {
                m.Requires("Discriminator").HasValue("Property");
                m.Requires("IsDeleted").HasValue(false);
            })
            .Map<Block>(m =>
            {
                m.Requires("Discriminator").HasValue("Block");
                m.Requires("IsDeleted").HasValue(false);
            });
}

SaveChangesをオーバーライドし、削除するすべてのエントリを見つけます。

Editsqlの削除をオーバーライドする別の方法 は、EF6によって生成されたストアドプロシージャを変更することです

public override int SaveChanges()
{
   foreach (var entry in ChangeTracker.Entries()
             .Where(p => p.State == EntityState.Deleted 
             && p.Entity is ModelBase))//I do have a base class for entities with a single 
                                       //"ID" property - all my entities derive from this, 
                                       //but you could use ISoftDelete here
    SoftDelete(entry);

    return base.SaveChanges();
}

SoftDeleteメソッドは、識別子列をエンティティに含めることができないため、データベースで直接sqlを実行します。

private void SoftDelete(DbEntityEntry entry)
{
    var e = entry.Entity as ModelBase;
    string tableName = GetTableName(e.GetType());
    Database.ExecuteSqlCommand(
             String.Format("UPDATE {0} SET IsDeleted = 1 WHERE ID = @id", tableName)
             , new SqlParameter("id", e.ID));

    //Marking it Unchanged prevents the hard delete
    //entry.State = EntityState.Unchanged;
    //So does setting it to Detached:
    //And that is what EF does when it deletes an item
    //http://msdn.Microsoft.com/en-us/data/jj592676.aspx
    entry.State = EntityState.Detached;
}

GetTableNameは、エンティティに対して更新されるテーブルを返します。テーブルが派生型ではなくBaseTypeにリンクされている場合を処理します。継承階層全体をチェックする必要があると思います...しかし、 メタデータAPIを改善する の計画があり、必要に応じて EF Code Firstの型とテーブル間のマッピング

private readonly static Dictionary<Type, EntitySetBase> _mappingCache 
       = new Dictionary<Type, EntitySetBase>();

private ObjectContext _ObjectContext
{
    get { return (this as IObjectContextAdapter).ObjectContext; }
}

private EntitySetBase GetEntitySet(Type type)
{
    type = GetObjectType(type);

    if (_mappingCache.ContainsKey(type))
        return _mappingCache[type];

    string baseTypeName = type.BaseType.Name;
    string typeName = type.Name;

    ObjectContext octx = _ObjectContext;
    var es = octx.MetadataWorkspace
                    .GetItemCollection(DataSpace.SSpace)
                    .GetItems<EntityContainer>()
                    .SelectMany(c => c.BaseEntitySets
                                    .Where(e => e.Name == typeName 
                                    || e.Name == baseTypeName))
                    .FirstOrDefault();

    if (es == null)
        throw new ArgumentException("Entity type not found in GetEntitySet", typeName);

    _mappingCache.Add(type, es);

    return es;
}

internal String GetTableName(Type type)
{
    EntitySetBase es = GetEntitySet(type);

    //if you are using EF6
    return String.Format("[{0}].[{1}]", es.Schema, es.Table);

    //if you have a version prior to EF6
    //return string.Format( "[{0}].[{1}]", 
    //        es.MetadataProperties["Schema"].Value, 
    //        es.MetadataProperties["Table"].Value );
}

以前、次のようなコードを使用して、移行時に自然キーのインデックスを作成しました。

public override void Up()
{
    CreateIndex("dbo.Organisations", "Name", unique: true, name: "IX_NaturalKey");
}

ただし、削除された組織と同じ名前の新しい組織を作成することはできません。これを可能にするために、コードを変更してインデックスを作成しました:

public override void Up()
{
    Sql(String.Format("CREATE UNIQUE INDEX {0} ON dbo.Organisations(Name) WHERE IsDeleted = 0", "IX_NaturalKey"));
}

そして、それは削除されたアイテムをインデックスから除外します

関連アイテムが一時的に削除された場合、ナビゲーションプロパティは設定されませんが、外部キーは設定されます。例えば:

if(foo.BarID != null)  //trying to avoid a database call
   string name = foo.Bar.Name; //will fail because BarID is not null but Bar is

//but this works
if(foo.Bar != null) //a database call because there is a foreign key
   string name = foo.Bar.Name;

PSここでグローバルフィルタリングに投票 https://entityframework.codeplex.com/workitem/945?FocusElement=CommentTextBox# およびフィルタリングされたインクルード ここ

39
Colin

EntityFramework.DynamicFilters を使用します。クエリの実行時に自動的に(ナビゲーションプロパティに対するものを含む)適用されるグローバルフィルターを作成できます。

プロジェクトページには、次のような「IsDeleted」フィルタの例があります。

modelBuilder.Filter("IsDeleted", (ISoftDelete d) => d.IsDeleted, false);

そのフィルターは、ISoftDeleteであるエンティティに対するクエリにwhere句を自動的に挿入します。フィルターはDbContext.OnModelCreating()で定義されます。

免責事項:私は著者です。

36
John

1つのオプションは、!IsDeletedを拡張メソッドにカプセル化することです。以下のようなものは単なる例です。拡張メソッドのアイデアを提供するためだけに注意してください。以下はコンパイルされません。

public static class EnumerableExtensions
{
    public static T FirstOrDefaultExcludingDeletes<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        return source.Where(args => args != IsDeleted).FirstOrDefault(predicate);
    }
}

使用法:

_db.Users.FirstOrDefaultExcludingDeletes(UserId == id)
7
Ricky G

Entity Framework Core 2.0では Global Query Filters を使用できます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("_tenantId");

    // Configure entity filters
    modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
    modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
}
2
hkutluay