Неужели так сложно просто присоединиться? - PullRequest
0 голосов
/ 31 января 2019

У меня есть входящие данные в форме DataTable.Нет статических классов, к которым можно прибегнуть.У меня есть 2 таблицы, клиент и биллинг.Есть 7000 клиентов, 1200 учетных записей.

Все записи клиентов имеют «ResponsiblePartyID», у нескольких клиентов может быть один и тот же идентификатор, который ссылается на идентификатор таблицы фактур.

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. Если нет подходящих записей, заполните ее нулем.

Я бы не хотел определять каждыйстолбец, он должен вернуть уплощенный массив 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
        );

ОБНОВЛЕНИЕ:

Я использовал пример из ответа Джона Скита, который был связан вкомментарии ( Linq возвращает все столбцы из всех таблиц в соединении ). Его решение действительно ничем не отличается от моей первой попытки, оно все еще не решает, как свести результаты в один 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"]);
}

Ответы [ 3 ]

0 голосов
/ 31 января 2019

Итак, у вас есть Customers и Billings.Каждый Customer имеет первичный ключ в Id и внешний ключ к Billing в RespId.

. Несколько Клиентов могут иметь одинаковое значение для этого внешнего ключа.Обычно это отношение «один ко многим» между Billings и Customers.Тем не менее, некоторые из ваших 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 и обратно.См. методы расширения. Демистифицировано

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 лучше, чем я, поэтому я оставлю вам кодирование.Для получения дополнительной информации: Преобразование DataTable в IEnumerable и Преобразование IEnumerable в DataTable

Итак, теперь у нас есть следующее:

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

Мы готовыLINQ!

Ваш запрос Linq

Если между двумя последовательностями используется внешний ключ и вы выполняете полное внутреннее соединение, вы не получитеCustomers, которые не соответствуют Billing.Если вы хотите их, вам нужно левое внешнее объединение: Customers без Billing будет иметь некоторое значение по умолчанию для Billing, обычно нулевое.

LINQ не имеет левого внешнего-присоединиться.В Stackoverflow вы можете найти несколько решений о том, как имитировать левое внешнее соединение .Вы даже можете написать функцию Extension для этого.

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, или если ваш клиент получит некоторые дополнительные поля.

0 голосов
/ 31 января 2019

Харальд и Оливер дали отличные ответы, но я обсуждал отсутствие статических классов.Я начинаю с двоичной базы данных плоских файлов, которая разбирается побайтно в byte[] и после прохождения любых двоичных преобразований, добавляемых в DataRows с использованием файла определения JSON по пути определения типов данных.В результате получается API, который может запрашивать любой плоский файл и возвращать его в DataTable, который затем можно запрашивать без использования статических классов - который затем преобразуется в JSON для публикации в веб-API.

Таким образом, я могу быстро скорректировать свой запрос и распространять изменения без необходимости переопределять статические классы и сложные отношения.Первоначально я планировал экспортировать в базу данных SQLite, а затем выполнять запросы к ней, прежде чем я нашел Linq на прошлой неделе.

Поскольку я все еще очень плохо знаком с 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();
0 голосов
/ 31 января 2019

Приведенные в качестве примера данные велики, но, если использовать их в формате, который можно использовать для копирования / вставки, было бы намного лучше.

Отношения между вашими клиентами и выставлением счетов являются однозначными.много.Где много может быть ноль, один или несколько.Из-за этого вы должны использовать .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; }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...