-->

суббота, 30 мая 2015 г.

Баг в Entity Framework - дублирование строк при запросе к SQL VIEW

При выборке из View часть строк дублируется и отличается от результата, возвращаемого SQL Server. Ошибка не нова и связана с тем, что View не содержит столбцов, позволяющих однозначно идентифицировать каждую строку, либо EF не может самостоятельно распознать такие столбцы. Легко лечится. Итак, обо всем по порядку.
Среда выполнения:
.NET 4.5
Entity Framework 6.1.2 (Database First)
SQL Server 2012
Возьмем 2 простейшие таблицы:
И создадим View со следующим запросом:

CREATE VIEW [dbo].[WrongView]
AS
 SELECT P.Name AS ProductName
 ,      O.Name AS OrderName
 FROM       [Product] AS P
 INNER JOIN [Order]   AS O ON P.Id = O.ProductId
Допустим, у нас есть один единственный продукт и 3 привязанных к нему заказа, в этом случае наш View вернет что-нибудь подобное:

SELECT * FROM [WrongView]
ProductName       OrderName
Product #1        Order #1
Product #1        Order #2
Product #1        Order #3
 Однако, если обратиться к этому View посредством EF, результат будет иным:
var allRows = context.WrongView.ToList(); 
ProductName       OrderName
Product #1        Order #1
Product #1        Order #1
Product #1        Order #1
При этом на сервер уйдет абсолютно корректный SQL-запрос, ошибка присутствует в маппинге данных. Для работы с View, EF автоматически назначает один или несколько столбцов ключевыми на уровне EDMX-модели. Если View не содержит подходящего not null столбца, либо EF ошиблась с выбором - возникает вышеописанная проблема с дублированием строк.

Простейший workaround - использование AsNoTracking:
var allRows = context.WrongView.AsNoTracking().ToList();
Почему отсоединение результата запроса от контекста влияет на маппинг, мне не известно. Этот способ плох уже тем, что его необходимо знать и помнить. Немногим лучше - вариант указать столбец вручную в EDMX-модели (я предпочитаю не хранить в EDMX никаких дополнительных настроек, так как потерять их - проще простого. Как показывает практика, удаление таблицы из EDMX с последующим созданием - единственный надежный способ решить конфликты рассинхронизации EDMX и БД).

Самый надежный вариант - решить проблему на уровне SQL Server, и такой способ есть. Добавим во View следующий столбец:
ISNULL(ROW_NUMBER() OVER (ORDER BY P.Name), -1) AS RowId
Функция ROW_NUMBER() нумерует строки в результирующем наборе, а ISNULL() подскажет EF, что столбец является not null и пригодным в качестве ключевого. Мне хватило этого для решения проблемы. Есть еще более усиленный вариант: остальные not null столбцы оборачиваются в вызов функции NULLIF(), а столбец RowId перемещается на первое место:
 SELECT ISNULL(ROW_NUMBER() OVER (ORDER BY P.Name), -1) AS RowId
 ,      NULLIF(P.Name, -1) AS ProductName
 ,      O.Name AS OrderName
Теперь EF не запутается!

Комментариев нет:

Отправить комментарий