Пользовательское сравнение для метода GroupBy в запросе - PullRequest
0 голосов
/ 09 июля 2020

Я хочу поместить объекты с определенными c значениями (в данном случае (int?)null) в разные группы.

Итак, это:

Id  NullableInt
A      1
B      2
C      1
D     null
E     null
F      1
G     null

Должно получиться так:

Key   Ids
1      A, C, F
2      B
null   D
null   E
null   G

Я использую .GroupBy с настраиваемым компаратором, чтобы попытаться достичь этого.

Проблема в том, что возникает ошибка

LINQ to Entities не распознает метод, и этот метод не может быть преобразован в выражение хранилища

Когда я тестирую выражение LINQ самостоятельно, оно работает, поэтому я предполагаю, что оно не поддерживается или работает иначе в Entity Framework, но я не могу найти никакой информации об этом.

Мой код (упрощенный):

var result = db.Table
    ...
    .GroupBy(
        t => t.NullableInt,
        new NullNotEqualComprare())
    ...
    .ToList();

Очевидно, я хочу сделать как можно больше в самой базе данных.

Код Comparer:

    private class NullNotEqualComparer : IEqualityComparer<int?>
    {
        public bool Equals(int? x, int? y)
        {
            if (x == null || y == null)
            {
                return false;
            }

            return x.Value == y.Value;
        }

        public int GetHashCode(int? obj)
        {
            return obj.GetHashCode();
        }
    }

Я что-то делаю не так, и если это не поддерживается, как я могу решить эту проблему?

Ответы [ 3 ]

0 голосов
/ 09 июля 2020

Увы, вы забыли написать именно требования ваших групп. Я не могу вычесть его из вашего компаратора, потому что компаратор неправильный.

Согласно вашему компаратору, если x равно null, то x не равно x

IEqualityComparer<int?> comparer = new NullNotEqualComparer();
int? x = null;
bool b = comparer(x, x);

Следовательно, x не будет в той же группе, что и x.

Давайте поговорим позже о компараторе, сначала давайте объясним исключение.

IQueryable не может использовать IEqualityComparer

An IQueryable<...> имеет Expression и Provider. Выражение c представляет в общей форме запрос, который должен быть выполнен; Провайдер знает, какой процесс будет выполнять запрос (обычно это система управления базами данных) и какой язык используется для связи с этим процессом (обычно SQL).

Пока вы объединяете операторы LINQ, возвращающие IQueryable<...>, меняется только Expression. К базе данных не обращаются, запрос не выполняется. Только когда вы начинаете перечисление, вызывая GetEnumerator() (напрямую или глубоко в другой функции, например ToList() или foreach), Выражение отправляется Провайдеру, который попытается перевести Выражение в SQL, и выполните запрос. Возвращенные данные представлены в виде IEnumerator<...>, который вы можете использовать для доступа к возвращенным элементам один за другим.

Проблема в том, что поставщик не знает ваш NullNotEqualComparer и, следовательно, не может переведите его в SQL. Фактически, существует несколько методов LINQ, которые не поддерживаются LINQ-to-Entities. См. [Поддерживаемые и неподдерживаемые методы (LINQ to Entities)] 1

Поэтому вам придется попытаться поместить сравнение в keySelector GroupBy.

Intermezzo : your NullNotEqualComparer

Ваш компаратор проверки на равенство не является хорошим компаратором. Он не соответствует требованию, чтобы x равнялся x:

IEqualityComparer<int?> comparer = new NullNotEqualComparer();
int? x = null;
bool b = comparer.Equals(x, x);

int? y = x;
bool c = comparer.Equals(x, y);

int? z = null;
bool d = comparer.Equals(x, z);

Что вы ожидаете и каковы результаты?

Практически всегда правильный компаратор Equality начинается с тех же четырех строк :

public bool equals(MyClass x, MyClass y)
{
    if (x == null) return y == null; // true if both null, false if x null, y not null
    if (y == null) return false;     // false, because x != null and y == null

    // the following two lines are just for efficiency:
    if (object.ReferenceEquals(x, y) return true;
    if (x.GetType() != y.GetType()) return false;

    // here starts the real comparison:
    ...
}

В редких случаях вы хотите, чтобы разные типы объектов были одинаковыми. В этом случае вы не будете проверять тип.

GetHashCode используется для быстрой проверки того, что два объекта различны. Если вам нужно сравнить равенство тысячи объектов, и вы легко обнаружите, что 990 из них отличаются, вам нужно полностью проверить только последние 10 элементов.

Представьте себе класс с 20 свойствами. Для полного равенства нужно проверить все 20 свойств. Если вы выберете свой GetHashCode мудро, возможно, нет необходимости проверять все 20 свойств.

Например, если вы хотите найти всех людей, живущих по одному адресу, вам нужно будет проверить страну, город, PostCode, Street, HouseNumber, ...

Быстрый способ исключить большинство людей из вашей входной последовательности - это проверить только PostCode: если у двух людей разные PostCode, они не будут жить на тот же адрес.

Следовательно, единственное требование к вашему GetHashCode: если Equals(x, y), то GetHashCode(x) == GetHashCode(y). Имейте в виду: не наоборот: могут быть разные x и y, которые имеют одинаковый HashCode. Это легко увидеть: GetHashCode возвращает Int32, поэтому должно быть несколько объектов Int64, которые имеют общий HashCode.

EqualityComparer<int?> comparer = new NullNotEqualComparer();
int? x = null;
int y = comparer.GetHashcode(x);   // <== Exception!

Возвращаясь к вашему вопросу

Мне кажется, что вы создали это компаратор равенства, потому что вам нужна отдельная группа для всех элементов в вашей таблице, для которых значение t.NullableInt равно null.

Id  NullableInt
A      1
B      2
C      1
D     null
E     null
F      1
G     null

Вам нужны три группы:

  • Ключ 1, элементы с Id A, C, F
  • Ключ 2, элемент с Id B
  • Нулевой ключ, элементы с Id D, E, G

Если это то, что вы хотите, вы можете использовать компаратор по умолчанию для класса Nullable<T>:

  • HasValue для x и y ложны: вернуть true
  • HasValue для x и y истинны: вернуть x.Value == y.Value
  • во всех остальных случаях: вернуть false.

Предполагая, что у вас есть метод для переведите строки вашего dt.Table в IQueryable:

IQueryable<MyClass> tableRows = db.Table.ToMyClass();
var result = tableRows.GroupBy(row => row.NullableInt);
0 голосов
/ 14 июля 2020

В конце концов, я пошел на хитрость, но это довольно просто и хорошо работает. Я подумал, что напишу это здесь, поскольку это может быть полезно для других.

В моем вопросе NullableInt на самом деле является идентификатором другой таблицы, и поэтому я знаю, что он всегда будет больше нуля.

По этой причине я могу сделать примерно так:

var result = db.Table
    ...
    .GroupBy(t => t.NullableInt ?? int.MinValue + t.Id)
    ...
    .ToList();
0 голосов
/ 09 июля 2020
  1. Согласно MSDN https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/supported-and-unsupported-linq-methods-linq-to-entities linq-to-entity GroupBy не поддерживает компаратор.

  2. Согласно вашему требованию, вы можете попробовать 2 функции. Первый блок фильтрует все нулевые ключи, затем второй блок группирует ненулевые ключи. Вот так

     var nullKeyList = db.Table.Where(x => x.NullableInt == null).ToList();
    
     var valueKeyGroup = db.Table.Where(x => x.NullableInt != null)
                        .GroupBy(t => t.NullableInt).ToList();
    
...