【问题标题】:Entity Framework Include OrderBy random generates duplicate dataEntity Framework Include OrderBy随机生成重复数据
【发布时间】:2011-12-18 14:59:18
【问题描述】:

当我从包含一些子项的数据库中检索项目列表(通过 .Include)并随机排序时,EF 给了我一个意想不到的结果..我创建/克隆了添加项..

为了更好地解释自己,我创建了一个小而简单的 EF CodeFirst 项目来重现该问题。 首先我会给你这个项目的代码。

项目

创建一个基本的MVC3项目并通过Nuget添加EntityFramework.SqlServerCompact包。
这会添加以下软件包的最新版本:

  • 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>

这应该是全部,你的应用程序应该编译:)


问题

如您所见,我们有 2 个简单的模型(Person 和 Address),Person 可以有多个 Address。
我们为生成的数据库播种3个人和5个地址。
如果我们从数据库中获取所有人员(包括地址)并随机化结果并打印出这些人员的姓名,那就是一切都出错了。

因此,我有时会得到 4 个人,有时会得到 5 个人,有时会得到 3 个人,而我预计会有 3 个。总是。
例如:

  • 人 1
  • 第三个人
  • 人 1
  • 第三个人
  • 第二个人

所以.. 它正在复制/克隆数据!这并不酷..
似乎 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 =&gt;p.Addresses),一切正常。但当然不会加载地址,并且每次访问该集合都会对数据库进行新的调用。
  2. 我可以先从数据库中获取数据,然后通过在 .OrderBy.. 之前添加一个 .ToList() 来进行随机化,如下所示:var persons = db.Persons.Include(p =&gt; p.Addresses).ToList().OrderBy(p =&gt; Guid.NewGuid());

有人知道为什么会这样吗?
这可能是 SQL 生成中的错误吗?

【问题讨论】:

  • “这可能是 SQL 生成中的错误” - 您是否尝试过执行这两个查询(工作/不工作)并查看它们的结果是否不同?无论如何。我认为这只是您在框架中发现的另一个错误。请参阅msdn.microsoft.com/en-us/library/bb896317.aspx 和“Projecting to an Anonymous Type”,这听起来有点相关。在这个框架中有一些可疑的东西需要被杀死。
  • @stefan:是的,我试过了,当然会返回不同的结果,这确实指向了一个 sql 生成错误。但我不认为它已经在那里列出了。。
  • 顺便说一句,SQL Server 上的 EF 4.1(在 2008 R2 上测试)也会出现问题,而不仅仅是 SQLCompact。我认为,@np-hard 的答案是正确的,问题出在 EF 的对象实现中,而不是在生成的 SQL 中。如果OrderBy 不在父实体的属性上,就会发生废话。

标签: asp.net-mvc linq entity-framework random sql-order-by


【解决方案1】:

由于可以通过阅读AakashM answerNicolae Dascalu answer 对其进行排序,因此很明显Linq OrderBy 需要一个稳定的排名函数,而NewID/Guid.NewGuid 则不需要。

所以我们必须使用另一个在单个查询中稳定的随机生成器。

为此,在每次查询之前,使用 .Net 随机生成器来获取随机数。然后将这个随机数与实体的唯一属性结合起来进行随机排序。并将结果“随机化”一点,checksum 它。 (checksum 是一个计算哈希的 SQL Server 函数;最初的想法基于this blog。)

假设Person Idint,您可以这样编写查询:

// Random instances should be stored and reused, not instanciated at each usage.
// But beware, it is not thread safe. If you want to share it between threads, you
// would have to use locks, see its documentation.
// https://docs.microsoft.com/en-us/dotnet/api/system.random.
// But using locks is a bad idea for scalability, especially in a Web context.
var randomGenerator = new Random();
// ...

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

就像NewGuid hack,这很可能不是一个具有良好分布的好的随机生成器等等。但它不会导致实体在结果中重复。

当心:
如果您的查询排序不能保证您的实体排名的唯一性,您必须对其进行补充以保证它。例如,如果您使用实体的非唯一属性进行校验和调用,则在 OrderBy 之后添加类似 .ThenBy(p =&gt; p.Id) 的内容。
如果您查询的根实体的排名不是唯一的,则其包含的子实体可能会与具有相同排名的其他实体的子实体混合。然后这个 bug 就会留在这里。

注意:
我更喜欢使用.Next() 方法来获得int,然后通过异或(^)将其组合到实体int 唯一属性,而不是使用double 并将其相乘。但不幸的是,SqlFunctions.Checksum 没有为int 数据类型提供重载,尽管 SQL 服务器函数应该支持它。你可以使用强制转换来克服这个问题,但为了简单起见,我最终选择了乘法。

【讨论】:

  • 不错的解决方案!但是性能呢?它看起来一点也不轻巧!
  • 在我当前的数据量和请求负载上,这对用户没有明显影响。但我目前的数据量很轻:要排序的匹配行少于 5 000 个。 (并且由于应用程序的性质,它可能不会超过 50 000。)无论如何,我已经有了一个备用方案:我在 sql 作业中使用相同的技术定期重新计算目标表上的索引 RandomRank 列,对于我需要跨请求进行“静态”随机排序的其他情况。因此,如果动态的性能出现问题,我可能会与企业协商以在任何地方使用静态的。
  • 顺便说一下,这个CheckSum 排序在我看来并不比NewGuid 排序重:两者都会导致服务器没有执行排序的索引; NewID 计算可能并不比乘法和校验和更轻。
  • SqlFunctions 是从哪里来的?如何在 .NET Core 中执行此操作?
  • .Net Core 似乎不可用。请参阅我在上一段中添加的参考链接。
【解决方案2】:

我也遇到了这个问题,并通过向我正在获取的主类添加一个 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();

这似乎工作得很好,并且应该提供足够的熵,即使是大数据集也可以相当随机化。

【讨论】:

    【解决方案3】:

    从理论上: 要对项目列表进行排序,比较函数应该相对于项目是稳定的;这意味着对于任何 2 个项目 x, y,x

    我认为这个问题与对OrderBy method 的规范(文档)的误解有关: keySelector - 从元素中提取键的函数

    EF 没有明确提及所提供的函数是否应该多次为同一个对象返回相同的值(在您的情况下返回不同/随机值),但我认为他们使用的“关键”术语在文档中暗示了这一点。

    【讨论】:

    • 这个blog post 进一步确认使用不稳定的比较功能会导致错误。
    【解决方案4】:

    tl;dr:这里有一个泄漏的抽象。对我们来说,Include 是一个简单的指令,可以将 集合 粘贴到每个返回的 Person 行上。但是 EF 对 Include 的实现是通过为每个 Person-Address 组合返回一整行并在客户端重新组装来完成的。按 volatile 值排序会导致这些行被打乱,从而分解 EF 所依赖的 Person 组。


    当我们查看此 LINQ 的 ToTraceString() 时:

     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
    

    所以我们得到每个An 行,加上没有任何As 的每个P1 行。

    然而,添加一个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
    

    因此,在您的情况下,order-by-thing 不是P 的属性,而是易变的,因此对于 相同的不同 P-A 记录可能不同 P,整个事情都崩溃了。


    我不确定这种行为在working-as-intended ~~~ cast-iron bug 连续体中的哪个位置。但至少现在我们知道了。

    【讨论】:

    • Nicolae answer,它真的可能是“按预期工作”甚至“预期”。 NewGuid 随机排序 hack 太久了,它看起来就像坏了。
    【解决方案5】:

    我认为查询生成没有问题,但是当 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 用于当您希望数据以特定顺序(与随机相反)时使用,因此该假设似乎是合理的。

    我认为你真的应该把数据拿出来,然后根据你代码中的其他方式随机化它

    【讨论】:

    • “固有假设”:我同意并且会猜测相同。在随机排序的情况下,对象实现返回错误的结果,并且可能的修复将要求 EF 对返回的数据进行分组,这将使急切加载的数据的实现变得更加昂贵。在内存中排序也可能更便宜,因为它只对父实体进行排序,而不是像数据库那样对两个连接的(父、子)表进行排序。
    • "OrderBy 是为了当您想要数据以特定顺序(与随机相反)时使用的"随机顺序如何不是特定顺序?同样,计算机科学中的“随机”只是一种特定的算法,因此它只是按唯一的查询序列排序。
    • 在我的辩护中,随机作为形容词确实意味着“缺乏任何明确的计划或预先安排的顺序”:-)
    【解决方案6】:

    在定义查询路径来定义查询结果时,(使用Include),查询路径只对返回的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())
    

    因此您的第二个解决方法可以正常工作:)

    参考:在实体中查询概念模型时加载相关对象 框架 - http://msdn.microsoft.com/en-us/library/bb896272.aspx

    【讨论】:

    • 嘿@Sai,感谢您的反应,但您的查询在语法上不正确。我将其更改为:(from person in db.Persons.Include(p =&gt; p.Addresses) select person).OrderBy(p =&gt; Guid.NewGuid()),但这只会生成相同的 SQL 并保留问题。所以这不是答案。只是我的查询写法不同:)
    猜你喜欢
    • 2014-10-12
    • 1970-01-01
    • 1970-01-01
    • 2012-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-26
    • 2012-01-23
    相关资源
    最近更新 更多