Нечетное поведение в LINQ to SQL с анонимными объектами и постоянными столбцами
Мой коллега получал ошибку с более сложным запросом с использованием LINQ to SQL в .NET 4.0, но, похоже, он легко воспроизводится в более простых условиях. Рассмотрим таблицу с именем TransferJob с синтетическим идентификатором и битовым полем.
Если мы сделаем следующий запрос
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 рассматривали производительность и на самом деле не имеют сгенерированного SQL pull неизменяемого значения в случае с явным значением true в версии withConstant.
Наконец, если я переключаю порядок, все работает:
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
Ответы
Ответ 1
Во время перечисления в конструкторе together.ToList()
мы пытаемся перейти к следующему элементу в отложенном запросе, который теперь разрешен.
MoveNext
собирается создать некоторый объект из результатов базы данных.
Запрос базы данных преобразуется в DataReader
, и строка извлекается из DataReader
.
Теперь get_Boolean
реализуется таким образом, что он выполняет VerifyType
объекта и исключает исключение, если оно недействительно.
В вашем вопросе вам не нужно указывать SqlText
запроса together
(а также _sqlText
вашего ctx.TransferJobs
), поэтому я вынужден сделать разумное предположение.
TRUE преобразуется в 1, а FALSE преобразуется в 0. Преобразование в бит повышает ненулевое значение до 1.
Источник данных Linq to Sql преобразует Select
для параметра true
в нечто вроде
([table].[column] = 1)
и для параметра false
в
NOT ([table].[column] = 1)
Итак - когда ваш первый фильтр не основан на логическом условии true
- указанная выше строка кода - это то, где исключение литья может вступить в игру, если поставщик Linq получает объект, который не равен 0 (или что false
boolean соответствует), моя догадка - это нуль.
- сноска -
Помощник для регистрации фактического sql в запросе Linq (помимо Log Property, конечно)
Debug.WriteLine(together.ToString());
(или GetQueryText(query)
, как описано в отладочной поддержке)
UPDATE
После того, как вы увидели SQL, рабочее исправление просто отображает битовое поле как int, как показано ниже, используя DbType Свойство
[global::System.Data.Linq.Mapping.ColumnAttribute
(Storage="_IsFromAutoRebalance", DbType="INT NOT NULL")]
public bool IsFromAutoRebalance
{
get
{
return this._IsFromAutoRebalance;
}
Связанная (старая) ссылка VS ссылка, где ошибка была закрыта как Won't Fix
с предлагаемым обходным решением
Ответ 2
Это ошибка L2S. Это видно из следующих фактов:
- Это краш во внутреннем коде L2S. Это не контролируемое/ожидаемое исключение.
- Это должно сработать.
- Случайные изменения в запросе приводят к исчезновению сбоя.
Измените запрос случайным образом, пока это не сработает. У вас уже есть обходное решение. Оставьте комментарий С# к документу о том, что этот запрос зависит от обходного пути к ошибке L2S.
Я обнаружил, вероятно, дюжину ошибок L2S на протяжении многих лет (при выдаче необычных или сложных запросов). Продукт заброшен, поэтому в конечном итоге мы все должны переключиться на EF. Я читаю журналы фиксации EF, и они также имеют ошибки перевода запросов.
Что на самом деле происходит под обложками?
Я не могу ответить на это без особого расследования. Можно отлаживать исходный код L2S, но это большая работа. Этот вопрос вызван любопытством только потому, что у вас уже есть обходной путь к этой ошибке.
Как я могу доказать это себе?
Докажите, что это ошибка? Я привел несколько причин.
похоже, что программисты LINQ to SQL рассматривали производительность и на самом деле не имеют сгенерированного SQL pull константное значение в случае с явным значением true в версии withConstant.
Это не кажется мне правдоподобным. Если бы это было так, я ожидал бы, что все вытащенные объекты будут иметь значение true
. Я бы не ожидал недействительный листинг, если этот столбец даже не вытащил из базы данных, как вы предлагаете. Я думаю, что это ошибка перевода запросов.
Идея для другого обходного пути:
IsAuto = x.IsFromAutoRebalance == x.IsFromAutoRebalance
Теперь это уже не константа, но всегда будет верным во время выполнения. Оптимизатор запросов SQL Server способен упростить этот код до 1
. Надеюсь, L2S больше не будет выполнять сломанную переписку.
Update:
Из введенного вами кода T-SQL ошибка очевидна. Параметр @p0
- это int, а не bool. Это приводит к тому, что результирующий столбец будет продвигаться до int в соответствии с правилами. Это int в обоих случаях. По-видимому, в одном из случаев L2S пытается извлечь его как bool, в другом - как int. Получение его как bool не работает и сбой. Таким образом, другой способ заключается в преобразовании запроса для использования ints (например, x.IsFromAutoRebalance ? 1 : 0
и 1
).