C#实体框架:Linq过滤掉某些GrandChild元素

时间:2020-10-13 17:56:26

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

如何使用Linq EF查询过滤出孙元素?该客户有多个交易,仅需要带有某些ProductTypeId的子元素。当前,它会带来所有ProductType Id,而忽略过滤器。

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比SQL更喜欢Include语法,而不是linq。

4 个答案:

答案 0 :(得分:0)

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

答案 1 :(得分:0)

因此,请尝试根据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();

答案 2 :(得分:0)

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

我的测试模型:

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]

答案 3 :(得分:0)

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

给出一个Customer / Transaction实体,使用一个Customer / Transaction ViewModel,其中仅包含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成为客户,它变为该视图模型,您只需要调整所有引用以使用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();

这可能是一个有吸引力的选项,因为它不需要更改使用方视图,但是您必须谨慎,因为当/如果传递回控制器或任何可能假设提供的客户是完整的代表或跟踪的实体。我相信您可以利用<Customer, Customer>的Automapper配置来帮助简化仅适用列之间的过滤和复制,而忽略不需要的相关实体等。

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