Проектирование баз данных и работа с ними Веб-приложений. LINQ, ADO.NET Entities, DDD
Презентацию к данной лекции Вы можете скачать здесь.
10.1. Доступ к данным в .NET
Продолжим рассмотрение технологий .NET для доступа к данным. В данной лекции рассмотрим технологии LINQ и LINQ to SQL, а также ADO.NET Entity Framework.
10.1.1. LINQ
10.1.1.1. Общие сведения
Language Integrated Query ( LINQ ) – проект Microsoft по добавлению синтаксиса языка запросов, напоминающего SQL, в языки программирования платформы .NET Framework [1]. LINQ выпущен вместе с Visual Studio 2008 в конце ноября 2007 года.
LINQ (Language-Integrated Query) представляет собой набор функций Visual Studio 2008, расширяющих мощные возможности запроса в синтаксисе языка C# и Visual Basic. LINQ представляет стандартные шаблоны для создания запросов и обновления данных; технология может быть расширена для поддержки потенциально любого типа хранилища данных [2]. Visual Studio 2008 включает сборки поставщиков LINQ, позволяющие использовать LINQ с коллекциями платформы .NET Framework, базами данных SQL Server, наборами данных ADO.NET и XML-документами.
LINQ представляет собой набор расширений языка, поддерживающий формирование запросов данных способом, безопасным по типам. Запрашиваемые данные могут быть представлены в форме XML (запросы LINQ к XML), баз данных (ADO.NET с поддержкой LINQ, куда входят LINQ к SQL, LINQ к наборам данных и LINQ к экземплярам), объектов ( LINQ к объектам) и т.д. Архитектура LINQ показана на рис. 10.1 [3].
Рассмотрим пример интегрированного запроса [3]:
var contacts = from c in customers where c.City == "Москва" select new { c.Name, c.Phone };
Здесь из некоторого источника данных customers выбираются имена и телефоны клиентов, проживающих в городе Москва. Запрос очень похож на язык SQL, однако использует строго типизированный синтаксис.
Этот же запрос можно записать и в другом виде – используя лямбда-выражения и методы расширения (рис. 10.2). Именно к такому виду приведет код, приведенный в примере выше, компилятор.
Используя некоторые новые особенности языка, LINQ позволяет использовать SQL-подобный синтаксис непосредственно в коде программы, написанной, например, на языке C#:
- Анонимные типы;
- Методы расширения;
- Лямбда-исчисление;
- Дерево выражений;
- Стандартные операторы языка запросов.
Рассмотрим подробнее основные принципы работы с LINQ на C# [2, 3, 4, 5, 6, 7, 8].
10.1.1.2. Введение в запросы LINQ
LINQ предлагает согласованную модель для работы с данными в различных видах источников данных и в различных форматах. В запросе LINQ работа всегда осуществляется с объектами. Для запросов и преобразований данных в XML-документах, базах данных SQL, наборах данных ADO.NET, коллекциях .NET и любых других форматах, для которых доступен поставщик LINQ, используются одинаковые базовые шаблоны кодирования.
Все операции запроса LINQ состоят из трех различных действий.
- получение источника данных;
- создание запроса;
- выполнение запроса.
В следующем примере показано выражение этих трех частей операции запроса в исходном коде. В примере в качестве источника данных для удобства используется массив целых чисел; тем не менее, те же принципы применимы и к другим источникам данных. Оставшаяся часть раздела ссылается на этот пример:
class IntroToLINQ { static void Main() { // 1. Получение источника данных int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; // 2. Создание запроса // numQuery – это IEnumerable<int> var numQuery = from num in numbers where (num % 2) == 0 select num; // 3. Выполнение запроса foreach (int num in numQuery) { Console.Write("{0,1} ", num); } } }
На рис. 10.3 показана завершенная операция запроса. В LINQ выполнение запроса отличается от самого запроса; другими словами, создание переменной запроса само по себе не связано с получением данных.
Источник: Введение в запросы LINQ [2]
Рассмотрим подробнее действия в LINQ.
10.1.1.2.1. Источник данных
В предыдущем примере источником данных является массив, поэтому он неявно поддерживает универсальный интерфейс IEnumerable<(Of <(T>)>). Это значит, что к нему можно выполнять запросы с LINQ. Запрос выполняется в операторе foreach, и оператору foreach требуется интерфейс IEnumerable или IEnumerable<(Of <(T>)>). Типы, которые поддерживают IEnumerable<(Of <(T>)>) или производные интерфейсы, такие как универсальный интерфейс IQueryable<(Of <(T>)>), называются запрашиваемыми типами.
Для запрашиваемого типа, выступающего в качестве источника данных LINQ, не требуются изменения или специальная обработка. Если источник данных еще не находится в памяти в виде запрашиваемого типа, поставщик LINQ должен представить его как таковой. Например, LINQ to XML загружает XML-документ в запрашиваемый тип XElement:
// Создание источника данных из XML-документа, используя System.Xml.Linq XElement contacts = XElement.Load(@"c:\myContactList.xml");
Используя LINQ to SQL, сначала вручную либо с помощью Реляционный конструктор объектов создается объектно-реляционное сопоставление в режиме разработки. Затем можно написать запросы к объектам, а во время выполнения LINQ to SQL будет осуществлять взаимодействие с базой данных. В следующем примере Customer представляет определенную таблицу в базе данных, а Table<Customer> поддерживает универсальный IQueryable<(Of <(T>)>), производный от IEnumerable<(Of <(T>)>):
// Создание источника данных из базы данных SQL Server, используя System.Data.Linq DataContext db = new DataContext(@"c:\northwind\northwnd.mdf");
10.1.1.2.2. Запрос
Запрос указывает, какую информацию нужно извлечь из источника или источников данных. При необходимости, запрос также указывает способ сортировки, группировки и формирования этих сведений перед возвращением. Запрос хранится в переменной запроса и инициализируется выражением запроса. Чтобы упростить написание запросов, в C# появился новый синтаксис запроса.
Запрос из предыдущего примера возвращает все четные числа из массива целых чисел. Выражение запроса содержит три предложения: from, where и select. Предложение from указывает источник данных, предложение where применяет фильтр, а предложение select указывает тип возвращаемых элементов. Важно, что в LINQ сама переменная запроса не предпринимает действий и не возвращает никаких данных. Она просто хранит сведения, необходимые для предоставления результатов при последующем выполнении запроса.
10.1.1.2.3. Выполнение запроса
Выполнение запроса разделяют на отложенное и принудительное (немедленное).
Как уже говорилось ранее, сама переменная запроса только хранит команды запроса. Фактическое выполнение запроса откладывается до выполнения итерации переменной запроса в операторе foreach. Эту концепцию называют отложенным выполнением, она показана в следующем примере:
// Выполнение запроса foreach (int num in numQuery) { Console.Write("{0,1} ", num); }
Оператор foreach является также местом, где извлекаются результаты запроса. Например, в предыдущем запросе переменная итерации num содержит каждое (по очереди) значение в возвращаемой последовательности.
Запросы, выполняющие статистические функции над диапазоном исходных элементов, должны сначала выполнить итерацию этих элементов. Примерами таких запросов являются Count, Max, Average и First. Они выполняются без явного оператора foreach, поскольку сам запрос должен использовать foreach для возвращения результата. Обратите внимание, что такой тип запросов возвращает одиночное значение, а не коллекцию IEnumerable. Следующий запрос возвращает количество четных чисел в исходном массиве:
var evenNumQuery = from num in numbers where (num % 2) == 0 select num; int evenNumCount = evenNumQuery.Count();
Чтобы принудительно вызвать немедленное выполнение любого запроса и кэшировать его результаты, можно вызвать метод ToList<(Of <(TSource>)>) или ToArray<(Of <(TSource>)>):
List<int> numQuery2 = (from num in numbers where (num % 2) == 0 select num).ToList(); // или так: // numQuery3 – это по прежнему int[] var numQuery3 = (from num in numbers where (num % 2) == 0 select num).ToArray();
Можно также принудительно выполнить запрос, поместив цикл foreach сразу после выражения запроса. Однако вызов ToList или ToArray также кэширует все данные в одной коллекции объектов.