Entity Framework Include OrderBy random генерирует повторяющиеся данные

Когда я извлекаю список элементов из базы данных, включая некоторых дочерних элементов (через .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 and Address), а Person может иметь несколько адресов.
Мы засеваем сгенерированную базу данных 3 человека и 5 адресов.
Если мы получим всех людей из базы данных, включая адреса и рандомизируем результаты, и просто распечатаем имена этих лиц, , где все идет не так.

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

  • Лицо 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

Обходные

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

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

Ответ 1

Поскольку можно разобраться, прочитав ответ AakashM и ответ Nicolae Dascalu, кажется, что 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-сервера его поддерживает. Вы можете использовать бросок, чтобы преодолеть это, но для простоты я наконец решил пойти с умножением.

Ответ 2

Я не думаю, что в генерации запросов есть проблема, но есть определенная проблема, когда 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 предназначен для тех случаев, когда вам нужны данные в определенном порядке (как противоположные случайным), так что предположение кажется разумным.

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

Ответ 3

tl; dr: Здесь есть нечеткая абстракция. Для нас Include - простая инструкция вставлять коллекцию вещей в каждую возвращенную строку Person. Но реализация EF Include выполняется путем возврата целой строки для каждого комбо 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 s.

Добавление предложения OrderBy, тем не менее, помещает вещь на заказ в начало упорядоченных столбцов:

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 это поведение падает. Но по крайней мере теперь мы знаем об этом.

Ответ 4

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

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

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

Ответ 5

Когда вы определяете путь запроса для определения результатов запроса (используйте Включить), путь запроса действителен только для возвращаемого экземпляра объекта 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())

Следовательно, ваше второе обходное решение отлично работает:)

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

Ответ 6

Я также столкнулся с этой проблемой и решил ее, добавив свойство 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();

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