Подтвердить что ты не робот

Как я могу автоматически отфильтровать мягкие удаленные объекты с помощью Entity Framework?

Я использую Entity Framework Code First. Я переопределяю SaveChanges в DbContext, чтобы разрешить "мягкое удаление":

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

    continue;
}

Это здорово, поэтому объект знает, как отмечать себя как мягкое удаление (в этом случае он просто устанавливает IsDeleted в true).

Мой вопрос в том, как я могу сделать так, что когда я получаю объект, он игнорирует любой с помощью IsDeleted? Поэтому, если бы я сказал _db.Users.FirstOrDefault(UserId == id), если у этого пользователя был IsDeleted == true, он проигнорировал бы его. По сути, я хочу фильтровать?

Примечание. Я не хочу просто поставить && IsDeleted == true Вот почему я отмечаю классы интерфейсом, поэтому remove знает, как "Just Work", и я хотел бы каким-то образом изменить поиск, чтобы узнать, как "Just Work" также основан на этом интерфейсе.

4b9b3361

Ответ 1

У меня есть мягкое удаление, работающее для всех моих сущностей, и мягкие удаленные элементы не извлекаются через контекст, используя метод, предложенный этим ответом. Это касается и доступа к сущности через свойства навигации.

Добавьте дискриминатор 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 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 и найдите все записи, которые нужно удалить:

Изменить Другой способ переопределить удаление sql - изменить хранимые процедуры, сгенерированные 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 между типами & Столы

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;

P.S. Голосовать за глобальную фильтрацию здесь https://entityframework.codeplex.com/workitem/945?FocusElement=CommentTextBox# и фильтровать включает в себя здесь

Ответ 2

Используйте EntityFramework.DynamicFilters. Он позволяет создавать глобальные фильтры, которые будут применяться автоматически (в том числе против свойств навигации) при выполнении запросов.

На странице проекта есть пример фильтра IsDeleted, который выглядит следующим образом:

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

Этот фильтр будет автоматически вводить предложение where для любого запроса в отношении объекта, который является ISoftDelete. Фильтры определены в вашем DbContext.OnModelCreating().

Отказ от ответственности: я автор.

Ответ 3

Один из вариантов заключается в инкапсуляции !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)

Ответ 4

Вы можете использовать фильтры глобальных запросов в Entity Framework Core 2.0.

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);
}

Ответ 5

Отличный вопрос.

Вам нужно будет перехватить SQL-запрос до того, как он каким-то образом будет выполнен, а затем добавить дополнительное предложение where, чтобы удалить "удаленные" элементы из выделения. К сожалению, у Entity нет GetCommand, который может быть использован для изменения запроса.

Возможно, EF Provider Wrapper, который находится в нужном месте, может быть изменен для изменения запроса.

Или, вы можете использовать QueryInterceptor, но каждый запрос должен будет использовать InterceptWith(visitor) для изменения выражений...

Итак, я бы сосредоточился на этом подходе, поскольку AFAIK не имеет другого варианта, а затем перехватывает запрос и исправляет его (если вы хотите сохранить код, который не требует изменений).

В любом случае, если вы выясните что-то полезное, сообщите нам.