Entity Framework Включить OrderBy в случайном порядке генерирует дубликаты данных - PullRequest
17 голосов
/ 31 октября 2011

Когда я получаю список элементов из базы данных, включая некоторые дочерние элементы (через .Include), и упорядочиваю случайным образом, EF дает мне неожиданный результат .. Я создаю / клонирую дополнительные элементы ...

Чтобы объяснить себя лучше, я создал небольшой и простой проект EF CodeFirst, чтобы воспроизвести проблему. Сначала я дам вам код для этого проекта.

Проект

Создайте базовый проект MVC3 и добавьте пакет EntityFramework.SqlServerCompact через Nuget.
Это добавляет последние версии следующих пакетов:

  • EntityFramework v4.3.0
  • SqlServerCompact v4.0.8482.1
  • EntityFramework.SqlServerCompact v4.1.8482.2
  • WebActivator v1.5

Модели и DbContext

using System.Collections.Generic;
using System.Data.Entity;

namespace RandomWithInclude.Models
{
    public class PeopleContext : DbContext
    {
        public DbSet<Person> Persons { get; set; }
        public DbSet<Address> Addresses { get; set; }
    }

    public class Person
    {
        public int ID { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Address> Addresses { get; set; }
    }

    public class Address
    {
        public int ID { get; set; }
        public string AdressLine { get; set; }

        public virtual Person Person { get; set; }
    }
}

Данные настройки БД и семян: EF.SqlServerCompact.cs

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using RandomWithInclude.Models;

[assembly: WebActivator.PreApplicationStartMethod(typeof(RandomWithInclude.App_Start.EF), "Start")]

namespace RandomWithInclude.App_Start
{
    public static class EF
    {
        public static void Start()
        {
            Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
            Database.SetInitializer(new DbInitializer());
        }
    }
    public class DbInitializer : DropCreateDatabaseAlways<PeopleContext>
    {
        protected override void Seed(PeopleContext context)
        {
            var address1 = new Address {AdressLine = "Street 1, City 1"};
            var address2 = new Address {AdressLine = "Street 2, City 2"};
            var address3 = new Address {AdressLine = "Street 3, City 3"};
            var address4 = new Address {AdressLine = "Street 4, City 4"};
            var address5 = new Address {AdressLine = "Street 5, City 5"};
            context.Addresses.Add(address1);
            context.Addresses.Add(address2);
            context.Addresses.Add(address3);
            context.Addresses.Add(address4);
            context.Addresses.Add(address5);
            var person1 = new Person {Name = "Person 1", Addresses = new List<Address> {address1, address2}};
            var person2 = new Person {Name = "Person 2", Addresses = new List<Address> {address3}};
            var person3 = new Person {Name = "Person 3", Addresses = new List<Address> {address4, address5}};
            context.Persons.Add(person1);
            context.Persons.Add(person2);
            context.Persons.Add(person3);
        }
    }
}

Контроллер: HomeController.cs

using System;
using System.Data.Entity;
using System.Linq;
using System.Web.Mvc;
using RandomWithInclude.Models;

namespace RandomWithInclude.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var db = new PeopleContext();
            var persons = db.Persons
                                .Include(p => p.Addresses)
                                .OrderBy(p => Guid.NewGuid());

            return View(persons.ToList());
        }
    }
}

Просмотр: Index.cshtml

@using RandomWithInclude.Models
@model IList<Person>

<ul>
    @foreach (var person in Model)
    {
        <li>
            @person.Name
        </li>
    }
</ul>

это должно быть все, и ваше приложение должно скомпилироваться:)


Проблема

Как видите, у нас есть две простые модели (Person и Address), и Person может иметь несколько адресов.
Заполняем сгенерированную базу данных 3 человека и 5 адресов.
Если мы возьмем всех людей из базы данных, включая адреса, рандомизируем результаты и просто распечатаем имена этих людей, то вот где все пойдет не так.

В результате я иногда получаю 4 человека, иногда 5, а иногда 3, и я ожидаю 3. Всегда.
e.g.:

  • Человек 1
  • Человек 3
  • Человек 1
  • Персона 3
  • Человек 2

Итак ... это копирование / клонирование данных! И это не круто ..
Просто кажется, что EF теряет информацию о том, какие адреса являются потомками какого человека ..

Сгенерированный SQL-запрос выглядит так:

SELECT 
    [Project1].[ID] AS [ID], 
    [Project1].[Name] AS [Name], 
    [Project1].[C2] AS [C1], 
    [Project1].[ID1] AS [ID1], 
    [Project1].[AdressLine] AS [AdressLine], 
    [Project1].[Person_ID] AS [Person_ID]
FROM ( SELECT 
    NEWID() AS [C1], 
    [Extent1].[ID] AS [ID], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[ID] AS [ID1], 
    [Extent2].[AdressLine] AS [AdressLine], 
    [Extent2].[Person_ID] AS [Person_ID], 
    CASE WHEN ([Extent2].[ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
    FROM  [People] AS [Extent1]
    LEFT OUTER JOIN [Addresses] AS [Extent2] ON [Extent1].[ID] = [Extent2].[Person_ID]
)  AS [Project1]
ORDER BY [Project1].[C1] ASC, [Project1].[ID] ASC, [Project1].[C2] ASC

Обходные

  1. Если я удаляю .Include(p =>p.Addresses) из запроса, все идет хорошо. но, конечно, адреса не загружены, и при доступе к этой коллекции каждый раз будет выполняться новый вызов базы данных.
  2. Сначала я могу получить данные из базы данных, а затем рандомизировать, просто добавив .ToList () перед .OrderBy .., например: var persons = db.Persons.Include(p => p.Addresses).ToList().OrderBy(p => Guid.NewGuid());

Кто-нибудь знает, почему это происходит так?
Может ли это быть ошибкой в ​​генерации SQL?

Ответы [ 6 ]

8 голосов
/ 06 июля 2015

Поскольку можно разобраться, прочитав AakashM answer и Nicolae Dascalu answer , кажется, что Linq OrderBy требует стабильной функции ранжирования, а NewID/Guid.NewGuid - нет.

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

Для этого перед каждым запросом используйте генератор случайных чисел .Net, чтобы получить случайное число. Затем объедините это случайное число с уникальным свойством объекта, чтобы получить случайную сортировку. И чтобы немного «рандомизировать» результат, checksum это. (checksum - это функция SQL Server, которая вычисляет хэш; оригинальная идея основана на этом блоге .)

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

var rnd = (new Random()).NextDouble();
var persons = db.Persons
    .Include(p => p.Addresses)
    .OrderBy(p => SqlFunctions.Checksum(p.Id * rnd));

Как и взлом NewGuid, очень вероятно, что это не хороший генератор случайных чисел с хорошим распределением и так далее. Но это не приводит к дублированию сущностей в результатах.

Внимание:
Если ваш порядок запросов не гарантирует уникальность вашего рейтинга, вы должны дополнить его, чтобы гарантировать его. Например, если вы используете неуникальное свойство ваших сущностей для вызова контрольной суммы, то добавьте что-то вроде .ThenBy(p => p.Id) после OrderBy.
Если ваш рейтинг не уникален для вашего запрашиваемого корневого объекта, включенные в него дочерние элементы могут смешиваться с дочерними элементами других объектов, имеющих такой же рейтинг. И тогда ошибка останется здесь.

Примечание:
Я бы предпочел использовать метод .Next(), чтобы получить int, а затем объединить его через xor (^) с уникальным свойством объекта int, вместо использования double и умножить его. Но SqlFunctions.Checksum, к сожалению, не обеспечивает перегрузки для типа данных int, хотя предполагается, что функция сервера SQL поддерживает его. Вы можете использовать приведение, чтобы преодолеть это, но для простоты я наконец решил пойти с умножением.

5 голосов
/ 17 ноября 2011

tl; dr: Здесь есть дырявая абстракция.Для нас Include - это простая инструкция для прикрепления коллекции вещей к каждой возвращаемой строке Person.Но реализация Include в EF выполняется путем возврата целой строки для каждой комбинации Person-Address и повторной сборки на клиенте.Упорядочение по изменчивому значению приводит к тому, что эти строки перемешиваются, разделяя группы Person, на которые полагается EF.


Когда мы посмотрим на ToTraceString() для этого LINQ:

 var people = c.People.Include("Addresses");
 // Note: no OrderBy in sight!

мы видим

SELECT 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[C1] AS [C1], 
[Project1].[Id1] AS [Id1], 
[Project1].[Data] AS [Data], 
[Project1].[PersonId] AS [PersonId]
FROM ( SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[PersonId] AS [PersonId], 
    [Extent2].[Data] AS [Data], 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM  [Person] AS [Extent1]
    LEFT OUTER JOIN [Address] AS [Extent2] ON [Extent1].[Id] = [Extent2].[PersonId]
)  AS [Project1]
ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC

Итак, мы получаем n строк для каждого A, плюс 1 ряд для каждого P без каких-либо A с.

Однако добавление предложения OrderBy позволяет упорядочить элементы в начале 1027 * упорядоченных столбцов:

var people = c.People.Include("Addresses").OrderBy(p => Guid.NewGuid());

дает

SELECT 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[C2] AS [C1], 
[Project1].[Id1] AS [Id1], 
[Project1].[Data] AS [Data], 
[Project1].[PersonId] AS [PersonId]
FROM ( SELECT 
    NEWID() AS [C1], 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[PersonId] AS [PersonId], 
    [Extent2].[Data] AS [Data], 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
    FROM  [Person] AS [Extent1]
    LEFT OUTER JOIN [Address] AS [Extent2] ON [Extent1].[Id] = [Extent2].[PersonId]
)  AS [Project1]
ORDER BY [Project1].[C1] ASC, [Project1].[Id] ASC, [Project1].[C2] ASC

Так что в вашем случае, когда упорядоченная вещь не является свойством P, но вместо этого является изменчивой, и, следовательно, может отличаться для различных P-A записей то же самое P, все разваливается.


Я не уверен, где на working-as-intended ~~~ cast-iron bug континууме это поведение падает.Но, по крайней мере, теперь мы знаем об этом.

5 голосов
/ 15 ноября 2011

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

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

например, результат объединенного запроса всегда будет

P.Id P.Name  A.Id A.StreetLine
1    Person 1 10    --- 
1    Person 1 11
2    Person 2 12
3    Person 3 13
3    Person 3 14 

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

это предположение в основном верно для любого присоединенного запроса.

Но здесь, на мой взгляд, есть более глубокая проблема.OrderBy предназначен для случаев, когда вам нужны данные в определенном порядке (в противоположность случайному), поэтому такое предположение кажется разумным.

Я думаю, что вы действительно должны получить данные и затем рандомизировать их в соответствии с некоторыми другими средствами в вашем коде

2 голосов
/ 18 ноября 2011

Из теории: Чтобы отсортировать список элементов, функция сравнения должна быть стабильной относительно элементов; это означает, что для любых 2 элементов x, y результат x

Я думаю, что проблема связана с неправильным пониманием спецификации (документации) OrderBy метода : keySelector - функция для извлечения ключа из элемента .

EF не упоминал явно, должна ли предоставленная функция возвращать одно и то же значение для одного и того же объекта, который вызывается много раз (в вашем случае возвращает разные / случайные значения), но я думаю, что «ключ» термин, который они использовали в документации, подразумевал это.

0 голосов
/ 10 мая 2019

Я также столкнулся с этой проблемой и решил ее, добавив свойство Randomizer Guid в основной класс, который я выбирал. Затем я устанавливаю значение столбца по умолчанию в NEWID () следующим образом (используя EF Core 2)

builder.Entity<MainClass>()
    .Property(m => m.Randomizer)
    .HasDefaultValueSql("NEWID()");

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

var rand = new Random();
var randomIndex1 = rand.Next(0, 31);
var randomIndex2 = rand.Next(0, 31);
var taskSet = await DbContext.MainClasses
    .Include(m => m.SubClass1)
        .ThenInclude(s => s.SubClass2)
    .OrderBy(m => m.Randomizer.ToString().Replace("-", "")[randomIndex1])
        .ThenBy(m => m.Randomizer.ToString().Replace("-", "")[randomIndex2])
    .FirstOrDefaultAsync();

Кажется, это работает достаточно хорошо и должно обеспечить достаточную энтропию, чтобы даже большой набор данных был достаточно рандомизирован.

0 голосов
/ 31 октября 2011

Когда вы определяете путь запроса для определения результатов запроса (используйте Включить ), путь запроса действителен только для возвращенного экземпляра ObjectQuery.Другие экземпляры ObjectQuery и сам контекст объекта не затрагиваются.Эта функциональность позволяет объединять несколько «включений» для быстрой загрузки.

Поэтому ваше заявление преобразуется в

from person in db.Persons.Include(p => p.Addresses).OrderBy(p => Guid.NewGuid())
select person

вместо того, что вы намеревались.

from person in db.Persons.Include(p => p.Addresses)
select person
.OrderBy(p => Guid.NewGuid())

Следовательноваш второй обходной путь работает нормально:)

Ссылка: загрузка связанных объектов при запросе концептуальной модели в Entity Framework - http://msdn.microsoft.com/en-us/library/bb896272.aspx

...