Linq и Join: как написать запрос? (и почему я не получаю "прямое произведение" множеств?)
У меня довольно приличный опыт с SQL, а в Linq я полный профан.
Пока речь идет о fluent syntax и работе с одной последовательностью - вроде всё понятно.
Но при поптыке сделать Join или хотя бы понять синтаксис Join'а я теряюсь.
Я нашел наиболее похожий пример на CodeRoad
Теперь о моей задаче: есть класс Employer и есть класс Phone. У одного сотрудника может быть несколько телефонов. Нужно вывести список сотрудников с их телефонами.
class Employer {
public long ID { get; set; }
public string Name { get; set; }
}
class Phone {
public long ID { get; set; }
public string PhoneNumber { get; set; }
public long EmployerID { get; set; }
}
Я заполняю тестовые таблицы и у меня есть список четырех сотрудников и пяти телефонов:
Employees:
1 Secretar
2 Michail
3 Dmitry
4 Cleaner
Phones:
1 1 401
2 1 501
3 2 699
4 3 520
5 3 407
В SQL я написал бы примерно такой запрос:
select Employees.Id, Name, ( select STRING_AGG (PhoneNumber, ' ') from Phones where EmployerId=Employees.Id ) from Employees
и получил результат, близкий к нужному:
1 Secretar 401 501
2 Michail 699
3 Dmitry 520 407
4 Cleaner NULL
В Linq я написал запрос
// Соединяем людей и их телефоны:
var s = employees.Join(
phones, // inner sequence
e => e.ID, // outerKeySelector
p => p.ID, // innerKeySelector
(e, p) => new { id = e.ID, name = e.Name, phones = p.PhoneNumber })
.Where( < и тут моя фантазия кончилась... > )
И еще один дополнительный вопрос.
Если я в SQL напишу тупой запрос (без WHERE)
select * from Employees, Phones
то я получу 20 строк - все возможные комбинации людей и телефонов
Но в Linq если я напишу первую часть своего запроса
var s = employees.Join(
phones, // inner sequence
e => e.ID, // outerKeySelector
p => p.ID, // innerKeySelector
(e, p) => new { id = e.ID, name = e.Name, phones = p.PhoneNumber })
и сделаю что то типа .ToList(), то мне вернется только кол-во строк, соответсвующее кол-ву людей. Не могли бы Вы объяснить, почему так происходит - я же не написал where, и вроде бы результатом долждно быть "прямое произведение множеств":
Ответы (1 шт):
Уважаемый, M.O., по моему, у Вас есть три проблемы, котрые Вы объединили в один вопрос
Попробую расказать о них последовательно.
- Синтаксис Join
По той ссылке, которую Вы привели в вопросе, содержится "ну так себе" русский перевод вопроса с английского SO. Достаточно взять ссылку на coderoad.ru, и подставить тот же ID - шник в ссылку на stackoverflow.com, полчится ссылка на оригинал вопроса.
Самое главное заблуждение:
когда Вы пишите, что вторым и третьим параметром Join являются ключи тех коллекций, о которым происходит Join - это не совсем так.
Это - те ключи, по которым как раз эти коллекции и джоинятся! То есть, в Вашем случае, если надо телефоны сопоставлять с людьми по хранящейся в телефоне ссылке EmployerID, то именно так и должно быть написано в третьем параметре Join'а (о чем Вам кратко в комментарии написал Alexander Petrov).
(кстати, это заодно и ответ на вопрос "почему не получается прямого произведения": джоин делается в любом случае, просто либо по правильному набору ключей, либо по неправильному)
Тогда получится такая картина:
var q = employees.
Join(
phones,
e => e.ID,
p => p.EmployerID,
(e, p) => new { eid = e.ID, name = e.Name, peid = p.EmployerID, number = p.PhoneNumber }
);
Если это штуку выполнить в чем то вроде LinqPad (очень рекомендую!) то Вы увидите, что Ваш список правильно сджоинился: там будет 5 элементов, где рядом с человеком написан его телефон. Люди с несколькими телефонами присутствуют в списке несколько раз. Последнего человека (у которого телефона нет) в списке не будет.
Дальше можно поступить так:
можно попробовать сгруппировать:
var q2 = employees.
Join(
phones,
e => e.ID,
p => p.EmployerID,
(e, p) => new { eid = e.ID, name = e.Name, peid = p.EmployerID, number = p.PhoneNumber }
).
GroupBy(x => x.eid);
Но... на этом месте я предлагаю остановиться, и перейти к проблеме номер два:
- fluent синтакс не зря так называется.
В формате fluent каждое (отделенное от другого точкой) выражение работает как преобразователь, который порождает новый объект.
Именно эта проблема, как я понял, возникла в месте, где "фантазия кончилась".
То есть, обратите внимание на то, как уже после Join'а написан GroupBy: там используются не те поля, которые есть в исходных объектах.
Также, Where здесь нет: всё, что нужно для Join'а - задано в самом Join'е.
- Как мне кажется, решение задачи вообще выглядит по другому.
Вот такое выражение
var list = employees.Select(
e => new {
id=e.ID,
name = e.Name,
phones = string.Join(", ", phones.Where(p => p.EmployerID == e.ID).Select(p => p.PhoneNumber) )
}).ToList();
даёт результат, очень похожий на тот, который выводит Ваш SQL - запрос:
1 Secretar 401, 501
2 Michail 699
3 Dmitry 520, 407
4 Cleaner
( забавно, что здесь тоже присутствует слово Join, но это "совсем другой" Join: он просто соединяет в одну строку разные строковые переменнные через запятую )
И финальное замечание: у меня есть сомнения насчет производительтности этого решения - я сам знаю Linq только "постольку-поскольку". Возможно, кто то укажет на недостатки такого решения.
Кстати, сам вопрос оформлен - шикарно! Все бы так...
