Вопрос или проблема
Я пытаюсь реализовать мягкое удаление, используя техники, описанные в данном видео и данной статье.
Я добавил два столбца в свою сущность.
public bool IsDeleted { get; set; }
public DateTime? DeletedOnUtc { get; set; }
И я добавил следующее в свой OnModelCreating
.
builder.Entity<Company>().HasQueryFilter(c => c.IsDeleted == false);
builder.Entity<Company>()
.HasIndex(c => c.IsDeleted)
.HasFilter("IsDeleted = 0");
Но когда я добавляю миграцию с этими изменениями, я получаю множество предупреждений о валидации, касающихся связанных таблиц.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'CompanyReport'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Customer'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Image'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'PdfTemplate'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Product'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'ProductCategory'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'ReportJob'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'CompanyReport'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'CompanyReport'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Customer'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Customer'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Image'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Image'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'PdfTemplate'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'PdfTemplate'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Product'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'Product'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Microsoft.EntityFrameworkCore.Model.Validation[10622]
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'ProductCategory'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'ProductCategory'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'ReportJob'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Сущность 'Company' имеет определенный глобальный фильтр запроса и является обязательной стороной отношения с сущностью 'ReportJob'. Это может привести к непредвиденным результатам, если обязательная сущность будет отфильтрована. Либо настройте навигацию как опциональную, либо определите соответствующие фильтры запросов для обеих сущностей в навигации. См. https://go.microsoft.com/fwlink/?linkid=2131316 для получения дополнительной информации.
Я прочитал ссылку, но неясно, почему мне нужно решать эту проблему, пока в упомянутых статьях этого не делают. Действительно ли мне нужно изменять каждый из моих существующих внешних ключей?
Может ли кто-нибудь, кто лучше понимает это, объяснить, что я делаю не так?
Проблема в том, что у вас есть фильтр запроса IsDeleted для Company, но в отличие от жесткого удаления с опцией каскадного удаления против установки внешнего ключа в null, любая сущность, которая имеет ссылку на внешний ключ по отношению к компании, должна справляться с тем фактом, что упоминаемая компания может быть помечена как удаленная.
Например, если у нас есть компания и заказ, где заказ содержит обязательный ненулевой CompanyId, EF предупреждает вас о том, что с фильтром запроса компания, у которой “IsDeleted = True”, не вернется, поэтому заказы, ссылающиеся на эту компанию, могут быть исключены или компания может быть включена в запрос.
Когда компания помечена как удаленная, ничто не гарантирует, что Order.IsDeleted также будет установлено в True для всех заказов этой компании. Предложение от EF состоит в том, что Order.Company должен быть помечен как nулевой и не обязательный, чтобы заказы, которые не помечены как удаленные, но у их компании помечено как удаленная, все еще могли возвращаться.
Фильтры запросов могут быть двусторонним мечом, поскольку удаленная компания все еще может быть действительной, читаемой записью в контексте связанных данных. Пометка компании как удаленной имеет смысл в том, что она больше не обрабатывает заказы, но это не обязательно аннулирует прошлые заказы. Поэтому, если я помечаю компанию как удаленную, я могу не смочь связать ее с будущими заказами, но это все еще имеет смысл учитывать в контексте исторических заказов. Фильтр запроса может иметь с этим проблемы, требуя от нас помнить, чтобы отключить фильтр запроса для компании, чтобы получить связанные данные.
Лично я не использую фильтры запросов для мягкого удаления, вместо этого применяю их как часть базового правила в паттерне репозитория. По умолчанию, когда я запрашиваю сущности на верхнем уровне, я применяю фильтр IsDeleted, но связанные сущности всегда будут включены для всех сущностей. То есть, по умолчанию я показываю только активные заказы, но для всех активных заказов данные клиентов/компаний могут включать неактивные связи.
Редактировать: например, если у нас есть заказы с
public class Order
{
// ...
public int CompanyId { get; set; }
public Company Company { get; set; }
}
… где Company имеет флаг мягкого удаления IsDeleted. EF даст нам предупреждение о валидации, что Company является обязательной, но ссылается на сущность с фильтром запроса, что может предотвратить получение записей. Если у меня есть 4 заказа, 2 из которых относятся к компании с ID #1, а 2 к компании с ID #2, и я помечаю компанию #2 как IsDeleted, то следующие два запроса вернут разные данные:
var orders1 = context.Orders.ToList();
var orders2 = context.Orders.Include(x => x.Company).ToList();
orders1 будет содержать 4 заказа, в то время как orders2 вернет только два заказа для компании #1, так как компания #2 будет отфильтрована. Два заказа для CompanyId #2 в orders1 будут возвращены и будут иметь CompanyId равный 2, если внешний ключ будет экспонирован. Что интересно, если DbContext будет отслеживать мягко удаленную компанию #2, то orders1 будет содержать удаленную компанию.
context.Companies.IgnoreQueryFilters().ToList(); // просто загрузить и отслеживать все компании, включая удаленные.
var orders1 = context.Orders.ToList();
var orders2 = context.Orders.Include(x => x.Company).ToList();
Здесь, даже если мы не загружаем компании в orders1, в нем будут все заказы, а ссылки на компанию #1 и #2 будут присутствовать и целыми. orders2 по-прежнему будет содержать заказы только для компании #1.
Если мы изменим конфигурацию заказа на:
public class Order
{
// ...
public int? CompanyId { get; set; }
public Company? Company { get; set; }
}
то выполнение следующих запросов:
var orders1 = context.Orders.ToList();
var orders2 = context.Orders.Include(x => x.Company).ToList();
… вернет все заказы в обоих случаях. Для заказов, принадлежащих компании #2, CompanyId будет заполнен, но ссылка на компанию будет оставлена как #null. Если CompanyId экспонируется в сущности, он все равно должен быть помечен как нулевой (int?
); если он останется как int
, то EF будет рассматривать его как обязательный, что приведет к исключенному поведению.
Ответ или решение
Ваш вопрос касается использования глобальных фильтров при реализации механизма мягкого удаления (soft delete) в Entity Framework Core, что привело к множеству предупреждений валидации. Рассмотрим, как правильно справиться с этой ситуацией.
Причина предупреждений
Предупреждения, которые вы получаете, указывают на то, что у вас есть глобальный фильтр HasQueryFilter
, примененный к сущности Company
, но эта сущность является обязательной (required) на стороне других связанных сущностей, таких как Order
, Customer
, и т.д. Когда вы помечаете компанию как "удаленную" (с IsDeleted = true
), EF Core не сможет вернуть эти связанные сущности, что может привести к непредсказуемым результатам.
Решение
Для решения этой проблемы у вас есть несколько вариантов:
-
Изменение навигационных свойств на необязательные (nullable): Если компания может быть удалена, вам следует сделать внешние ключи, ссылающиеся на
Company
, необязательными. Это означает, что вы должны изменить ваши навигационные свойства в связанных сущностях, чтобы они могли принимать значениеnull
. Например, в классеOrder
код будет выглядеть так:public class Order { public int? CompanyId { get; set; } // Сделать CompanyId nullable public Company? Company { get; set; } // Сделать ссылку на Company nullable }
Это изменение позволит вашим запросам возвращать все заказы, даже если связанные компании были помечены как удаленные. В таких случаях компания будет просто иметь значение
null
в связанных записях. -
Использование фильтров для связанных сущностей: Вы можете определить соответствующие фильтры для связанных сущностей, чтобы гарантировать совместимость с фильтром
IsDeleted
. Например, если у вас есть навигационное свойство в сущностиOrder
, которое ссылается наCompany
, и вы хотите, чтобы заказы сохраняли ссылку на удаленные компании, вам нужно будет добавить фильтр к сущностиOrder
, чтобы он был совместим сCompany
. -
Использование паттерна репозитория: Вместо применения глобального фильтра к вашим сущностям вы можете реализовать логику фильтрации через паттерн репозитория. В этом случае вы могли бы делать выборку сущностей с учетом
IsDeleted
, но также могли бы получать связанные сущности без применения фильтра, что дало бы вам больше контроля над записями, которые вы хотите вернуть.
Пример реализации
Если вы выбрали первый вариант (сделать внешние ключи nullable), ваш класс Order
будет выглядеть так:
public class Order
{
public int? CompanyId { get; set; } // Nullable
public Company? Company { get; set; } // Nullable
}
После этого следуйте следующим шагам:
- Обновите модель базы данных с помощью миграций.
- Убедитесь, что все связанные сущности правильно настроены на использование nullable внешних ключей.
Заключение
Использование глобальных фильтров с механикой мягкого удаления требует тщательного управления связями между сущностями. Понимание того, как работает EF Core с удаленными записями, критически важно для избежания проблем с отображением данных в вашем приложении. Как правило, использование nullable для внешних ключей и продуманный подход к репозиториям позволит вам правильно управлять состоянием ваших данных в базе.