Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 17.06.2014 | нет комментариев
В статье я описал несколько примеров неочевидных моментов при применении 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();


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

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

Отчего «один и тот же способ» возвращает различные значения для одних и тех же данных? Всё дело в том, что это вовсе не один и тот же способ, а различные способы с идентичными наименованиями. 1-й итерирует по объектам в памяти, 2-й же преобразуется в SQL запрос, тот, что будет исполнен на сервере и вернет нам иной итог. Итоги отличаются из-за наличия среди наших типов действий одного с неопределенным системным именем. И в данном моменте проявляются специфические отличия 2-х сред выполнения: для .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!

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB