Группировать по нескольким столбцам, как если бы это был один столбец - PullRequest
1 голос
/ 01 декабря 2019

Есть ли способ группировки по нескольким столбцам, как если бы столбцы были объединены в C # с использованием Linq? Допустим, у меня есть объект, представляющий сообщение, отправленное одним пользователем другому:

public class Message
{
    public Int32 SenderId { get; set; }
    public Int32 RecipientId { get; set; }
    public String Text { get; set; }
    public DateTime SentAt { get; set; }
}

Я получаю набор из них из репозитория, а затем мне нужно упорядочить их по дате отправки и отфильтровать по паре SenderIdRecipientId. Но я хочу получить только самое последнее сообщение из данного диалога между двумя пользователями. Направление не имеет значения, поэтому (SenderId == 1 && RecipientId == 2) следует рассматривать как (SenderId == 2 && RecipientId == 1).

Пока я получил это:

Messages
    .GroupBy(m => new { m.SenderId, m.RecipientId })
    .Select(gm => gm.OrderByDescending(m => m.SentAt).FirstOrDefault());

Но проблема с этим подходом состоит в том, что когда я получаю сообщение от пользователя 1 к пользователю 2 и наоборот 2 => 1, оба получатбыть возвращенным. Поскольку ключ (1,2) не совпадает с (2,1).

Любые предложения будут хорошими. Спасибо!

UPD: я должен был добавить, что использую выражение с выражением IQueriable. Так что это LINQ to Entity.

Ответы [ 3 ]

2 голосов
/ 02 декабря 2019

Даже если вы пометили свой вопрос , не все, похоже, понимают, что многие решения в памяти дают сбой, потому что у них нет эквивалента SQL. Вот запрос, который будет преобразован в SQL для многих различных IQueryable реализаций и поставщиков запросов:

Messages
    .GroupBy(m => new
    {
        MinId = m.SenderId <= m.RecipientId ? m.SenderId : m.RecipientId,
        MaxId = m.SenderId > m.RecipientId ? m.SenderId : m.RecipientId
    })
    .Select(gm => gm.OrderByDescending(m => m.SentAt).FirstOrDefault());

Использование Math.Min / Math.Max будет работать только в ядре Entity Framework 2, поскольку это единственноеВерсия EF, которая автоматически переключается на оценку на стороне клиента для любой части запроса, которая не переводится в SQL. Это было отменено в ядре 3 EF. В ядре 3 EF применяется только оценка на стороне клиента в окончательном прогнозе (Select), если это необходимо. Таким образом, GroupBy завершится неудачей в EF core 3, который вы собираетесь использовать рано или поздно.

Однако я должен предупредить вас, что в настоящее время это не будет работать в EF core 3, потому чтоони еще не полностью поддерживают GroupBy. Но я уверен, что это вопрос времени.

1 голос
/ 01 декабря 2019

GroupBy принимает пользовательский компаратор равенства . Вы можете реализовать свою конкретную логику сопоставления там следующим образом:

class MessageComparer : IEqualityComparer<Message>
{
    public bool Equals(Message x, Message y)
    {
        return (x.SenderId == y.SenderId || x.SenderId == y.RecipientId)
        && (x.RecipientId == y.RecipientId || x.RecipientId == y.SenderId); // i guess you can go as specific as you like here, depending on your requirements
    }

    public int GetHashCode(Message obj)
    {
        return (obj.SenderId ^ obj.RecipientId).GetHashCode();
    }
}

в своем вызывающем методе, сделать что-то вроде этого, с помощью пользовательского компаратора: Messages.GroupBy(m => m, new MessageComparer()) .Select(gm => gm.OrderByDescending(m => m.SentAt).FirstOrDefault());

UPD: используя LINQ to Entitiesизменит решение совсем немного тогда. Вы правы, пользовательские функции и средства сравнения равенств не будут поддерживаться EF, поскольку ему необходимо каким-то образом перевести это на SQL (даже если вам удастся это сделать, производительность вашего запроса, скорее всего, резко снизится из-за отсутствия функциональных индексов). Я вижу несколько возможностей для изучения:

  1. выберите все сообщения для обоих пользователей без группировки и примените группировку в C # (где у вас есть пользовательские компараторы и вы можете делать что угодно, например так:Messages.Select(gm => gm.OrderByDescending(m => m.SentAt).ToList(/*DB call happens here */).GroupBy(m => m, new MessageComparer()).FirstOrDefault() - это, однако, потенциально извлекает много данных из БД, поэтому вам необходимо рассмотреть возможность фильтрации
  2. изменить схему базы данных, чтобы ввести столбец SequenceNumber , который будет использоваться отправителем иполучатель, и вы можете использовать это в SQL, все индексы, которые вам нравятся,
  3. напишут сохраненный процесс, который выполняет какую-то хитрость с вашими двумя столбцами и дает желаемый результат (например, вы можете отсортировать два целых числа и объединить их всоздайте одно значение, на которое вы затем положитесь).
0 голосов
/ 01 декабря 2019

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

Messages
    .GroupBy(m => new { Math.Min(m.SenderId, m.RecipientId), Math.Max(m.SenderId, m.RecipientId)})
    .Select(gm => gm.OrderByDescending(m => m.SentAt).FirstOrDefault());
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...