Как выполнить фильтр в дочерней сущности внутри Aggregate Root в Entity Framework - PullRequest
0 голосов
/ 11 октября 2018

Я пытаюсь получить доступ к элементу в списке внутри совокупного корня, но, поскольку в нем много записей (40K +), Entity Framework требует много времени для его выполнения, 150,180 мс на моем компьютере разработчика.

Вот урезанный пример, который показывает это поведение:

public class Parent
{
    public int Id { get; private set; }
    public virtual ICollection<Child> Children { get; private set; }

    public void Remove(string someProperty)
    {
        var itensToRemove = Children
            .Where(x => x.SomeProperty == someProperty)
            .ToList(); // -> this is where it takes a long time to run

        // remove...
    }
}

public class Child
{
    public int Id { get; set; }
}

Заполнение:

INSERT [dbo].[Parent] ([Id]) VALUES (1)
INSERT [dbo].[Child] ([Id], [Parent_Id]) VALUES (1, 1)
...
INSERT [dbo].[Child] ([Id], [Parent_Id]) VALUES (40000, 1)

Я также пытался привести к списку и использовать .RemoveAll(), норезультат один и тот же.

(Children as List<Child>).RemoveAll(x => x.SomeProperty == someProperty);

Поскольку я использую отложенную загрузку, я всегда думал, что EF рассмотрит .Where(...) и создаст отфильтрованный SQL-запрос, но SQL Profiler говорит мне, что это не так:

exec sp_executesql N'SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Parent_Id] AS [Parent_Id]
    FROM [dbo].[Child] AS [Extent1]
    WHERE 
        ([Extent1].[Parent_Id] IS NOT NULL) AND 
        ([Extent1].[Parent_Id] = @EntityKeyValue1)
',N'@EntityKeyValue1 int',@EntityKeyValue1=1

Интересно то, что когда я запускаю вышеупомянутый запрос в SSMS, он мгновенно возвращает все строки.

С точки зрения дизайна я рассматриваю прямой доступ к нему на основе этот ответ , но я чувствую, что это нарушит дизайн DDD в моем случае, так как он включает в себя бизнес-логику, которая принадлежит родительскому элементу.

1 Ответ

0 голосов
/ 12 октября 2018

Я бы не стал пытаться подобной логике изнутри сущности.«Дети» либо охотно загружаются в виде списка во время чтения родительского объекта, либо лениво загружаются в контексте контекста при обращении к нему.

При попытке:

var itensToRemove = Children
            .Where(x => x.SomeProperty == someProperty)
            .ToList(); 

... это lazy-load all children для родителя до выполнения условия Where.Если это касается многих родителей или больших групп детей, это будет очень неэффективно.

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

Например, с помощью родительского идентификатора для удаления всех дочерних элементов с параметром "childType", равным "Answer".Если у родителя есть несколько дочерних элементов, а дочерние объекты являются относительно небольшими объектами, вы можете загрузить родительский элемент с его загруженными дочерними элементами и удалить соответствующие объекты, а затем сохранить родительский элемент:

var parent = context.Parents.Where(p => p.ParentId == parentId)
  .Include(p => p.Children)
  .Single();
var childrenToRemove = parent.Children.Where(c => c.ChildType == "Answer").ToList();
foreach (child in childrenToRemove)
   parent.Children.Remove(child);

context.SaveChanges();

Если вы групповыеудаление дочерних элементов от многих / всех родителей или дочерних сущностей имеет громоздкий размер, тогда я хотел бы рассмотреть вопрос о сохранении дочерних элементов как объекта верхнего уровня (с DbSet в контексте) и удалить их напрямую.Предполагается, что между родителем и потомком существует отношение 1-ко-многим (дочерний элемент содержит parentId), а дочерний элемент сам по себе не имеет дочерних элементов:

List<Child> childrenToDelete = new List<Child>();
do
{
  childrenToDelete = context.Children
    .Where(c => c.ChildType == "Answer")
    .Select(c => c.ChildId)
    .Take(1000)
    .ToList() // Execute the query to get our 1000 IDs. We need Linq2Obj references to continue.
    .Select(id => new Child { ChildId = id})
    .ToList();

   foreach(var child in childrenToDelete)
     context.Children.Attach(child);

   context.Children.RemoveRange(childrenToDelete);
   context.SaveChanges();
} while (childrenToDelete.Any());

Выше загружены соответствующие дочерние идентификаторы и составлены /прикрепите новые ссылки на дочерние объекты, используя эти идентификаторы.Это сохраняет загрузку всех дочерних данных для выполнения удаления.Мы загружаем и удаляем партиями по 1000, чтобы сохранить разумный размер транзакции.Это также необходимо сделать в «чистом» контексте, где родители / дети еще не были загружены, так как это может вызвать проблемы с Attach.Для этого также необходимы соображения по обработке ошибок и соображения по поводу обработки ошибок на полпути, так как будет принят каждый пакет из 1000.

Если вы столкнулись с детьми, у которых есть собственные дочерние ссылки, тогда вы можете посмотреть наопределение контекста и модели сущностей, установленных специально для этой операции, где единственными данными являются обязательные ссылки.(PK и FK), где вы можете загрузить дочерние элементы и связанные с ними сущности из ограниченного контекста для операции и выполнить удаление.Опять же, если это повлияет на большое количество строк, извлекайте данные пакетами.

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

Надеемся, что это дает некоторые идеи о том, как обрабатывать ваш сценарий.

...