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