带有匿名对象和常量列的LINQ to SQL中的奇怪行为

时间:2015-04-07 15:59:11

标签: c# sql-server linq

我的同事在.NET 4.0中使用LINQ to SQL进行更复杂的查询时出错,但在更简单的情况下似乎很容易重现。考虑一个名为TransferJob的表,其中包含合成ID和位字段。

如果我们进行以下查询

using (var ctx = DBDataContext.Create())
{
    var withOutConstant = ctx.TransferJobs.Select(x => new { Id = x.TransferJobID, IsAuto = x.IsFromAutoRebalance });
    var withConstant = ctx.TransferJobs.Select(x => new { Id = x.TransferJobID, IsAuto = true });//note we're putting a constant value in this one

    var typeA = withOutConstant.GetType();
    var typeB = withConstant.GetType();
    bool same = typeA == typeB; //this is true!

    var together = withOutConstant.Concat(withConstant);
    var realized = together.ToList();//invalid cast exception
}

如果需要,会抛出无效的强制转换异常。但奇怪的是,在调试器中查看时,我们有类型相等。

只需将第二行更改为最后一行即可从IQueryable转移到使用linq-to-objects

var together = withOutConstant.ToList().Concat(withConstant.ToList());
var realized = together.ToList();//no problem here

然后一切都按预期正常工作。

经过一些初步的挖掘,我发现看起来LINQ to SQL的程序员正在考虑性能,并且在withConstant版本中显式设置为true的情况下实际上并没有生成的SQL拉常量值。

最后,如果我切换订单,一切似乎都有效:

var together = withConstant.Concat(withOutConstant); //no problem this way

然而,我仍然想知道更好的细节是否真的发生了。我觉得很奇怪,这些将被视为相同类型,但会导致无效的强制转换异常。实际发生在幕后的是什么?我怎么能去证明自己?

堆栈追踪:

at System.Data.SqlClient.SqlBuffer.get_Boolean()
   at Read_<>f__AnonymousType2`2(ObjectMaterializer`1 )
   at System.Data.Linq.SqlClient.ObjectReaderCompiler.ObjectReader`2.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at KBA.GenericTestRunner.Program.Main(String[] args) in c:\Users\nick\Source\Workspaces\KBA\Main\KBA\KBA.GenericTestRunner\Program.cs:line 59
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

生成的SQL如下:

SELECT [t2].[TransferJobID] AS [Id], [t2].[IsFromAutoRebalance] AS [IsAuto]
FROM (
    SELECT [t0].[TransferJobID], [t0].[IsFromAutoRebalance]
    FROM [dbo].[TransferJob] AS [t0]
    UNION ALL
    SELECT [t1].[TransferJobID], @p0 AS [value]
    FROM [dbo].[TransferJob] AS [t1]
    ) AS [t2]
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.34209

顺序颠倒(没有崩溃),SQL是:

SELECT [t2].[TransferJobID] AS [Id], [t2].[value] AS [IsAuto]
FROM (
    SELECT [t0].[TransferJobID], @p0 AS [value]
    FROM [dbo].[TransferJob] AS [t0]
    UNION ALL
    SELECT [t1].[TransferJobID], [t1].[IsFromAutoRebalance]
    FROM [dbo].[TransferJob] AS [t1]
    ) AS [t2]
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.34209

对于我之前的评论,在执行

时不会拉动常数
withConstant.ToList()

SELECT [t0].[TransferJobID] AS [Id]
FROM [dbo].[TransferJob] AS [t0]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.34209

2 个答案:

答案 0 :(得分:8)

together.ToList()构造函数中的枚举期间,我们尝试移动到延迟查询中的下一个元素,现在已解决

MoveNext将从数据库结果中创建一些对象。 数据库查询变为DataReader,并从DataReader中提取行。 现在get_Boolean的实现方式是它对象的VerifyType执行,如果无效则会抛出异常。

您在问题中显示的内容是SqlText together的{​​{1}}查询(以及_sqlText的{​​{1}}),所以我'我被迫作出合理的假设。

TRUE转换为1,FALSE转换为0.转换为位将任何非零值提升为1.

Linq to Sql数据源将转换ctx.TransferJobs Select参数,如

true

以及

中的([table].[column] = 1) 参数
false

所以 - 当你的第一个过滤器不是基于NOT ([table].[column] = 1) 布尔条件时 - 如果Linq Provider获得的对象不是0,则上面的代码行可能会发挥强制转换异常(或者true boolean对应的是什么,我猜是空的。

- 脚注 -

在Linq查询下记录实际sql的帮助程序(当然除Log属性外)

false

(或debugging support中所述的Debug.WriteLine(together.ToString());

<强>更新

在看过SQL之后,一个有效的修复方法是使用DbType属性

将位字段映射为int,如下所示
GetQueryText(query)

相关(旧)VS反馈link,其中错误已被关闭为 [global::System.Data.Linq.Mapping.ColumnAttribute (Storage="_IsFromAutoRebalance", DbType="INT NOT NULL")] public bool IsFromAutoRebalance { get { return this._IsFromAutoRebalance; } 并提供了建议的解决方法

答案 1 :(得分:5)

这是一个L2S错误。从以下事实可以清楚地看出这一点:

  • 这是L2S内部代码中的崩溃。它不是受控/预期的例外。
  • 这应该可行。
  • 对查询的随机更改会导致崩溃消失。

以随机方式修改查询,直到它正常工作。你已经有了一个很好的解决方法。保留C#注释以记录此查询依赖于L2S错误的变通方法。

多年来我发现了十二个L2S错误(发出异常或复杂的查询时)。该产品被废弃,所以最终我们都必须切换到EF。我正在阅读EF提交日志,他们也有查询转换错误。

  

幕后实际发生了什么?

没有太多调查,我无法回答这个问题。可以调试L2S源代码,但这是很多工作。这个问题仅仅是出于好奇心的原因,因为你已经解决了这个问题。

  

我怎么能去证明自己?

证明这是一个错误?我上面给出了一些理由。

  

看起来LINQ to SQL的程序员正在考虑性能,并且在withConstant版本中显式设置为true的情况下,实际上并没有生成的SQL拉常量值。

这对我来说似乎不合理。如果这是真的,我希望所有拉出的对象的值都为true。如果根据您的建议甚至没有从数据库中提取该列,我不会指望无效的强制转换。我认为这是一个查询翻译错误。

另一种解决方法的想法是:

IsAuto = x.IsFromAutoRebalance == x.IsFromAutoRebalance

现在这不再是常量,但在运行时总是如此。 SQL Server查询优化器能够将此代码简化为1。希望L2S不再执行破坏的重写。


更新

从您发布的T-SQL代码中可以看出该错误。参数@p0是一个int,而不是bool。这会导致生成的列升级为int according to the rules。两种情况都是int。显然,在其中一种情况下,L2S尝试将其作为bool获取,而另一种情况则作为int。将它作为一个bool获取不起作用并崩溃。因此,另一种解决方法是将查询转换为使用整数(例如x.IsFromAutoRebalance ? 1 : 01)。