Подтвердить что ты не робот

Нечетное поведение в 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
4b9b3361

Ответ 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).