【问题标题】:C# Entity Framework: Linq Filter out certain GrandChild ElementsC# 实体框架:Linq 过滤掉某些 GrandChild 元素
【发布时间】:2020-10-13 17:56:26
【问题描述】:

如何使用 Linq EF 查询过滤掉孙元素?此客户有多个交易,并且只需要具有特定 ProductTypeId 的子元素。它目前带来 All ProductType Ids 忽略过滤器。

var result = db.Customer
        .Include(b => b.Transactions)
        .Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())

Sql 查询,我想要的:

select distinct c.customerName
from dbo.customer customer
inner join dbo.Transactions transaction
    on transaction.customerid = customer.customerid
where transaction.ProductTypeId= 5


Customer (need 7 ProductTypeId)
    Transaction ProductTypeId 2
    Transaction ProductTypeId 4
    Transaction ProductTypeId 5
    Transaction ProductTypeId 7  <--- only need this 7 in the filter, example
    Transaction ProductTypeId 7  <--- only need this 7 in the filter, example
    Transaction ProductTypeId 8
    Transaction ProductTypeId 8
    Transaction ProductTypeId 9

*Mgmt 更喜欢 Include 语法,而不是 linq to Sql 。

【问题讨论】:

    标签: c# .net entity-framework linq asp.net-core-3.1


    【解决方案1】:

    要过滤相关实体,您需要使用投影。 EF 实体图的目的是反映完整数据状态。您想要的是过滤后的数据状态。这通常是为了向视图提供相关数据。那是一个单独的目的。

    给定一个客户/交易实体,使用一个客户/交易视图模型,其中只包含您的视图/消费者将需要的 PK 和属性。例如:

    [Serializable]
    public class CustomerViewModel
    {
        public int CustomerId { get; set; }
        public string Name { get; set; }
        // ...
        public ICollection<TransactionViewModel> ApplicableTransactions { get; set; } = new List<TransactionViewModel>();
    }
    
    [Serializable]
    public class TransactionViewModel
    {
        public int TransactionId { get; set; }
        // ...
    }
    

    然后,当您加载客户和过滤交易时:

    var result = db.Customer
        .Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
        .Select(a => new CustomerViewModel
        {
            CustomerId = a.CustomerId,
            Name = a.Name,
            // ...
            ApplicableTransactions = a.Transactions
                .Where(c => c.ProductTypeId == productTypeId)
                .Select(c => new TransactionViewModel
                {
                    TransactionId == c.TransactionId,
                    // ...
                }).ToList();
       }).ToList();
    

    利用 Automapper 进行投影可以大大简化这一过程,因为您可以将实体配置为查看模型映射(如果字段命名相同,则为单行),然后调用 ProjectTo,Automapper 将解析所需的字段SQL 并为您构建视图模型:

    var mappingConfig = new MapperConfiguration(cfg => 
    {
        cfg.CreateMap<Customer, CustomerViewModel>()
            .ForMember(dest => dest.ApplicableTransactions,
                opt => opt.MapFrom(src => src.Transactions.Where(t => t.ProductTypeId == productTypeId)
            ));
        cfg.CreateMap<Transaction, TransactionViewModel>();
    });
    
    var result = db.Customer
        .Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
        .ProjectTo<CustomerViewModel>(mappingConfig)
        .ToList();
    

    对于视图模型,我将使用反映您为其提供的视图的命名约定,因为它们实际上仅适用于为该视图提供服务。例如,如果这是按客户审核交易,则类似于 ReviewTransactionsCustomerViewModel 或 ReviewTransactionsCustomerVM。不同的视图可以提供不同的视图模型,而不是试图让一种尺寸适合所有人。

    或者,如果您的代码已经将实体发送到视图(我强烈反对),还有一些替代方案,但它们确实有缺点:

    1. 使用带有过滤子集的包装视图模型:

    例如:

    [Serializable] 
    public class ReviewTransactionsViewModel
    {
        public Customer Customer { get; set; }
        public ICollection<Transaction> ApplicableTransactions { get; set; } = new List<Transaction>();
    }
    

    然后选择:

    var result = db.Customer
        .Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
        .Select(a => new ReviewTransactionsViewModel
        {
            Customer = a,
            ApplicableTransactions = a.Transactions
                .Where(c => c.ProductTypeId == productTypeId)
                .ToList();
       }).ToList();
    

    然后在您看来,@Model 不再是 Customer,而是成为此视图模型,您只需调整任何引用以使用 Model.Customer.{property} 而不是 Model.{property},重要的是,任何对 Model.Transactions 的引用都应该更新为Model.ApplicableTransactions,而不是Model.Customer.Transactions

    这种方法的注意事项是,为了提高性能,您应该禁用填充模型的 DbContext 实例上的延迟加载以发送回,并且只预先加载视图需要的数据。延迟加载将被代码序列化实体触发以发送到视图,这很容易成为主要的性能损失。这意味着对Model.Customer.Transactions 的任何引用都将为空。这也意味着您的模型不会代表一个完整的实体,因此在将此模型传递回控制器时,您需要了解这一事实,并且不要尝试将其附加为完整的实体或传递给期望完整的方法实体。

    1. 将数据过滤到新实体中:(将实体视为视图模型)

    例如:

    var result = db.Customer
        .Where(a => a.Transactions.Select(c=> c.ProductTypeId== productTypeId).Any())
        .Select(a => new Customer
        {
            CustomerId = a.CustomerId,
            Name = a.Name,
            // ... Just the values the view will need.
            Transactions = a.Transactions
                .Where(c => c.ProductTypeId == productTypeId)
                .ToList();
       }).ToList();
    

    这可能是一个有吸引力的选项,因为它不需要更改消费视图,但您必须小心,因为当/如果传递回控制器或任何可能假设前提是客户是完整的代表或被跟踪的实体。我相信您可以利用 &lt;Customer, Customer&gt; 的 Automapper 配置来帮助促进过滤和复制仅适用的列,忽略不需要的相关实体等。

    无论如何,这应该为您提供一些权衡风险与努力的选项。

    【讨论】:

      【解决方案2】:

      更好的方法是使用 Transactions dbset 以 Include Customer 开始查询,然后对交易应用过滤器,然后从具有 Distinct 表达式的结果中选择 customerName 以获得唯一的客户名称。

      【讨论】:

        【解决方案3】:

        因此,请尝试按照您对 SQL 的期望编写查询。

        var namesAll = 
            from customer in db.Customer
            from transaction in customer.Transactions
            where transaction.ProductTypeId == 5
            select customer.CustomerName;
        
        var result = namesAll.Distinct();
        

        Lambda 语法(方法链),恕我直言,可读性最差。

        var result = db.Customer
            .SelectMany(customer => customer.Transactions, 
               (customer, transaction) => new {customer, transaction})
            .Where(pair => pair.transaction.ProductTypeId == 5)
            .Select(pair => pair.customer.CustomerName)
            .Distinct();
        

        【讨论】:

        • 抱歉,忘了说明,Mgmt 更喜欢 Include 语法,而不是 linq to Sql。
        • Include 不是查询语法,您的目标是 EF 加载完整的实体附加实体。此查询不包含投影中的任何完整实体。
        【解决方案4】:

        如果我正确理解您的需求,请尝试以下解决方案:

        我的测试模型:

        public sealed class Person
        {
            public Guid Id { get; set; }
            public DateTime? Deleted { get; set; }
            public string Email { get; set; }
            public string Name { get; set; }
            public int? B { get; set; }
        
            public IList<Vehicle> Vehicles { get; set; } = new List<Vehicle>();
        }
        
        public sealed class Vehicle
        {
            public Guid Id { get; set; }
            public int ProductTypeId { get; set; }
        
            public Guid PersonId { get; set; }
            public Person Person { get; set; }
        }
        

        查询:

        var queueItem = await _context.Persons
                    .Where(item => item.Vehicles.Any(i => i.ProductTypeId == 1))
                    .Select(item => new Person
                    {
                        Id = item.Id,
                        //Other props
                        Vehicles = item.Vehicles.Where(item2 => item2.ProductTypeId == 1).ToList()
                    })
                    .ToListAsync();
        

        来自分析器的 Sql:

        SELECT [p].[Id], [t].[Id], [t].[PersonId], [t].[ProductTypeId]
        FROM [Persons] AS [p]
        LEFT JOIN (
           SELECT [v].[Id], [v].[PersonId], [v].[ProductTypeId]
           FROM [Vehicle] AS [v]
           WHERE [v].[ProductTypeId] = 1
        ) AS [t] ON [p].[Id] = [t].[PersonId]
        WHERE EXISTS (
           SELECT 1
           FROM [Vehicle] AS [v0]
           WHERE ([p].[Id] = [v0].[PersonId]) AND ([v0].[ProductTypeId] = 1))
        ORDER BY [p].[Id], [t].[Id]
        

        另一种变体:

              var queueItem1 = await _context.Vehicle
                .Where(item2 => item2.ProductTypeId == 1)
                .Include(item => item.Person)
                .Distinct()
                .ToListAsync();
                    
               var list = queueItem1
                .GroupBy(item => item.Person)
                .Select(item => new Person
                {
                    Id = item.First().Person.Id,
                    //Other props
                    Vehicles = item.ToList()
                })
                .ToList();
        

        来自分析器的 Sql:

        SELECT [t].[Id], [t].[PersonId], [t].[ProductTypeId], [p].[Id], [p].[B], 
               [p].[Deleted], [p].[Email], [p].[Name]
        FROM (
           SELECT DISTINCT [v].[Id], [v].[PersonId], [v].[ProductTypeId]
           FROM [Vehicle] AS [v]
           WHERE [v].[ProductTypeId] = 1
         ) AS [t]
        INNER JOIN [Persons] AS [p] ON [t].[PersonId] = [p].[Id]
        

        【讨论】:

          猜你喜欢
          • 2022-06-14
          • 2011-06-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-11-08
          • 2017-07-07
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多