EF Core如何通过另一个列表过滤嵌套的多对多列表

时间:2018-11-14 18:38:43

标签: c# entity-framework-core

使用.Net Core 2.1和EF Core 2.1.4

如果这些记录中的任何一条包含我在列表中拥有的名称,并且仅包含具有匹配的嵌套记录的对象,那么我将尝试获取该对象的所有嵌套记录。

我有一个查询可以产生正确的结果,但是它正在查询数据库表中的每条记录。

如果可能的话,我希望进一步减少它。

我的ViewModel(实际模型几乎相同):

EventViewModel

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace MyProject.Models.ViewModels
{
    public class EventViewModel
    {
        private string _name;

        public Guid Id { get; set; }

        [Required, StringLength(100)]
        public string Name
        {
            get => _name?.Trim();
            set => _name = value?.Trim();
        }

        [DataType(DataType.Date)]
        public DateTime Date { get; set; }

        [Display(Name = "Programs")]
        public IEnumerable<EventProgramViewModel> EventProgramViewModels { get; set; }

        public string AllProgramNames
        {
            get
            {
                string result = EventProgramViewModels.Aggregate(string.Empty,
                    (current, program) => current + $"{program?.ProgramViewModel?.Name}, ");
                return result.TrimEnd(',', ' ');
            }
        }

        public EventViewModel()
        {
            Id = Guid.NewGuid();
            Date = DateTime.Now;
            EventProgramViewModels = new List<EventProgramViewModel>();
        }
    }
}

EventProgramViewModel

using System;

namespace MyProject.Models.ViewModels
{
    public class EventProgramViewModel
    {
        public Guid EventViewModelId { get; set; }
        public EventViewModel EventViewModel { get; set; }

        public Guid ProgramViewModelId { get; set; }
        public ProgramViewModel ProgramViewModel { get; set; }
    }
}

ProgramViewModel

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace MyProject.Models.ViewModels
{
    public class ProgramViewModel
    {
        private string _name;

        public Guid Id { get; set; }

        [Required, StringLength(100)]
        [Display(Name = "Program Name")]
        public string Name
        {
            get => _name?.Trim();
            set => _name = value?.Trim();
        }
    }
}

在我的 ApplicationDbContext.cs 文件中,此映射如下:

modelBuilder.Entity<EventProgram>()
            .HasKey(eventProgram => new {eventProgram.EventId, eventProgram.ProgramId});

最后,这是我的查询

var testQuery = context.Events
        .AsNoTracking()
        .Select(e => new EventViewModel
        {
            Id = e.Id,
            Name = e.Name,
            Date = e.Date,
            EventProgramViewModels = e.EventPrograms.Select(eventProgram =>
                new EventProgramViewModel
                {
                    ProgramViewModel = new ProgramViewModel
                    {
                        Name = eventProgram.Program.Name
                    }
                })
        })
        .OrderByDescending(eventViewModel => eventViewModel.Date)
        .ThenBy(eventViewModel => eventViewModel.Name)
        .Where(eventViewModel =>
            !search.ProgramsChosen.Any() || eventViewModel.EventProgramViewModels.Any(
                eventProgramViewModel =>
                    search.ProgramsChosen.Contains(eventProgramViewModel.ProgramViewModel
                        .Name)))
        .ToList()
    ;

foreach (var item in testQuery)
{
    // Mock usage to show where queries are generated
    _loggingServices.LogInformation(JsonConvert.SerializeObject(item));
}

这将对主要Event属性产生一个EF查询:

SELECT [e].[Id], [e].[Name], [e].[Date]
FROM [Events] AS [e]
ORDER BY [e].[Date] DESC, [e].[Name]

每个 结果为1(在这种情况下,在上面的foreach循环中)

SELECT [eventProgram.Program].[Name]
FROM [EventPrograms] AS [eventProgram]
INNER JOIN [Programs] AS [eventProgram.Program] ON [eventProgram].[ProgramId] = [eventProgram.Program].[Id]
WHERE @_outer_Id = [eventProgram].[EventId]

有什么方法可以改善此效果?


更新1:

基于TyCobb's comment,我将查询结构更改为:

var testQuery =
        context.Events
            .AsNoTracking()
            .OrderByDescending(@event => @event.Date)
            .ThenBy(@event => @event.Name)
            .Where(@event =>
                !search.ProgramsChosen.Any() || @event.EventPrograms.Any(
                    eventProgramViewModel =>
                        search.ProgramsChosen.Contains(eventProgramViewModel.Program
                            .Name)))
            .Select(e => new EventViewModel
            {
                Id = e.Id,
                Name = e.Name,
                Date = e.Date,
                EventProgramViewModels = e.EventPrograms.Select(eventProgram =>
                    new EventProgramViewModel
                    {
                        ProgramViewModel = new ProgramViewModel
                        {
                            Name = eventProgram.Program.Name
                        }
                    })
            })
    ;

哪个更好,因为它只对每条有效记录生成查询,而在此之前它会进行每条记录。

现在将产生此查询:

SELECT [event].[Id], [event].[Name], [event].[Date]
FROM [Events] AS [event]
WHERE EXISTS (
    SELECT 1
    FROM [EventPrograms] AS [eventProgramViewModel]
    INNER JOIN [Programs] AS [eventProgramViewModel.Program] ON [eventProgramViewModel].[ProgramId] = [eventProgramViewModel.Program].[Id]
    WHERE [eventProgramViewModel.Program].[Name] IN (N'A SEARCHED PROGRAM NAME') AND ([event].[Id] = [eventProgramViewModel].[EventId]))
ORDER BY [event].[Date] DESC, [event].[Name]

每个有效记录一个:

SELECT [eventProgram.Program].[Name]
FROM [EventPrograms] AS [eventProgram]
INNER JOIN [Programs] AS [eventProgram.Program] ON [eventProgram].[ProgramId] = [eventProgram.Program].[Id]
WHERE @_outer_Id = [eventProgram].[EventId]

1 个答案:

答案 0 :(得分:3)

有两个问题。

第一个问题(您使用 Update 1 解决了)是由当前的EF Core查询转换缺陷引起的,该缺陷导致客户端对Where子句(OrderBy可以)。因此,将过滤器移到投影之前是解决该问题的当前方法。

第二个是所谓的N + 1子查询问题。 EF Core 2.1包含Optimization of correlated subqueries,它可以根据您的情况使用,但是如文档中所述,您应该通过添加ToList(或ToArray)来选择加入它:

  

我们改进了查询转换,以避免在许多常见情况下执行“ N + 1”个SQL查询,在这种情况下,使用投影中的导航属性会导致将根查询中的数据与相关子查询中的数据连接在一起。优化需要缓冲子查询的结果,我们要求您修改查询以选择采用新行为。

然后

  

通过在正确的位置添加ToList(),表示缓冲适用于订单,从而可以进行优化

所以最终的查询应该是这样的:

var testQuery = context.Events
    //.AsNoTracking() <-- No need when using projection
    .OrderByDescending(@event => @event.Date)
    .ThenBy(@event => @event.Name)
    .Where(@event =>
        !search.ProgramsChosen.Any() || @event.EventPrograms.Any(
            eventProgram =>
                search.ProgramsChosen.Contains(eventProgram.Program
                    .Name)))
    .Select(e => new EventViewModel
    {
        Id = e.Id,
        Name = e.Name,
        Date = e.Date,
        EventProgramViewModels = e.EventPrograms.Select(eventProgram =>
            new EventProgramViewModel
            {
                ProgramViewModel = new ProgramViewModel
                {
                    Name = eventProgram.Program.Name
                }
            }).ToList() // <-- 
    })
    .ToList()
    ;

这将导致2条SQL查询:

SELECT [event].[Id], [event].[Name], [event].[Date]
FROM [Events] AS [event]
WHERE EXISTS (
    SELECT 1
    FROM [EventPrograms] AS [eventProgramViewModel]
    INNER JOIN [Programs] AS [eventProgramViewModel.Program] ON [eventProgramViewModel].[ProgramId] = [eventProgramViewModel.Program].[Id]
    WHERE [eventProgramViewModel.Program].[Name] IN (N'P2', N'P4', N'P7') AND ([event].[Id] = [eventProgramViewModel].[EventId]))
ORDER BY [event].[Date] DESC, [event].[Name], [event].[Id]

SELECT [t].[Date], [t].[Name], [t].[Id], [eventProgram.Program].[Name] AS [Name0], [event.EventPrograms].[EventId]
FROM [EventPrograms] AS [event.EventPrograms]
INNER JOIN [Programs] AS [eventProgram.Program] ON [event.EventPrograms].[ProgramId] = [eventProgram.Program].[Id]
INNER JOIN (
    SELECT [event0].[Date], [event0].[Name], [event0].[Id]
    FROM [Events] AS [event0]
    WHERE EXISTS (
        SELECT 1
        FROM [EventPrograms] AS [eventProgramViewModel0]
        INNER JOIN [Programs] AS [eventProgramViewModel.Program0] ON [eventProgramViewModel0].[ProgramId] = [eventProgramViewModel.Program0].[Id]
        WHERE [eventProgramViewModel.Program0].[Name] IN (N'P2', N'P4', N'P7') AND ([event0].[Id] = [eventProgramViewModel0].[EventId]))
) AS [t] ON [event.EventPrograms].[EventId] = [t].[Id]
ORDER BY [t].[Date] DESC, [t].[Name], [t].[Id]