简体   繁体   English

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

[英]C# Entity Framework: Linq Filter out certain GrandChild Elements

How do I filter out Grandchild elements with a Linq EF Query?如何使用 Linq EF 查询过滤孙子元素? This Customer has multiple transactions, and only need subchild elements with Certain ProductTypeId.该客户有多个交易,并且只需要具有特定 ProductTypeId 的子子元素。 Its currently bringing All ProductType Ids ignoring the filter .它目前带来了忽略过滤器的所有产品类型 ID。

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

Sql query, that I want:我想要的 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 prefers Include syntax, rather than linq to Sql . *Mgmt 更喜欢 Include 语法,而不是 linq to Sql 。

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

So, try to write queries as you expect them from SQL.因此,请尝试按照您对 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 syntax (method chain), IMHO which is worst readable. 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();

If I correctly understand what do you need, try a solution like this:如果我正确理解您需要什么,请尝试以下解决方案:

My test models:我的测试模型:

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; }
}

Query:询问:

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 from profiler:来自探查器的 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]

One more variant:另一种变体:

      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 from profiler:来自探查器的 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]

To filter related entities you need to use projection.要过滤相关实体,您需要使用投影。 The purpose of an EF entity graph is to reflect the complete data state. EF 实体图的目的是反映完整的数据状态。 What you want is a filtered data state.您想要的是过滤后的数据状态。 This is usually to provided relevant data to a view.这通常是为了向视图提供相关数据。 That is a separate purpose.那是一个单独的目的。

Given a Customer/Transaction entity, use a Customer/Transaction ViewModel containing just the PKs and the properties that your view/consumer is going to need.给定一个 Customer/Transaction 实体,使用一个 Customer/Transaction ViewModel,它只包含您的视图/消费者需要的 PK 和属性。 For example:例如:

[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; }
    // ...
}

Then, when you go to load your customers & filtered transactions:然后,当您加载客户和过滤的交易时:

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();

Leveraging Automapper for your projections can Simplify this considerably, as you can configure the entity to view model mapping (which are one-liners if the field naming is the same) then call ProjectTo and Automapper will resolve the fields needed for the SQL and construct the view models for you:利用 Automapper 进行投影可以大大简化这一过程,因为您可以将实体配置为查看模型映射(如果字段命名相同,则为单行)然后调用ProjectTo ,Automapper 将解析 SQL 所需的字段并构建为您查看模型:

Ie IE

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();

For the view models I would use a naming convention that reflects the view you are supplying them for as they are really only applicable to serve that view.对于视图模型,我将使用一个命名约定来反映您为其提供的视图,因为它们实际上仅适用于为该视图提供服务。 For instance if this is reviewing transactions by customer then something like ReviewTransactionsCustomerViewModel or ReviewTransactionsCustomerVM.例如,如果这是按客户审查交易,则类似于 ReviewTransactionsCustomerViewModel 或 ReviewTransactionsCustomerVM。 Different views can source different view models vs. trying to have one size fit all.不同的视图可以提供不同的视图模型,而不是试图让一个尺寸适合所有。

Alternatively if your code is already sending Entities to views (which I strongly discourage) there are a couple alternatives, but these do have drawbacks:或者,如果您的代码已经将实体发送到视图(我强烈不鼓励这样做),则有几种选择,但这些确实有缺点:

  1. Using a wrapper view model with a filtered sub-set:使用带有过滤子集的包装视图模型:

For example:例如:

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

Then when selecting:然后在选择时:

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();

Then in your view, instead of the @Model being a Customer, it becomes this view model and you just need to tweak any references to use Model.Customer.{property} rather than Model.{property} and importantly, any references to Model.Transactions should be updated to Model.ApplicableTransactions , not Model.Customer.Transactions .然后在您的视图中,而不是 @Model 作为客户,它成为这个视图模型,您只需要调整任何引用以使用Model.Customer.{property}而不是Model.{property}并且重要的是,对Model.Transactions任何引用Model.Transactions应该更新为Model.ApplicableTransactions ,而不是Model.Customer.Transactions

The caveat to this approach is that for performance you should disable lazy loading on the DbContext instance populating your model to send back, and only eager-load the data your view will need.这种方法的警告是,为了提高性能,您应该在填充模型的 DbContext 实例上禁用延迟加载以发送回,并且仅预先加载您的视图需要的数据。 Lazy loading will get tripped by code serializing entities to send to a view which can easily be a major performance hit.延迟加载将被代码序列化实体绊倒以发送到视图,这很容易成为主要的性能损失。 This means any references to Model.Customer.Transactions will be empty.这意味着对Model.Customer.Transactions任何引用Model.Customer.Transactions将为空。 It also means that your model will not represent a complete entity, so when passing this model back to the controller you need to be aware of this fact and not attempt to attach it to use as a complete entity or pass to a method expecting a complete entity.这也意味着您的模型不会代表一个完整的实体,因此当将此模型传递回控制器时,您需要了解这一事实,不要尝试将其附加以用作完整实体或传递给需要完整实体的方法实体。

  1. Filter the data into a new entity: (Treat entity as view model)将数据过滤成新实体:(将实体视为视图模型)

For example:例如:

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();

This can be an attractive option as it requires no changes to the consuming view but you must be cautious as this model now does not reflect a complete data state when/if passed back to the controller or any method that may assume that a provided Customer is a complete representation or a tracked entity.这可能是一个有吸引力的选择,因为它不需要对消费视图进行更改,但您必须谨慎,因为当/如果传递回控制器或任何可能假定提供的客户是完整的表示或跟踪的实体。 I believe you can leverage an Automapper confguration for <Customer, Customer> to help facilitate the filtering and copying across only applicable columns, ignoring unneeded related entities etc.我相信您可以利用<Customer, Customer> Automapper 配置来帮助促进仅适用列之间的过滤和复制,忽略不需要的相关实体等。

In any case, this should give you some options to weigh up risks vs. effort.无论如何,这应该为您提供一些权衡风险与努力的选择。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM