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

nlh*_*lh3 20 c# linq sql-server

我的同事在.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
}
Run Code Online (Sandbox Code Playgroud)

注意到抛出了无效的强制转换异常.但奇怪的是,在调试器中查看时,我们有类型相等.

只需将第二行更改为最后一行即可从IQueryable转换为使用linq转换为对象

var together = withOutConstant.ToList().Concat(withConstant.ToList());
var realized = together.ToList();//no problem here
Run Code Online (Sandbox Code Playgroud)

那么一切都按预期工作正常.

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

最后,如果我切换顺序,一切似乎都有效:

var together = withConstant.Concat(withOutConstant); //no problem this way
Run Code Online (Sandbox Code Playgroud)

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

堆栈跟踪:

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()
Run Code Online (Sandbox Code Playgroud)

生成的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
Run Code Online (Sandbox Code Playgroud)

顺序颠倒(不会崩溃),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
Run Code Online (Sandbox Code Playgroud)

对于我之前的评论,这个常数在做的时候不会被拉

withConstant.ToList()

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

小智 8

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

MoveNext将从数据库结果创建一些对象.将数据库查询转换为a DataReader并从中提取行DataReader.现在get_Boolean实现的方式是它执行VerifyType对象的一个​​并且如果它无效则转换异常.

什么你缺少你的问题,显示的是SqlTexttogether的查询(还有_sqlText你的ctx.TransferJobs),所以我不得不做出一个合理的假设.

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

Linq to Sql数据源将转换Select为类似的true参数

([table].[column] = 1)
Run Code Online (Sandbox Code Playgroud)

并为...中的false参数

NOT ([table].[column] = 1)
Run Code Online (Sandbox Code Playgroud)

所以 - 当你的第一个过滤器不是基于一个true布尔条件时 - 上面的代码行是如果Linq Provider得到一个非0的对象(或者false布尔对应的对象)的话,可能会发挥强制转换异常的地方,猜是空的.

- 脚注 -

在Linq查询下记录实际sql的帮助器(当然除了Log Property)

Debug.WriteLine(together.ToString());
Run Code Online (Sandbox Code Playgroud)

(或调试支持中GetQueryText(query)描述的)

UPDATE

在看过SQL之后,一个工作修复就是使用DbType属性将位字段映射为int,如下所示

    [global::System.Data.Linq.Mapping.ColumnAttribute
(Storage="_IsFromAutoRebalance", DbType="INT NOT NULL")]
            public bool IsFromAutoRebalance
            {
                get
                {
                    return this._IsFromAutoRebalance;
                }
Run Code Online (Sandbox Code Playgroud)

关联错误的相关(旧)VS反馈链接,Won't Fix与建议的解决方法一样


usr*_*usr 5

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

  • 它是L2S内部代码中的崩溃.它不是受控/预期的例外.
  • 这应该工作.
  • 对查询的随机更改会使崩溃消失.

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

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

实际发生在幕后的是什么?

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

我怎么能去证明自己呢?

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

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

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

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

IsAuto = x.IsFromAutoRebalance == x.IsFromAutoRebalance
Run Code Online (Sandbox Code Playgroud)

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


更新:

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