我的同事在.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
答案 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错误。从以下事实可以清楚地看出这一点:
以随机方式修改查询,直到它正常工作。你已经有了一个很好的解决方法。保留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 : 0
和1
)。