Для будущих посетителей: для EF6 вам, вероятно, лучше использовать фильтры, например, через этот проект: https://github.com/jbogard/EntityFramework.Filters
В приложении, которое мы создаем, мы применяем шаблон "soft delete", в котором каждый класс имеет "Deleted" bool. На практике каждый класс просто наследует этот базовый класс:
public abstract class Entity
{
public virtual int Id { get; set; }
public virtual bool Deleted { get; set; }
}
Чтобы дать краткий пример, предположим, что у меня есть классы GymMember
и Workout
:
public class GymMember: Entity
{
public string Name { get; set; }
public virtual ICollection<Workout> Workouts { get; set; }
}
public class Workout: Entity
{
public virtual DateTime Date { get; set; }
}
Когда я получаю список членов тренажерного зала из базы данных, я могу убедиться, что ни один из "удаленных" членов тренажерного зала не будет выбран, например:
var gymMembers = context.GymMembers.Where(g => !g.Deleted);
Однако, когда я повторяю эти члены тренажерного зала, их Workouts
загружаются из базы данных, не обращая внимания на их флаг Deleted
. Хотя я не могу обвинять Entity Framework в том, что вы не собираете этого, я бы хотел настроить или перехватить ленивую загрузку свойств так или иначе, чтобы удаленные навигационные свойства никогда не загружались.
Я просматривал свои варианты, но они кажутся скудными:
Это просто не вариант, так как это будет слишком много ручной работы. (Наше приложение огромно и каждый день становится хегером). Мы также не хотим отказываться от преимуществ использования Code First (которых их много)
Опять же, не вариант. Эта конфигурация доступна только для каждого объекта. Всегда охотно загружающие объекты также налагают серьезную штрафную санкцию.
- Применяя шаблон Expression Visitor, который автоматически вводит
.Where(e => !e.Deleted)
в любом месте, он находитIQueryable<Entity>
, как описано здесь и здесь.
Я действительно протестировал это в доказательстве концепции приложения, и он отлично работал.
Это был очень интересный вариант, но, увы, он не смог применить фильтрацию к лениво загруженным свойствам навигации. Это очевидно, так как эти ленивые свойства не будут отображаться в выражении/запросе и как таковые не могут быть заменены. Интересно, сможет ли Entity Framework указать точку впрыска где-нибудь в классе DynamicProxy
, который загружает ленивые свойства.
Я также опасаюсь за другие последствия, такие как возможность взлома механизма Include
в EF.
- Написание пользовательского класса, который реализует ICollection, но автоматически фильтрует объекты
Deleted
.
Это был мой первый подход. Идея заключалась бы в использовании свойства backing для каждого свойства коллекции, которое внутренне использует собственный класс Collection:
public class GymMember: Entity
{
public string Name { get; set; }
private ICollection<Workout> _workouts;
public virtual ICollection<Workout> Workouts
{
get { return _workouts ?? (_workouts = new CustomCollection()); }
set { _workouts = new CustomCollection(value); }
}
}
Хотя этот подход на самом деле не плохой, у меня все еще есть некоторые проблемы с ним:
-
Он по-прежнему загружает все
Workout
в память и фильтрует тегиDeleted
при попадании устройства настройки свойств. По моему скромному мнению, это слишком поздно. -
Существует логическое несоответствие между выполненными запросами и загружаемыми данными.
Изобразите сценарий, где мне нужен список членов тренажерного зала, которые тренировались с прошлой недели:
var gymMembers = context.GymMembers.Where(g => g.Workouts.Any(w => w.Date >= DateTime.Now.AddDays(-7).Date));
Этот запрос может вернуть члену спортзала, который имеет только удаленные тренировки, но также удовлетворяет предикату. Как только они загружаются в память, кажется, что у этого члена спортзала нет тренировок вообще!
Вы могли бы сказать, что разработчик должен знать о Deleted
и всегда включать его в свои запросы, но это то, что я действительно хотел бы избежать. Возможно, ExpressionVisitor может предложить ответ здесь снова.
- Фактически невозможно отметить свойство навигации как
Deleted
при использовании CustomCollection.
Представьте себе этот сценарий:
var gymMember = context.GymMembers.First();
gymMember.Workouts.First().Deleted = true;
context.SaveChanges();`
Вы ожидаете, что соответствующая запись Workout
будет обновлена в базе данных, и вы ошибаетесь! Поскольку GymMember
проверяется ChangeTracker
для любых изменений, свойство gymMember.Workouts
неожиданно возвращает 1 меньше тренировки. Это потому, что CustomCollection автоматически фильтрует удаленные экземпляры, помните? Итак, теперь Entity Framework думает, что разминка должна быть удалена, и EF попытается установить FK в значение null или фактически удалить запись. (в зависимости от конфигурации вашей БД). Это то, чего мы пытались избежать с помощью шаблона мягкого удаления, с самого начала.
Я наткнулся на интересный блогpost, который отменяет метод SaveChanges
по умолчанию для DbContext
, так что любые записи с EntityState.Deleted
меняются обратно на EntityState.Modified
, но это снова кажется "взломанным" и довольно опасным. Тем не менее, я готов попробовать, если он решает проблемы без каких-либо непредвиденных побочных эффектов.
Итак, я StackOverflow. Я довольно подробно изучил свои варианты, если так могу сказать сам, и я нахожусь на своем пути. Итак, теперь я обращаюсь к вам. Как вы реализовали мягкие удаления в своем корпоративном приложении?
Чтобы повторить, это требования, которые я ищу:
- Запросы должны автоматически исключать объекты
Deleted
на уровне DB - Удаление объекта и вызов "SaveChanges" должны просто обновить соответствующую запись и не иметь других побочных эффектов.
- При загрузке навигационных свойств, будь то ленивые или нетерпеливые, теги
Deleted
должны быть автоматически исключены.
Я с нетерпением жду любых предложений, спасибо заранее.