Не стреляйте себе в ногу, используя LINQ

В статье я описал несколько примеров неочевидных моментов при использовании LINQ to SQL. Если вы гуру .NET, вам, возможно, покажется это скучным, остальным — добро пожаловать!
Начнем с такого примера. Допустим, у нас есть сущность «тип действия». У типа действия есть human-readable имя и системное имя — некий уникальный идентификатор, по которому с объектами этой сущности мы сможем работать из кода. Вот такая структура в виде объектов в коде:

class ActionType { 	public int id; 	public string systemname; 	public string name; } 

var ActionTypes = new ActionType[] { 	new ActionType { 		id = 1, 		systemname = "Registration", 		name = "Регистрация" 	}, 	new ActionType { 		id = 2, 		systemname = "LogOn", 		name = "Вход на сайт" 	}, 	new ActionType { 		id = 3, 		systemname = null, 		name = "Некоторый тип действия без системного имени" 	} }; 

Для такой же структуры с аналогичными данными создана таблица в БД и вспомогательные объекты для использования LINQ to SQL. Допустим, нам необходимо выяснить, существует ли у нас тип действия с системным именем NotExistingActionType. Вопрос в том, что будет выведено на экран после выполнения этих инструкций:

var resultForObjects = ActionTypes.All(actionType => actionType.systemname != "NotExistingActionType"); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.ActionTypes.All(actionType => actionType.SystemName != "NotExistingActionType");  Console.WriteLine("Result for objects: " + resultForObjects + "\nResult for Linq to sql: " + resultForLTS); Console.ReadLine();


Ответ в данном случае на первый взгляд странный. Результатом работы приложения будет:

Result for objects: True
Result for LINQ to sql: False

Почему «один и тот же метод» возвращает разные значения для одних и тех же данных? Всё дело в том, что это совсем не один и тот же метод, а разные методы с одинаковыми названиями. Первый итерирует по объектам в памяти, второй же преобразуется в SQL запрос, который будет выполнен на сервере и вернет нам другой результат. Результаты отличаются из-за наличия среди наших типов действий одного с неопределенным системным именем. И в данном моменте проявляются специфические различия двух сред выполнения: для .NET выражение null != objRef — истина (если конечно objRef не null), а следовательно и значение выражения «системные имена всех типов действий не равны NotExistingActionType» в нашей ситуации будет истинным.
Но LINQ to SQL выражения преобразуются в SQL и выполняются на сервере, и в SQL сравнение с NULL работает по-другому. Значения выражений NULL == Something, NULL != Something и даже NULL == NULL всегда будут ложными, таков стандарт, поэтому выражение «системные имена всех типов действий не равны NotExistingActionType» и не будет истинной, так как NULL != ‘NotExistingActionType’ — ложь.

Теперь рассмотрим другой пример. Допустим, у нас есть пользователи с их текущим балансом:

class User { 	public int id; 	public int balance; 	public string name; } 

var users = new User[] {  	new User {                      		id = 1,  		name = "Василий", 		balance = 0 	}, 	new User {                      		id = 2,  		name = "Георгий", 		balance = 0 	} }; 

Вопрос в том, что должна возвращать сумма по пустому набору элементов. Для меня, например, очевидным значением является 0, но тут тоже не всё так просто. Выполним что-то типа такого:

var resultForObjects = users.Where(user => user.id < 0).Sum(user => user.balance); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.Users.Where(user => user.Id < 0).Sum(user => user.Balance);  Console.WriteLine("Result for objects: " + resultForObjects + "\nResult for Linq to sql: " + resultForLTS); Console.WriteLine(context.ActionTypes.First().Name); Console.ReadLine(); 

Результат снова будет необычным. Вообще говоря, выполнить эти инструкции в таком виде мы не сможем, так как при выполнении возникнет исключение:

System.InvalidOperationException: «Значение NULL не может быть присвоено члену, который является типом System.Int32, не допускающим значения NULL.»

Причины происходящего снова в трансляции наших вызовов в SQL. Для обычного IEnumerable экстеншн Sum возвращает 0 для пустого набора, в чем легко убедиться, не вычисляя resultForLTS (ну или в конце концов прочитав это вот тут msdn.microsoft.com/ru-ru/library/bb549046). Однако СУБД вычисляет сумму пустого набора как NULL (правильно это или нет — вопрос довольно холиварный, но сейчас это просто факт), и LINQ, пытаясь вернуть null вместо целого числа, немедленно терпит фиаско. Починить это место крайне просто, но необходимо держать ухо востро:

var resultForLTS = context.Users.Where(user => user.Id < 0).Sum(user => (int?)user.Balance) ?? 0;

Тут возвращаемое значение функции Sum становится не int, а nullable int (этого можно добиться и явным указанием типа generic’а), что дает возможность LINQ вернуть null, а оператор ?? превратит этот null в 0.

Ну и последний пример. Удивительно, но трансляция в SQL дает нам немного синтаксического сахара. Рассмотрим вот какой пример. Добавим объект Location, и у пользователей теперь будет ссылка на их город:

class User { 	public int id; 	public int balance; 	public string name; 	public Location location; }  class Location { 	public int id; 	public string Name; } 

Не будем создавать никаких объектов Location и изменять пользователей, интерес представляет вот такой код:

var resultForObjects = users.Select(user =>  	user.location == null ?  		"Локация не указана" : user.location.Name == null ?  			"Локация не указана" : user.location.Name) 	.First(); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.Users.Select(user =>  	user.Location == null ?  		"Локация не указана" : user.Location.name == null ?  			"Локация не указана" : user.Location.name) 	 .First(); 

В обоих случаях результатом будет строка «Локация не указана», так как она действительно не указана, но что будет, если написать вот так:

var resultForLTS = context.Users.Select(user => user.Location.name ?? "Локация не указана");

Вы можете подумать, что так это не будет работать, так как тут присутствует явный NullReferenceException (ни один пользователь не имеет объекта Location априори, мы их не создавали, в базу не записывали), но не забываем, что этот код не будет запущен в окружении .NET, а будет транслирован в SQL и запущен СУБД. На самом деле, запрос, который получится из этого кода, будет выглядеть так (LINQPad в помощь):

SELECT COALESCE([t1].[name],@p0) AS [value] FROM [Users] AS [t0] LEFT OUTER JOIN [Locations] AS [t1] ON [t1].[Id] = [t0].[LocationId] 

Этот «трюк» позволяет нам не писать дикое количество тернарных операторов в запросах на LINQ.

Вывод:

Когда мы пишем код, мы постоянно полагаемся на функции более низкого уровня и считаем, что эти функции работают верно. Прекрасно, что есть такой способ сокращения сложности, но нужно всегда отдавать себе отчет в том, достаточно хорошо ли мы понимаем, что сделает та или иная функция, которую мы используем. А для этого — RTFM!

ссылка на оригинал статьи http://habrahabr.ru/post/203836/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *