В статье я описал несколько примеров неочевидных моментов при использовании 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!
Автор: timramone