实体框架包括OrderBy随机生成重复数据

时间:2011-10-31 09:57:59

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

当我从包含一些孩子的数据库中检索项目列表(通过.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>

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


问题

如您所见,我们有两个简单的模型(人员和地址),人可以有多个地址 我们为生成的数据库 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. 我可以先从数据库中获取数据,然后通过在.OrderBy之前添加.ToList()来随机化..像这样:var persons = db.Persons.Include(p => p.Addresses).ToList().OrderBy(p => Guid.NewGuid());
  3. 有没有人知道为什么会这样发生?
    这可能是SQL生成中的一个错误吗?

6 个答案:

答案 0 :(得分:8)

正如人们可以通过阅读AakashM answerNicolae Dascalu answer来解决这个问题一样,Linq OrderBy似乎需要一个稳定的排名函数,NewID/Guid.NewGuid不是。

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

为实现此目的,在每次查询之前,使用.Net Random生成器获取随机数。然后将此随机数与实体的唯一属性组合以进行随机排序。并随机化&#39;一点结果,checksum它。 (checksum是一个SQL Server函数,用于计算哈希;原始构思基于this blog。)

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

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

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

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

注意:
我希望使用.Next()方法获取int,然后通过xor(^)将其组合到实体int唯一属性,而不是使用double并乘以它。但遗憾的是SqlFunctions.Checksum并没有为int数据类型提供重载,尽管SQL服务器函数应该支持它。你可以用一个演员来克服这一点,但为了保持简单,我终于选择了乘法。

答案 1 :(得分: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适用于您希望数据按特定顺序(与随机相反)的情况,因此这种假设看似合理。

我认为您应该真正获取数据,然后根据代码中的其他方式随机化它

答案 2 :(得分:5)

tl;博士:这里有一个漏洞的抽象。对我们来说,Include是一个简单的指令,可以将集合的东西粘贴到每个返回的Person行。但EF的Include实现是通过为每个Person-Address组合返回一行,并在客户端重新组合来完成的。按易失性值排序会导致这些行变得混乱,从而破坏了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

因此,对于每个n,我们会为每个A获得1行,而P行只有A行。

然而,添加OrderBy子句会将订购列的 start 按顺序排列:

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的属性,而是volatile,因此对于不同的P-A记录可以不同相同 P ,整个事情就崩溃了。


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

答案 3 :(得分:2)

从理论上讲: 要对项目列表进行排序,比较函数应该相对于项目是稳定的;这意味着对于任何2个项目x,y,x&lt; y应该与查询(调用)的时间相同。

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

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

答案 4 :(得分:0)

定义查询路径以定义查询结果时(使用包含),查询路径仅对返回的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

答案 5 :(得分:0)

我也遇到了这个问题,并通过向我正在获取的主类中添加了Randomizer Guid属性来解决了这个问题。然后像这样(使用EF Core 2)将列的默认值设置为NEWID()

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();

这似乎工作得很好,并且应该为足够大的数据集提供足够的熵。