【问题标题】:Entity Framework 5 wrong data type in queryEntity Framework 5 查询中的数据类型错误
【发布时间】:2013-05-07 16:46:32
【问题描述】:

我们在我们的业务解决方案中使用 EF 5.0 作为我们选择的 ORM,以 n 层方式构建,所有内容都解耦,并使用 ninject 提供了一个不错的组合根。

最近,我们一直在构建一个在底层使用分区的数据库,并且我们在 DATE 列上有一些重要的索引。

在 Sql Server 2008 上正确声明了这些列。我们还在 EF 映射中添加了正确的数据类型,使用 HasColumnType("Date") 指令。

不过,当通过 Linq to Entities 查询表时,我们过滤日期的参数创建为 DateTime2 类型,甚至列在查询中被强制转换为 DateTime2,因此类型与参数匹配。

这种行为有几个问题。首先,如果我告诉 EF 引擎数据库上的列是 DATE,为什么要将其转换为 DateTime2

其次,这种转换使数据库忽略索引,因此不使用分区。我们每个物理分区有一年,如果我问一个日期范围,比如说 2013 年 2 月到 2013 年 3 月,扫描应该只发生在一个物理分区上。如果手动使用正确的数据类型 DATE,它可以正常工作,但使用强制转换为 DateTime2 会扫描所有分区,从而大大降低性能。

现在,我确定我错过了一些东西,因为如果 Microsoft ORM 在 Microsoft Sql Server 上无法正常工作,那将是相当愚蠢的。

我找不到任何关于 EF 如何在查询中使用正确数据类型的文档,所以我在这里问。任何帮助将不胜感激。

谢谢。

【问题讨论】:

    标签: c# .net sql-server entity-framework orm


    【解决方案1】:

    我认为这在实体框架中是不可能的。 This requested enhancement 可能会满足您的需求。 This MSDN page 显示 SQL Server 类型和 CLR 类型之间的映射。请注意,date 受支持并映射到 DateTime,但由于多个 SQL 类型映射到相同的 CLR 类型,EF 显然选择了一种 SQL 类型作为 CLR 类型的首选等效项。

    你能把你的选择代码包装在一个存储过程中吗?如果是这样,这似乎是一个合理的解决方案。您可以使用DbSet{T}.SqlQuery 来实现对象以执行 sp。

    代码示例

    以下简短的控制台应用程序演示了这个概念。请注意相关实体是如何成功延迟加载的。

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity;
    using System.Data.SqlClient;
    using System.Linq;
    
    namespace ConsoleApplication1
    {
        [Table("MyEntity")]    
        public class MyEntity
        {
            private Collection<MyRelatedEntity> relatedEntities;
    
            [Key]
            public virtual int MyEntityId { get; set; }
    
            [DataType(DataType.Date)]
            public virtual DateTime MyDate { get; set; }
    
            [InverseProperty("MyEntity")]
            public virtual ICollection<MyRelatedEntity> RelatedEntities
            {
                get
                {
                    if (this.relatedEntities == null)
                    {
                        this.relatedEntities = new Collection<MyRelatedEntity>();
                    }
    
                    return this.relatedEntities;
                }
            }
    
            public override string ToString()
            {
                return string.Format("Date: {0}; Related: {1}", this.MyDate, string.Join(", ", this.RelatedEntities.Select(q => q.SomeString).ToArray()));
            }
        }
    
        public class MyRelatedEntity
        {
            [Key]
            public virtual int MyRelatedEntityId { get; set; }
    
            public virtual int MyEntityId { get; set; }
    
            [ForeignKey("MyEntityId")]
            public virtual MyEntity MyEntity { get; set; }
    
            public virtual string SomeString { get;set;}
        }
    
        public class MyContext : DbContext
        {
            public DbSet<MyEntity> MyEntities
            {
                get { return this.Set<MyEntity>(); }
            }
        }
    
        class Program
        {
            const string SqlQuery = @"DECLARE @date date; SET @date = @dateIn; SELECT * FROM MyEntity WHERE MyDate > @date";
    
            static void Main(string[] args)
            {
                Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
    
                using (MyContext context = new MyContext())
                {
                    context.MyEntities.Add(new MyEntity
                        {
                            MyDate = DateTime.Today.AddDays(-2),
                            RelatedEntities =
                            {
                                new MyRelatedEntity { SomeString = "Fish" },
                                new MyRelatedEntity { SomeString = "Haddock" }
                            }
                        });
    
                    context.MyEntities.Add(new MyEntity
                    {
                        MyDate = DateTime.Today.AddDays(1),
                        RelatedEntities =
                            {
                                new MyRelatedEntity { SomeString = "Sheep" },
                                new MyRelatedEntity { SomeString = "Cow" }
                            }
                    });
    
                    context.SaveChanges();
                }
    
                using (MyContext context = new MyContext())
                {
                    IEnumerable<MyEntity> matches = context.MyEntities.SqlQuery(
                        SqlQuery,
                        new SqlParameter("@dateIn", DateTime.Today)).ToList();
    
                    // The implicit ToString method call here invokes lazy-loading of the related entities.
                    Console.WriteLine("Count: {0}; First: {1}.", matches.Count(), matches.First().ToString());
                }
    
                Console.Read();
            }
        }
    }
    

    【讨论】:

    • 我在大量导航属性上使用延迟加载。我需要急切地加载所有内容并手动创建我的对象图,这是非常复杂和深入的,所以这不是一个选择。也许迁移到像 NHibernate 这样的不同 ORM 将是正确的选择。
    • @MatteoMosca - 我认为事实并非如此,除非我误解了这个问题。 EF 允许您将 sp 的结果具体化为 attached 实体(根据 MSDN,“由上下文跟踪”),因此延迟加载应该可以工作。你不能做的是支持 eager 使用这种方法加载。
    • 这可能很有趣。我想我错过了这个功能。我会尽快调查的。
    【解决方案2】:

    .NET 和 SQL server 中 DateTime 类型的范围不同。

    .NET DateTime 范围是:0000-Jan-01 到 9999-Dec-31 SQL DateTime 范围是:1900-Jan-01, 2079-Jun-06

    为了匹配范围,EF 将您的 .NET DateTime 转换为 SQL 服务器 DateTime2 类型,该类型与 .NET DateTime 范围具有相同的范围。

    我认为您的问题仅在您具有未分配并通过 EF 传递给 SQL Server 的日期属性时发生。当日期未指定特定值时,默认为 DateTime.Min,即 0000-Jan-01,这会导致转换为 DateTime2。

    我认为你可以让你的 DateTime 属性为空 --> DateTime?或编写一个助手来转换您的 DateTime.Min 以满足 SQL DateTime 范围。

    希望这会有所帮助。

    【讨论】:

    • 其实不是。我们的问题的一个示例与使用两个日期作为搜索范围的搜索有关。 db 上的列是 DATE 而不是 DateTime 或 DateTime2。我向 EF 发送了两个有效的 .net DateTime 值,完全在范围内(例如,2013 年 3 月 1 日和 21013 年 5 月 1 日)并且转换仍然发生。
    【解决方案3】:

    我没有解决办法。我从未见过涉及.NET DateTime 参数的LINQ-to-Entites 查询在SQL 查询中使用了datetime2(7) 以外的参数类型。我怀疑你能摆脱它。试着解释一下为什么会这样:

    假设您有一个实体,其属性为SomeNumber,类型为int。对于这样的查询,您希望得到什么结果:

    ....Where(e => e.SomeNumber >= 7.3)....
    

    可能SomeNumber8 或更大的所有实体。如果(浮点十进制)参数7.3 将转换为存储在数据库中的int 类型,您必须决定如何将7.3 舍入到7(将导致错误结果)或@987654336 @?好的,你可以说,因为我的查询是 &gt;= 并且我知道数据库中的类型是一个整数,四舍五入到 8 必须是正确的。如果我使用&lt;=,那么四舍五入到7 一定是正确的。如果我要使用==,哦……我根本不能舍入,否则我知道结果一定是空的,我可以直接将这个Where 子句翻译成false。和!=true。但是7.0 的参数是一个特例。等等……

    好吧,这个例子中的困境有一个简单的解决方案:首先通过使用int 参数(78)在客户端决定你想要什么。

    DateTime 的解决方案并不是那么简单,因为 .NET 没有 Date 类型。带有DateTime 参数的查询将始终具有以下形式...

    DateTime dateTime = new DateTime(2013, 5, 13, 10, 30, 0);
    ....Where(e => e.SomeDateTime >= dateTime)....
    

    ...如果SomeDateTime 在 SQL Server 中存储为date,您又会遇到舍入困境。我必须投到2013.05.132013.05.14 吗?对于上面的查询,客户肯定会期望所有实体的日期为 14 日及以后。

    好吧,你可以做得很聪明,比如:如果我的 DateTime 参数的时间部分是午夜,则转换为日期部分。如果我使用&gt;= 投射到第二天等等,等等......或者你总是可以投射到datetime2(7)。然后查询的结果总是正确的,并且正如(.NET)客户端所期望的那样。正确...但可能使用次优索引。

    【讨论】:

    • 我不明白为什么你的第一个例子应该是一个问题。 SQL Server 已经允许使用非整数值过滤整数列。考虑这个脚本:gist.github.com/kappa7194/5574037 我可以使用浮点数过滤MyColumn,并且 SQL Server 不会将列转换为浮点数来执行此操作:gist.github.com/kappa7194/5574040 那么为什么在涉及实体框架时它会搞砸呢?
    • 您的解释很清楚,但我仍然认为缺少一些东西。如果 SQL 具有 Date 类型,则 EF 可以忽略 .Net DateTime 对象的时间部分。它甚至有一个 .Date 属性,它返回一个带有午夜时间的 DateTime。您是否建议我们应该在我们的数据库中仅使用符合 .Net 的类型?这似乎是一个很大的限制。
    • 这是一个更合适的例子:gist.github.com/kappa7194/5574997 这里 SQL Server 执行 Clustered Index Seek 搜索值 GT 我指定的 DATETIME,我认为实体框架应该简单地将值传递给底层数据库(如果它支持它们)并让它做它的事情。
    • @MatteoMosca:我同意有可能的解决方案。我不能从我的例子中说服自己。 SQL 的生成方式以及为什么以这种方式生成是一个高级主题。也许您可以尝试将此作为可能的错误报告或请求改进 CodePlex:entityframework.codeplex.com/workitem/list/advancedAlbireo 也提供了很好的示例。
    • @Albireo:这些都是很好的例子,你的分析比我的更有根据,非常值得你在这里自己回答。
    猜你喜欢
    • 1970-01-01
    • 2013-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多