【问题标题】:Is it really this hard to just do a LEFT JOIN?只做 LEFT JOIN 真的这么难吗?
【发布时间】:2019-06-24 08:53:35
【问题描述】:

我有数据表形式的传入数据。没有可以依赖的静态类。我有 2 张桌子,客户和计费。有7000个客户,1200条计费记录。

所有客户记录都有一个“ResponsiblePartyID”,多个客户可以有同一个ID,它引用了计费表的ID。

DataTable customer= ETL.ParseTable("customer"); // 7000 records
DataTable billing= ETL.ParseTable("billing");   // 1200 records

var JoinedTables = (from c in customer.AsEnumerable()
            join p in billing.AsEnumerable() on (string) c["ResponsiblePartyID"] equals (string) p["ID"] into ps
            from p in ps.DefaultIfEmpty()
            select new {c, p}
        );

所以这不能正常工作,即使它以错误的格式输出结果我也会很高兴,但它只返回 2200 个结果而不是 7000 个。

如果它只返回 1200,或者如果它返回全部 7000,似乎是有道理的,但 2200 是一个奇怪的地方让它停下来。

我手动解析二进制数据作为我的数据源,我选择了一个 DataTable 作为目标,因为它似乎是正确的方法,但是在处理了 Linq 并尝试进行连接之后,我想知道我是否应该重新考虑一下。

Linq 似乎不是为查询 DataTables 而设计的,因为我必须对所有内容执行 .AsEnumerable(),然后在完成每个步骤时使用 .CopyToDataTable()

我没有为我的所有数据定义静态类,因为每个值的属性都已在 DataTable 中定义,那么获取 2 个 DataTables 的“正确”方式是什么,进行 LEFT JOIN(如 SQL)左边的结果没有被右边的结果排除在哪里?如果我从左边的一个表开始,有 7000 行,我想以 7000 结束。如果没有匹配的记录,用 null 填充它。

我不想定义每一列,它应该返回一个扁平的 Array / DataTable - 像这样:

var JoinedTables = (from c in customer.AsEnumerable()
            join p in billing.AsEnumerable() on (string) c["ResponsiblePartyID"] equals (string) p["ID"] into ps
            from p in ps.DefaultIfEmpty()
            select ALL_COLUMNS
        );

更新:

我使用了在 cmets (Linq return all columns from all tables in the join) 中链接的 Jon Skeet 答案中的示例,他的解决方案确实与我的第一次尝试没有什么不同,它仍然没有解决如何将结果扁平化为单个 DataTable .这是数据和当前输出的示例:

Customers
ID  Resp_ID Name
1   1   Fatafehi
2   2   Dan
3   1   Anthony
4   1   Sekona
5   1   Osotonu
6   6   Robert
7   1   Lafo
8   1   Sarai
9   9   Esteban
10  10  Ashley
11  11  Mitch
12  64  Mark
13  11  Shawn
14  53  Kathy
15  53  Jasmine
16  16  Aubrey
17  17  Peter
18  18  Eve
19  19  Brenna
20  20  Shanna
21  21  Andrea

Billing
ID  30_Day  60_Day
2   null    null
6   null    null
9   null    null
10  null    null
11  null    null
64  null    null
53  null    null
16  null    null
17  null    null
18  null    null
19  null    null
20  -36.52  null
21  1843.30 null

Output:
2   2   Dan 2      null   null  
6   6   Robert  6      null   null  
9   9   Esteban 9      null   null  
10  10  Ashley  10     null   null  
11  11  Mitch   11     null   null  
12  64  Mark    64  -131.20   null
13  11  Shawn   11     null   null  
14  53  Kathy   53     null   null  
15  53  Jasmine 53     null   null  
16  16  Aubrey  16     null   null  
17  17  Peter   17     null   null  
18  18  Eve 18     null   null  
19  19  Brenna  19     null   null  
20  20  Shanna  20   -36.52   null
21  21  Andrea  21  1843.30   null

请注意,结果中缺少 Resp_ID 为 1 的任何人。为了显示输出,我使用了以下内容,然后插入了 null 值进行可视化:

foreach (var row in joinedRows)
{
    Console.WriteLine(row.r1["ID"] + " " + row.r1["Resp_ID"] + " " + row.r1["Name"] + " " + row.r2["ID"] + " " + row.r2["30_Day"] + " " + row.r2["60_Day"]);
}

【问题讨论】:

  • 解决此类问题的一种方法是找到一个反例 - 即数据库中的一个示例,其中包含您认为应该是连接的一部分但不是连接的两条记录。如果仔细查看反例不能单独回答您的问题,您甚至可以创建一组仅包含这两条记录的简单测试表并尝试调试这种情况。
  • 查看Jon's answer
  • 谢谢大家,我用示例数据更新了我的问题

标签: c# linq datatable


【解决方案1】:

所以你有CustomersBillings。每个CustomerId 中都有一个主键,在RespId 中有一个Billing 的外键。

多个客户可以对此外键具有相同的值。通常这将是BillingsCustomers 之间的一对多关系。但是,您的某些Customers 具有不指向任何Billing 的外键值。

class Customer
{
    public int Id {get; set;}            // primary key
    ... // other properties

    // every Customer has exactly one Billing, using foreign key:
    public int RespId {get; set;}        // wouldn't BillingId be a better Name?
}
class Billing
{
    public int Id {get; set;}            // primary key
    ... // other properties
}

现在让我们做一些关注点分离:

我们将您的 DataTables 转换为 IEnumerable<...> 与您的 LINQ 处理分开。这不仅会使您的问题更清晰易懂,而且还可以使其更好地测试、可重用和可维护:如果您的 DataTables 更改为例如数据库或 CSV 文件,您将不必更改 LINQ 语句。

创建 DataTable 的扩展方法以转换为 IEnumerable 并返回。见extension methods Demystified

public static IEnumerable<Customer> ToCustomers(this DataTable table)
{
    ... // TODO: implement
}
public static IEnumerable<Billing> ToBillings(this DataTable table)
{
    ... // TODO: implement
}

public static DataTable ToDataTable(this IEnumerable<Customer> customers) {...}
public static DataTable ToDataTable(this IEnumerable<Billing> billings) {...}

你比我更了解 DataTables,所以我将把编码留给你。更多信息:Convert DataTable to IEnumerableConvert IEnumerable to DataTable

所以现在我们有以下内容:

DataTable customersTable = ...
DataTable billingsTable = ...
IEnumerable<Customer> customers = customersTable.ToCustomers();
IEnumerable<Billing> billings = billingsTable.ToBillings();

我们已准备好进行 LINQ!

您的 Linq 查询

如果使用外键的两个序列之间存在关系,并且您执行完全内连接,您将不会得到没有匹配 BillingCustomers。如果你确实想要它们,你需要一个左外连接:Customers 没有Billing 将有一些Billing 的默认值,通常为空。

LINQ 没有左外连接。你可以找到几个solutions on Stackoverflow on how to mimic a left-outer-join。你甚至可以为此编写一个扩展函数。

public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
    this IEnumerable<TLeft> leftCollection,     // the left collection
    IEnumerable<TRight> rightCollection,        // the right collection to join
    Func<TLeft, TKey> leftKeySelector,          // the function to select left key
    Func<TRight, TKey> rightKeySelector,        // the function to select right key  
    Func<TLeft, TRight, TResult> resultSelector // the function to create the result
    TRight defaultRight,                        // the value to use if there is no right key   
    IEqualityComparer<TKey> keyComparer)        // the equality comparer to use
{
    // TODO: exceptions if null input that can't be repaired
    if (keyComparer == null) keyComparer = EqualityComparer.Default<TKey>();
    if (defaultRight == null) defaultRight = default(TRight);

    // for fast Lookup: put all right elements in a Lookup using the right key and the keyComparer:
    var rightLookup = rightCollection
        .ToLookup(right => rightKeySelector(right), keyComparer);

    foreach (TLeft leftElement in leftCollection)
    {
         // get the left key to use:
         TKey leftKey = leftKeySelector(leftElement);
         // get all rights with this same key. Might be empty, in that case use defaultRight
         var matchingRightElements = rightLookup[leftKey]
             .DefaultIfEmtpy(defaultRight);
         foreach (TRight rightElement in matchingRightElements)
         {
             TResult result = ResultSelector(leftElement, rightElement);
             yield result;
         }
    }
}

为了使这个函数更可重用,创建一个不带 keyComparer 和 defaultRight 参数的重载:

public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
    this IEnumerable<TLeft> leftCollection,     // the left collection
    IEnumerable<TRight> rightCollection,        // the right collection to join
    Func<TLeft, TKey> leftKeySelector,          // the function to select left key
    Func<TRight, TKey> rightKeySelector,        // the function to select right key    
    Func<TLeft, TRight, TResult> resultSelector)// the function to create the result

{    // call the other overload with null for keyComparer and defaultRight
     return LeftOuterJoin(leftCollection, rightCollection,
        leftKeySelector, rightKeySelector, restultSelector, 
        null, null);
}

现在您已经有了这个非常可重用的函数,让我们创建一个函数来左外连接您的客户和帐单:

public static IEnumerable<TResult> LeftOuterJoin<TResult>(
    this IEnumerable<Customer> customers,
    IEnumerable<Billing> billings,
    Func<Customer, Billing, TResult> resultSelector)
{
    return customers.LeftOuterJoin(billings,  // left outer join Customer and Billings
       customer => customer.RespId,           // from every Customer take the foreign key
       billing => billing.Id                  // from every Billing take the primary key
       // from every customer with matching (or default) billings
       // create one result:
       (customer, billing) => resultSelector(customer, billing));                                
}

您没有在结果中指定您想要的内容,您必须自己编写该函数:

 public static IEnumerable<CustomerBilling> LeftOuterJoinCustomerBilling(
    this IEnumerable<Customer> customers,
    IEnumerable<Billing> billings)
 {
      // call the LeftOuterJoin with the correct function to create a CustomerBilling, something like:
      return customers.LeftOuterJoin(billings,
    (customer, billing) => new CustomerBilling()
    {    // select the columns you want to use:
         CustomerId = customer.Id,
         CustomerName = customer.Name,
         ...

         BillingId = billing.Id,
         BillingTotal = billing.Total,
         ...
    });

以 LINQ 方式将所有内容放在一起

DataTable customersTable = ...
DataTable billingsTable = ...
IEnumerable<Customer> customers = customersTable.ToCustomers();
IEnumerable<Billing> billings = billingsTable.ToBillings();
IEnumerable<CustomerBilling> customerBillings = customers.ToCustomerBillings(billing);
DataTable customerBillingTable = result.ToDataTable();

注意,除了最后一个函数之外的所有函数都使用延迟执行:在调用 ToDataTable 之前不会枚举任何内容。

如果需要,您可以将所有内容放在一个大的 LINQ 语句中。这不会大大加快您的流程,但会降低可读性、可测试性和可维护性。

请注意,由于我们将保存数据的方式与处理数据的方式分开,因此如果您决定将数据保存在 CSV 文件或数据库中,或者您希望在CustomerBilling,或者如果您的客户有一些额外的字段。

【讨论】:

  • 如果你说LINQ没有左外连接,左外连接和LINQ方法.GroupJoin()有什么区别?
  • 如果你和 Schools 和 Students 是一对多的,那么 GroupJoin 会返回 "Schools with their Students",所以 [School 1 with each Student with a foreign key to School 1], [School 2 每个学生都有学校的外键 2]。左外连接是 GroupJoin 之后的 SelectMany:[School 1, Student A with SchoolId 1], [School 1, Student B with ShoolId 1], [School 2, Student C with SchoolId 2], [School 2, Student C with SchoolId 2]等,
【解决方案2】:

给定一些示例数据很棒,但如果以可以复制/粘贴的格式使用它,那就更好了。

您的客户与帐单之间的关系是一对多的。其中 many 可以是零、一或多个。由于这个事实,您必须使用.GroupJoin() 而不是.Join()(这是一对一的关系):

var customers = new[]
{
    new Customer{ Id = 1, Resp_Id = 1, Name = "Fatafehi" },
    new Customer{ Id = 2, Resp_Id = 2, Name = "Dan" },
    new Customer{ Id = 3, Resp_Id = 1, Name = "Anthony" },
    new Customer{ Id = 4, Resp_Id = 1, Name = "Sekona" },
    new Customer{ Id = 5, Resp_Id = 1, Name = "Osotonu" },
    new Customer{ Id = 6, Resp_Id = 6, Name = "Robert" },
    new Customer{ Id = 7, Resp_Id = 1, Name = "Lafo" },
    new Customer{ Id = 8, Resp_Id = 1, Name = "Sarai" },
    new Customer{ Id = 9, Resp_Id = 9, Name = "Esteban" },
    new Customer{ Id = 10, Resp_Id = 10, Name = "Ashley" },
    new Customer{ Id = 11, Resp_Id = 11, Name = "Mitch" },
    new Customer{ Id = 12, Resp_Id = 64, Name = "Mark" },
    new Customer{ Id = 13, Resp_Id = 11, Name = "Shawn" },
    new Customer{ Id = 14, Resp_Id = 53, Name = "Kathy" },
    new Customer{ Id = 15, Resp_Id = 53, Name = "Jasmine" },
    new Customer{ Id = 16, Resp_Id = 16, Name = "Aubrey" },
    new Customer{ Id = 17, Resp_Id = 17, Name = "Peter" },
    new Customer{ Id = 18, Resp_Id = 18, Name = "Eve" },
    new Customer{ Id = 19, Resp_Id = 19, Name = "Brenna" },
    new Customer{ Id = 20, Resp_Id = 20, Name = "Shanna" },
    new Customer{ Id = 21, Resp_Id = 21, Name = "Andrea" },
};

var billings = new[]
{
    new Billing{ Id = 2, Day30 = null, Day60 = null },
    new Billing{ Id = 6, Day30 = null, Day60 = null },
    new Billing{ Id = 9, Day30 = null, Day60 = null },
    new Billing{ Id = 10, Day30 = null, Day60 = null },
    new Billing{ Id = 11, Day30 = null, Day60 = null },
    new Billing{ Id = 64, Day30 = null, Day60 = null },
    new Billing{ Id = 53, Day30 = null, Day60 = null },
    new Billing{ Id = 16, Day30 = null, Day60 = null },
    new Billing{ Id = 17, Day30 = null, Day60 = null },
    new Billing{ Id = 18, Day30 = null, Day60 = null },
    new Billing{ Id = 19, Day30 = null, Day60 = null },
    new Billing{ Id = 20, Day30 = -36.52, Day60 = null },
    new Billing{ Id = 21, Day30 = 1843.30, Day60 = null },
};

var aggregate = customers.GroupJoin(
    billings, 
    customer => customer.Resp_Id, 
    billing => billing.Id, 
    (customer, AllBills) => new
    {
        customer.Id,
        customer.Resp_Id,
        customer.Name,
        AllBills
    });

foreach (var item in aggregate)
{
    Console.WriteLine($"{item.Id.ToString().PadLeft(2)}   {item.Resp_Id.ToString().PadLeft(2)}   {item.Name}");

    if(!item.AllBills.Any())
        Console.WriteLine("No bills found!");

    foreach (var bill in item.AllBills)
    {
        Console.WriteLine($"   {bill.Id.ToString().PadLeft(2)}   {bill.Day30}   {bill.Day60}");
    }

    Console.WriteLine();
}

Console.WriteLine("Finished");
Console.ReadKey();

类:

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

public class Billing
{
    public int Id { get; set; }
    public double? Day30 { get; set; }
    public double? Day60 { get; set; }
}

【讨论】:

    【解决方案3】:

    Harald 和 Oliver 提供了很好的答案,但我曾讨论过没有静态类。我从一个二进制平面文件数据库开始,该数据库被逐字节解析为byte[],并在通过使用JSON定义文件添加到DataRows的任何二进制转换之后确定数据类型。结果是一个 API,它可以查询任何平面文件并将其返回到 DataTable,然后可以在不使用静态类的情况下进行查询 - 然后将其转换为 JSON 以发布到 Web API。

    这样我可以快速调整我的查询并传播更改,而不必重新定义静态类和复杂的关系。在上周找到 Linq 之前,我最初计划导出到 SQLite 数据库,然后在其上运行查询。

    由于我对 Linq 还很陌生,因此我学到了很多东西,并且无法确定如何就我称为 .AsEnumerable() 的数据提出问题,然后理解如何修改使用静态类的答案。虽然他们的答案很有价值,并且可能提供性能优势,但由于灵活性要求,它不适合我的用例。这是我使用的精简版:

    DataTable finalResults = ( from cus in customers.AsEnumerable()
        join bill in billing.AsEnumerable().DefaultIfEmpty() on  cus.Field<string>("Resp_ID")  equals age.Field<string>("ID")  into cs
        from c in cs.DefaultIfEmpty() 
        select new
        {
            reference_id = cus["CustomerId"],
            family_id = cus["Resp_ID"],
            last_name = cus["LastName"],
            first_name = cus["FirstName"],
            billing_31_60 = c == null ? "0" : c["billing_31_60"],
            billing_61_90 = c == null ? "0" : c["billing_61_90"],
            billing_over_90 = c == null ? "0" : c["billing_over_90"],
            billing_0_30 = c == null ? "0" : c["billing_0_30"]    
        }).CopyToDataTable();
    

    【讨论】:

      猜你喜欢
      • 2011-09-25
      • 1970-01-01
      • 1970-01-01
      • 2021-04-28
      • 1970-01-01
      • 1970-01-01
      • 2013-03-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多