Fil*_*sen 17 linq random asp.net-mvc entity-framework sql-order-by
当我从包含一些孩子的数据库中检索项目列表(通过.Include),并随机排序时,EF给我一个意想不到的结果..我创建/克隆添加项目..
为了更好地解释自己,我创建了一个小而简单的EF CodeFirst项目来重现问题.首先,我将为您提供此项目的代码.
创建一个基本的MVC3项目,并通过Nuget添加EntityFramework.SqlServerCompact包.
这会添加以下软件包的最新版本:
模型和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; }
}
}
Run Code Online (Sandbox Code Playgroud)
数据库设置和种子数据: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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
控制器: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());
}
}
}
Run Code Online (Sandbox Code Playgroud)
视图:Index.cshtml
@using RandomWithInclude.Models
@model IList<Person>
<ul>
@foreach (var person in Model)
{
<li>
@person.Name
</li>
}
</ul>
Run Code Online (Sandbox Code Playgroud)
这应该是全部,你的应用程序应该编译:)
如您所见,我们有两个简单的模型(Person和Address),Person可以有多个Addresses.
我们为生成的数据库播种3个人和5个地址.
如果我们从数据库中获取所有人员,包括地址并随机化结果,并打印出这些人员的姓名,那就是出错的地方.
结果,我有时会得到4个人,有时候会有5个人,有时候会有3个人,而且我希望有3个人.
例如:
所以..它正在复制/克隆数据!这并不酷..
看起来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
Run Code Online (Sandbox Code Playgroud)
.Include(p =>p.Addresses)
从查询中删除,一切都很顺利.但是当然没有加载地址,每次访问该集合都会对数据库进行新的调用.var persons = db.Persons.Include(p => p.Addresses).ToList().OrderBy(p => Guid.NewGuid());
有没有人知道为什么会这样发生?
这可能是SQL生成中的一个错误吗?
通过阅读AakashM的回答和Nicolae Dascalu的答案可以解决这个问题,看来Linq OrderBy
需要一个稳定的排名功能,但事实NewID/Guid.NewGuid
并非如此.
所以我们必须使用另一个在单个查询中稳定的随机生成器.
为此,在每次查询之前,使用.Net Random生成器来获取随机数.然后将此随机数与实体的唯一属性组合以进行随机排序.并且将结果'随机化',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));
Run Code Online (Sandbox Code Playgroud)
就像NewGuid
黑客一样,这很可能不是一个好的随机发生器,具有良好的分布等等.但它不会导致实体在结果中重复.
注意:
如果您的查询排序不保证您的实体排名的唯一性,您必须补充它以保证它.例如,如果您使用实体的非唯一属性进行校验和调用,则添加类似于.ThenBy(p => p.Id)
之后的内容OrderBy
.
如果您的排名对于查询的根实体不是唯一的,则其包含的子项可能会与具有相同排名的其他实体的子项混在一起.然后bug就会留在这里.
注意:
我更喜欢使用.Next()
方法来获取int
然后通过xor(^
)将它组合到实体int
唯一属性,而不是使用double
和乘以它.但SqlFunctions.Checksum
遗憾的是,不提供int
数据类型的重载,尽管SQL服务器功能应该支持它.你可以用一个演员来克服这一点,但为了保持简单,我终于选择了加倍.
tl;博士:这里有一个漏洞的抽象.对我们来说,这Include
是一个简单的指令,可以将一组东西粘贴到每个返回的Person
行上.但EF的实现Include
是通过为每个Person-Address
组合返回一整行并在客户端重新组装来完成的.按易失性值排序会导致这些行变得混乱,从而破坏Person
EF所依赖的组.
当我们看看ToTraceString()
这个LINQ时:
var people = c.People.Include("Addresses");
// Note: no OrderBy in sight!
Run Code Online (Sandbox Code Playgroud)
我们看
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
Run Code Online (Sandbox Code Playgroud)
这样我们就得到n
了各行A
,再加上1
每一行对应一个P
没有任何A
秒.
OrderBy
但是,添加一个子句会在有序列的开头放置"按订单排序":
var people = c.People.Include("Addresses").OrderBy(p => Guid.NewGuid());
Run Code Online (Sandbox Code Playgroud)
给
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
Run Code Online (Sandbox Code Playgroud)
所以你的情况,这里的有序通过,事情就不是一个性质P
,但反而易挥发,因此可以针对不同的不同P-A
的记录一样 P
,整个事情分崩离析.
我不确定working-as-intended ~~~ cast-iron bug
这个行为在连续统一体上的位置.但至少现在我们知道了.
我不认为查询生成存在问题,但是当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
Run Code Online (Sandbox Code Playgroud)
即使你通过其他专栏订购,同一个人也会一个接一个地出现.
对于任何连接的查询,此假设大多数都适用.
但我认为这里有一个更深层次的问题.OrderBy适用于您希望数据按特定顺序(与随机相反)的情况,因此这种假设看似合理.
我认为你应该真正获取数据,然后根据代码中的其他方法随机化它
小智 0
当您定义查询路径来定义查询结果时(使用Include),查询路径仅对返回的 ObjectQuery 实例有效。ObjectQuery 的其他实例和对象上下文本身不受影响。此功能允许您链接多个“包含”以进行急切加载。
因此,你的陈述转化为
from person in db.Persons.Include(p => p.Addresses).OrderBy(p => Guid.NewGuid())
select person
Run Code Online (Sandbox Code Playgroud)
而不是你想要的。
from person in db.Persons.Include(p => p.Addresses)
select person
.OrderBy(p => Guid.NewGuid())
Run Code Online (Sandbox Code Playgroud)
因此你的第二个解决方法工作正常:)
参考:在实体框架中查询概念模型时加载相关对象 - http://msdn.microsoft.com/en-us/library/bb896272.aspx
归档时间: |
|
查看次数: |
6460 次 |
最近记录: |