实体框架多对多查询生成gnarly SQL

时间:2012-06-29 16:04:51

标签: sql entity-framework-4.1

我正在研究佣金跟踪系统。业务案例需要销售人员和客户之间的多对多关系。 (简化)实体是:

public class Customer {
  public int Id { get; set; }
  public string Name { get; set; }

  public virtual ICollection<CustomerSeller> CustomerSellers { get; set; }
  public virtual ICollection<Payment> Payments { get; set; }
}

public class Seller {
  public int Id { get; set; }
  public string Name { get; set; }
  public virtual ICollection<CustomerSeller> CustomerSellers { get; set; }
}

// join table w/payload for many-to-many relationship
public class CustomerSeller {
  public int Id { get; set; }
  public Seller Seller { get; set; }
  public Customer Customer { get; set; }
  public decimal CommissionRate { get; set; }
}

public class Payment {
  public int Id { get; set; }
  public Customer ReceivedFrom { get; set; }
  public DateTime Date { get; set; }
  public decimal Amount { get; set; }
}

我目前的目标是获取与给定销售人员相关联的所有客户付款清单。如果我直接写SQL,它看起来像这样:

select Payment.* 
from Payment
inner join CustomerSeller on CustomerSeller.Customer_Id = Payment.ReceivedFrom_Id
where CustomerSeller.Seller_Id = @sellerIdToQuery

我正在使用此代码在我的ASP NET MVC网站上使用linq / EF进行此操作:

public ActionResult Sales(int id) {
  var qry = (
    from p in db.Payments
    join cs in db.CustomerSellers on p.ReceivedFrom equals cs.Customer 
    where cs.Seller.Id == id
    select p);
  var paymentList = qry.ToList();
  return View(paymentList);
}

这样做可行,但幕后的SQL看起来很复杂(这超出了我的SQL解码能力分析):

{SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Date] AS [Date], 
[Extent1].[Amount] AS [Amount], 
[Extent1].[ReceivedFrom_Id] AS [ReceivedFrom_Id]
FROM  [dbo].[Payment] AS [Extent1]
INNER JOIN [dbo].[CustomerSeller] AS [Extent2] ON  EXISTS (SELECT 
    1 AS [C1]
    FROM      ( SELECT 1 AS X ) AS [SingleRowTable1]
    LEFT OUTER JOIN  (SELECT 
        [Extent3].[Id] AS [Id]
        FROM [dbo].[Customer] AS [Extent3]
        WHERE [Extent1].[ReceivedFrom_Id] = [Extent3].[Id] ) AS [Project1] ON 1 = 1
    LEFT OUTER JOIN  (SELECT 
        [Extent4].[Id] AS [Id]
        FROM [dbo].[Customer] AS [Extent4]
        WHERE [Extent2].[Customer_Id] = [Extent4].[Id] ) AS [Project2] ON 1 = 1
    LEFT OUTER JOIN  (SELECT 
        [Extent5].[Id] AS [Id]
        FROM [dbo].[Customer] AS [Extent5]
        WHERE [Extent1].[ReceivedFrom_Id] = [Extent5].[Id] ) AS [Project3] ON 1 = 1
    LEFT OUTER JOIN  (SELECT 
        [Extent6].[Id] AS [Id]
        FROM [dbo].[Customer] AS [Extent6]
        WHERE [Extent2].[Customer_Id] = [Extent6].[Id] ) AS [Project4] ON 1 = 1
    WHERE ([Project1].[Id] = [Project2].[Id]) OR (([Project3].[Id] IS NULL) AND ([Project4].[Id] IS NULL))
)
WHERE [Extent2].[Seller_Id] = @p__linq__0}

我特别感兴趣的是我的linq查询(我是新的)形成得很糟糕......应该怎么写?或者我的实体结构有问题吗?或者SQL是否正常,并且能够在生产环境中有效地运行大量数据?

进展:

首先,Slauma提出了两个新的查询。在试验它们时,我注意到SQL在为Customer和Seller字段连接表CustomerSeller上处理NULL时付出了额外的努力。这是我的错误,因为这些字段应该是不可为空的。我在实体中将它们设置为[必需]:

public class CustomerSeller {
  public int Id { get; set; }
  [Required]
  public Seller Seller { get; set; }
  [Required]
  public Customer Customer { get; set; }
  public decimal CommissionRate { get; set; }
}

第一个查询Slauma写道:

var qry = from p in db.Payments
          where p.ReceivedFrom.CustomerSellers.Any(cs => cs.Seller.Id == id)
          select p;

具有非常优越的SQL生成:

SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Date] AS [Date], 
[Extent1].[Amount] AS [Amount], 
[Extent1].[ReceivedFrom_Id] AS [ReceivedFrom_Id]
FROM [dbo].[Payment] AS [Extent1]
WHERE  EXISTS (SELECT 
    1 AS [C1]
    FROM [dbo].[CustomerSeller] AS [Extent2]
    WHERE ([Extent1].[ReceivedFrom_Id] = [Extent2].[Customer_Id]) AND ([Extent2].[Seller_Id] = @p__linq__0)
)

Slauma建议的下一个调整是在Id字段而不是整个实体上加入我原来的查询:

var qry = from p in db.Payments
          //old: join cs in db.CustomerSellers on p.ReceivedFrom equals cs.Customer
          //new:
                 join cs in db.CustomerSellers on p.ReceivedFrom.Id equals cs.Customer.Id
          where cs.Seller.Id == id
          select p;

这会生成一个非常高质量的SQL语句:

SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Date] AS [Date], 
[Extent1].[Amount] AS [Amount], 
[Extent1].[ReceivedFrom_Id] AS [ReceivedFrom_Id]
FROM  [dbo].[Payment] AS [Extent1]
INNER JOIN [dbo].[CustomerSeller] AS [Extent2] ON [Extent1].[ReceivedFrom_Id] = [Extent2].[Customer_Id]
WHERE [Extent2].[Seller_Id] = @p__linq__0

基本上我自己直接在SQL中编写的内容。

小贴士:

(1)加入密钥而不是完整的实体。

(2)如果连接表不能具有多对多映射字段的空值,请将它们标记为[必需],这样可以简化查询。

(3)检查幕后SQL的linq查询,特别是在经常使用时,或者它们可能触及大量数据时。可能有怪物藏匿。

(4)Slauma是一位绅士和学者。 : - )

1 个答案:

答案 0 :(得分:1)

我会用这种方式编写查询:

var qry = from p in db.Payments
          where p.ReceivedFrom.CustomerSellers.Any(cs => cs.Seller.Id == id)
          select p;
var paymentList = qry.ToList();

或完全使用扩展方法:

var qry = db.Payments
    .Where(p => p.ReceivedFrom.CustomerSellers.Any(cs => cs.Seller.Id == id));
var paymentList = qry.ToList();

我不知道SQL与您的查询有什么不同或多少。

修改

替代:

var qry = db.CustomerSellers
    .Where(cs => cs.Seller.Id == id)
    .SelectMany(cs => cs.Customer.Payments);
var paymentList = qry.ToList();

如果Seller中的CustomerCustomerSellers具有复合唯一约束,则生成的Payment应该没有重复项。否则,您需要在Distinct()后添加SelectMany