понедельник, 18 января 2010 г.

Перегруппировка в Linq

Я уже два года пользуюсь Linq, и думал, что знаю его неплохо... Но вдруг спотнулся на, казалось бы, очень простой задачке. Может, кто подскажет?

У меня есть словарь, в котором ключ - это строка, а значение является списком строк (на самом деле это могут быть и не строки, суть не меняется). Пример. Имеется список товаров, заказанных каждым клиентом. Фамилия клиента является ключом (это плохая практика, но допустим). Для каждого запоминаем список кодов товаров (SKU).

var itemsByCustomer = new Dictionary<string, List<string>>();

itemsByCustomer.Add("Ivanov", new List<string>() { "Sony123", "Toshiba5454" });
itemsByCustomer.Add("Petrov", new List<string>() { "Kodak_4e34_d", "Toshiba5454", "Dell_456g" });
и так далее.

Задача: вывести список, отсортированный по коду товара. Т.е. сгруппировать наоборот - сначала получить список клиентов, которые заказали каждый конкретный товар. Вот примерно такой:

var сustomersByItem = new Dictionary<string, List<string>>();

сustomersByItem.Add("Toshiba5454", new List<string>() { "Ivanov", "Petrov" });
сustomersByItem.Add("Kodak_4e34_d", new List<string>() { "Petrov" });
и так далее. А потом отсортировать этот словарь по ключу.

Я не смог. В результате сделал без Linq, т.е. в цикле перебираю клиентов, и вручную добавляю записи в новый словарь. Единственное Linq-решение, которое пришло в голову - это опять-таки в цикле пройтись по клиентам и добавить .Union() для каждого из них. Но клиентов у меня тысячи, и думаю, что производительность будет ужасная (expression tree разрастется).

Спросил коллег - говорят: "О, это очень сложно". Но ведь с использованием обычного SQL всё бы решилась в два счета! Допустим, у нас были бы таблицы Customer(CustomerCode) и CustomerItem(CustomerCode, SKU). Для моей задачи достаточно было бы написать SELECT * FROM CustomerItem ORDER BY SKU .

Неужели и правда наткнулся на ограничение Linq? Или же, как говорится, проблема в ДНК? :)

Игорь Корхов подсказал ответ. Как я и подозревал, всё просто:

var customersByItem =
from ic in itemsByCustomer
from v in ic.Value
orderby v
select new { Value = v, ic.Key };

7 комментариев:

Igor Korkhov комментирует...

var customersByItem =
from ic in itemsByCustomer
from v in ic.Value
orderby v
select new { Value = v, ic.Key };

Игорь Корхов комментирует...

То есть предыдущий запрос выдаст тебе то же, что и "SELECT * FROM CustomerItem ORDER BY SKU".

Это то, что надо? Или тебе надо одним запросом родить сразу Dictionary, а не массив KeyValuePair?

Valik комментирует...

Спасибо, попробую. Нужно перевести в Dictionary, но это я уже и сам умею.

Igor Korkhov комментирует...

var customersByItem =
from ic in itemsByCustomer
from v in ic.Value
orderby v
select new { Key = v, Value = ic.Key };

var items =
from ic in itemsByCustomer
from v in ic.Value
orderby v
group v by v into g
select g.Key;

var customersByItemDict =
from i in items
select new
{
Key = i,
Value =
(from c in customersByItem
where c.Key == i
select c.Value)
};

var result = new Dictionary>();

foreach (var pair in customersByItemDict)
{
result.Add(pair.Key, pair.Value.ToList());
}

Как-то так. Не очень изящно, цикл в данном случае, по мне, был бы понятней.

Valik комментирует...

Думаю, можно сделать через ToDictionary(), одним махом.

Z комментирует...

ставь SQL-server и не забивай себе голову всякими циклами :))))))))

Valik комментирует...

Да уже стоит. Но слишком много чего происходит в реальном времени: запросили один веб-сервис, другой, сравнили, записали результат в кэш и пр. Если всё сначала сохранять в базу, то она будет узким местом.

Ratings by outbrain