【问题标题】:Unexplained behavior when using Entity Framework Code First with List<string>.Contains method将 Entity Framework Code First 与 List<string>.Contains 方法一起使用时出现无法解释的行为
【发布时间】:2014-03-12 16:26:35
【问题描述】:

问题

我们的一些查询遇到超时错误,经过调查和测试,我们已将问题缩小到由 Entity Framework Code First 创建的动态 Sql 查询。

背景

我们正在使用实体框架 v4.3.1,代码优先模型 使用 VS 2010 Premium 编写的代码

我们在数据库中有一个表定义如下,ID为PK

CREATE TABLE [dbo].[Reference](
    [DocumentID] [varchar](100) NOT NULL,
    [DetailID] [varchar](50) NULL,
    [TransactionSet] [char](5) NULL,
    [Qualifier] [varchar](80) NULL,
    [ReferenceID] [nvarchar](30) NULL,
    [Description] [nvarchar](80) NULL,
    [ID] [int] IDENTITY(1,1) NOT NULL

我们有一个实体类定义如下,对应表:

public class Reference
{
    public string DocumentID { get; set; }
    public string DetailID { get; set; }
    public string TransactionSet { get; set; }
    public string Qualifier { get; set; }
    public string ReferenceID { get; set; }
    public string Description { get; set; }
    public int ID { get; set; }
}

我们有如下定义的数据上下文(为简洁起见显示部分类):

public class DataContext : DbContext
{
    public DataContext() : base("Name=DataContext")
    {
        this.Configuration.AutoDetectChangesEnabled = false;
        Database.SetInitializer<DataContext>(null);
    }

    public DbSet<Reference> References { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

        modelBuilder.Entity<Reference>().HasKey(pk => pk.ID);
    }
}

最后调用代码如下:

private List<Reference> getDocumentReferences(List<string> documentIds)
{
    List<Reference> result = null;

    using (var context = new DataContext())
    {
        if (context != null)
        {
            var q = context.References.Where(r => documentIds.Contains(r.DocumentID) && r.Qualifier == "AFN");

            Logger.WriteLogEntry("Sql query:\r\n {0}", q.ToString());   //Added to see the query generated by EF

            result = q.ToList();
        }
    }

    return result;
}

说明

代码执行时,会向Sql Server发送如下查询:

SELECT 
    [Extent1].[ID] AS [ID], 
    [Extent1].[DocumentID] AS [DocumentID], 
    [Extent1].[DetailID] AS [DetailID], 
    [Extent1].[TransactionSet] AS [TransactionSet], 
    [Extent1].[Qualifier] AS [Qualifier], 
    [Extent1].[ReferenceID] AS [ReferenceID], 
    [Extent1].[Description] AS [Description]
FROM [dbo].[Reference] AS [Extent1]
WHERE ([Extent1].[DocumentID] IN (N'A2014011300028343869701A020',N'A2014011300028343869701A021')) AND (N'AFN' = [Extent1].[Qualifier])

请注意,由于代码使用 List 的 Contains 方法,因此它被 EF 转换为 sql 查询中的 IN 子句。 IN 子句中的每个字符串都以 'N' 为前缀,表示一个 unicode 字符串。由于表中的 DocumentID 列是 VARCHAR(不是 NVARCHAR),因此 Sql 必须进行隐式转换才能执行查询。经过调查和测试,我们发现在这种情况下,它不会选择相关的索引,导致表扫描,并且经常出现超时异常。

我们发现,通过删除查询中的 N 前缀,使用了正确的索引并且查询的运行速度提高了几个数量级。

所以为了让 EF 不给字符串加 N 前缀,我在 OnModelCreating 方法的数据上下文类中添加了下面两行:

modelBuilder.Entity<Reference>().Property(r => r.DocumentID).HasColumnType("VARCHAR");
modelBuilder.Entity<Reference>().Property(r => r.Qualifier).HasColumnType("VARCHAR");

当我在本地机器上测试它时,它达到了预期的效果。提交给 Sql Server 的查询如下。请注意,IN 子句中的字符串中不包含 N 前缀:

SELECT 
    [Extent1].[ID] AS [ID], 
    [Extent1].[DocumentID] AS [DocumentID], 
    [Extent1].[DetailID] AS [DetailID], 
    [Extent1].[TransactionSet] AS [TransactionSet], 
    [Extent1].[Qualifier] AS [Qualifier], 
    [Extent1].[ReferenceID] AS [ReferenceID], 
    [Extent1].[Description] AS [Description]
FROM [dbo].[Reference] AS [Extent1]
WHERE ([Extent1].[DocumentID] IN ('A2014011300028343869701A020','A2014011300028343869701A021')) AND ('AFN' = [Extent1].[Qualifier])

我以为我的问题已经解决了,但是当我将此代码部署到测试环境时,N个前缀没有被删除,我不知道为什么。

我可以通过手动创建 Sql 查询来解决这个问题,但是我在这个程序中还有一些查询需要类似的更改,如果我只需要更改 OnModelCreating 方法而不是手动创建每个查询会更容易.

有人对为什么会发生这种情况有任何想法吗?

顺便说一下,我的本地机器使用 Sql Server 2008,而测试环境(和生产环境)使用 Sql 2005。我不确定这是否会影响 EF 生成的查询,因为我的印象是查询是在生成之前生成的提交到数据库。

更新

如果我对测试数据库执行代码在我的盒子上,它可以正常工作。如果代码在测试盒上执行,即使它们都访问同一个数据库,它也不会。

更新 2

代码能否成功运行取决于执行的位置。

EXE Location  |  EXE Executed From  |  Database Server (Version)  |  Result
--------------------------------------------------------------------------
My Box        |  My Box             |  My Box (2008)              |  Success
Test Server   |  My Box             |  Test DB Server (2005)      |  Success
Test Server   |  Test Server        |  Test DB Server (2005)      |  Fail

根据上面的结果,我的本地机器上的某些东西与测试机器上的不同,这似乎是合乎逻辑的。但我不知道如何诊断。我能想到的唯一区别是我的盒子安装了 .Net 4.5 而测试服务器没有。

【问题讨论】:

    标签: c# sql-server entity-framework ef-code-first


    【解决方案1】:

    我已经能够使用DataAnnotations 纠正这种行为:

    public class Reference
    {
        [Column(TypeName = "varchar"), MaxLength(100)]
        public string DocumentID { get; set; }
        [Column(TypeName = "varchar"), MaxLength(50)]
        public string DetailID { get; set; }
        [Column(TypeName = "char"), MaxLength(5)]
        public string TransactionSet { get; set; }
        [Column(TypeName = "varchar"), MaxLength(80)]
        public string Qualifier { get; set; }
        [MaxLength(30)]
        public string ReferenceID { get; set; }
        [MaxLength(80)]
        public string Description { get; set; }
        public int ID { get; set; }
    }
    

    从 EF6 开始,您可以使用 Custom Conventions 更改此 DbContext-wide。

    【讨论】:

    • 谢谢,我会尝试使用注释。我虽然如果我在 OnModelCreating 方法中这样做,我不需要注释。
    • 不幸的是,这只取得了部分成功。带有 Qualifier 列的查询部分不包含 N 前缀,但 IN 子句中的值仍然具有 N 前缀。我的本地机器、测试环境和生产上的 SQL Server 版本不同这一事实是否会产生影响?
    • 很抱歉。我将使用 EF 4.3.1 设置一个环境并找出我错过了什么。
    • 如果我对测试数据库执行代码在我的盒子上,它可以正常工作。如果代码在测试框上执行,即使它们都访问同一个数据库,它也不会。
    • 我无法复制这种行为。我相信您对环境差异的怀疑起了作用。不幸的是,我手边没有 SqlSever2005 的实例。我会继续寻找并报告任何发现。
    猜你喜欢
    • 1970-01-01
    • 2015-06-07
    • 2011-03-18
    • 2011-06-27
    • 1970-01-01
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    • 2017-09-01
    相关资源
    最近更新 更多