Проектирование баз данных и работа с ними Веб-приложений. LINQ, ADO.NET Entities, DDD
10.1.1.3. LINQ и обобщения
Запросы LINQ основаны на обобщениях (generic), которые впервые были представлены в .NET Framework версии 2.0. Для написания запросов не требуется глубокое знание обобщений. Но понимание двух основных понятий может пригодиться.
- При создании экземпляра класса универсальной коллекции, например List<(Of <(T>)>), "T" заменяется типом объектов, которые будут храниться в списке. Например, список строк выражается как List<string>, а список объектов Customer выражается как List<Customer>. Универсальный список является строго типизированным и предоставляет множество преимуществ над коллекциями, которые хранят свои элементы как Object. При попытке добавить Customer к List<string> возникнет ошибка во время компиляции. Использование универсальных коллекций не вызывает сложностей, поскольку не нужно выполнять приведение типов во время выполнения.
- IEnumerable<(Of <(T>)>) является интерфейсом, который позволяет классам универсальных коллекций поддерживать перечисление с помощью оператора foreach. Классы универсальных коллекций поддерживают IEnumerable<(Of <(T>)>) так же, как не универсальные классы коллекций, например ArrayList, поддерживают IEnumerable.
10.1.1.3.1. Переменные IEnumerable в запросах LINQ
Переменные запросов LINQ определены как IEnumerable<(Of <(T>)>) или как производный тип, например IQueryable<(Of <(T>)>). Если переменная запроса имеет тип IEnumerable<Customer>, это означает, что запрос при выполнении выведет последовательность из нуля или более объектов Customer:
IEnumerable<Customer> customerQuery = from cust in customers where cust.City == "Москва" select cust; foreach (Customer customer in customerQuery) { Console.WriteLine(customer.LastName + ", " + customer.FirstName); }
10.1.1.3.2. Использование компилятора для обработки объявлений универсальных типов
При желании обычного синтаксиса универсальных шаблонов можно избежать с помощью ключевого слова var. Ключевое слово var сообщает компилятору о необходимости определения типа переменной запроса с помощью просмотра источника данных, указанного в предложении from. В следующем примере создается тот же самый скомпилированный код, что и в предыдущем примере:
var customerQuery2 = from cust in customers where cust.City == "Москва" select cust; foreach(var customer in customerQuery2) { Console.WriteLine(customer.LastName + ", " + customer.FirstName); }
Ключевое слово var удобно, когда тип переменной является очевидным, или когда не требуется явно указывать вложенные универсальные типы, например создаваемые запросами group. В целом, если используется var, важно осознавать, что код может быть более сложным для чтения.
10.1.1.4. Основные операции запроса
10.1.1.4.1. Получение источника данных
В первую очередь в запросе LINQ нужно указать источник данных. В C#, как и в большинстве языков программирования, переменная должна быть объявлена до ее использования. В запросе LINQ первым идет предложение from для указания источника данных ( customers ) и переменная диапазона ( cust ):
//queryAllCustomers – это IEnumerable<Customer> var queryAllCustomers = from cust in customers select cust;
Переменная диапазона схожа с переменной итерации в цикле foreach за исключением того, что в выражении запроса не происходит фактической итерации. При выполнении запроса переменная диапазона будет использоваться как ссылка на каждый последующий элемент в customers. Поскольку компилятор может определить тип cust, нет необходимости указывать его в явном виде. Дополнительные переменные диапазона могут быть введены предложением let.
10.1.1.4.2. Фильтрация
Возможно, наиболее распространенной операцией запроса является применение фильтра в виде логического выражения. Фильтр приводит к возвращению запросом только тех элементов, для которых выражение является истинным. Результат создается с помощью предложения where. Фильтр фактически указывает элементы для исключения из исходной последовательности. В следующем примере возвращаются только customers, находящиеся в Москве:
var queryMoscowCustomers = from cust in customers where cust.City == "Москва" select cust;
Для применения нужного числа выражений фильтра в предложении where можно использовать знакомые логические операторы C# AND и OR. Например, для получения только заказчиков из Москвы и с именем Иван следует написать следующий код:
where cust.City=="Москва" && cust.Name == "Иван"
Для получения заказчиков из Москвы или Смоленска следует написать следующий код:
where cust.City == "Москва" || cust.City == "Смоленск"
10.1.1.4.3. Упорядочение
Часто целесообразно отсортировать возвращенные данные. Предложение orderby сортирует элементы возвращаемой последовательности в зависимости от компаратора по умолчанию для сортируемого типа. Например, следующий запрос может быть расширен для сортировки результатов на основе свойства Name. Поскольку Name является строкой, сравнение по умолчанию выполняется в алфавитном порядке от А до Я:
var queryMoscowCustomers3 = from cust in customers where cust.City == "Москва" orderby cust.Name ascending select cust;
Для упорядочения результатов в обратном порядке от Я до А используется предложение orderby … descending.
10.1.1.4.4. Группировка
Предложение group позволяет группировать результаты на основе указанного ключа. Например, можно указать, что результаты должны быть сгруппированы по City так, чтобы все заказчики из Москвы или Смоленска оказались в отдельных группах. В этом случае ключом является cust.City.
// queryCustomersByCity – это IEnumerable<IGrouping<string, Customer>> var queryCustomersByCity = from cust in customers group cust by cust.City; // customerGroup – это IGrouping<string, Customer> foreach (var customerGroup in queryCustomersByCity) { Console.WriteLine(customerGroup.Key); foreach (Customer customer in customerGroup) { Console.WriteLine(" {0}", customer.Name); } }
Когда запрос завершается предложением group, результаты представляются в виде списка из списков. Каждый элемент в списке является объектом, имеющим член Key и список элементов, сгруппированных по этому ключу. При итерации запроса, создающего последовательность групп, необходимо использовать вложенный цикл foreach. Внешний цикл выполняет итерацию каждой группы, а внутренний цикл – итерацию членов каждой группы.
Если необходимо ссылаться на результаты операции группировки, можно использовать ключевое слово into для создания идентификатора, который можно будет запрашивать. Следующий запрос возвращает только те группы, которые содержат более двух заказчиков:
// custQuery – это IEnumerable<IGrouping<string, Customer>> var custQuery = from cust in customers group cust by cust.City into custGroup where custGroup.Count() > 2 orderby custGroup.Key select custGroup;
10.1.1.4.5. Соединение
Операции соединения создают связи между последовательностями, неявно смоделированными в источниках данных. Например, можно выполнить соединение для поиска всех заказчиков в Москве, заказавших продукты у поставщиков в Париже. В LINQ предложение join всегда работает с коллекциями объектов, а не непосредственно с таблицами базы данных. В LINQ нет необходимости использовать join так часто, как в SQL, так как внешние ключи в LINQ представлены в объектной модели свойствами, содержащими коллекцию элементов. Например, объект Customer содержит коллекцию объектов Order. Вместо выполнения соединения, доступ к заказам можно получить с помощью точечной нотации:
from order in Customer.Orders...
10.1.1.4.6. Выбор (Проецирование)
Предложение select создает результаты запроса и задает форму или тип каждого возвращаемого элемента. Например, можно указать, будут ли результаты состоять из полных объектов Customer, только из одного члена, подмножества членов или некоторых совершенно других типов, на основе вычислений или создания новых объектов. Когда предложение select создает что-либо отличное от копии исходного элемента, операция называется проекцией. Использование проекций для преобразования данных является мощной возможностью выражений запросов LINQ.
10.1.1.5. Преобразования данных с LINQ
LINQ используется не только для извлечения данных. Это также мощное средство для преобразования данных. С помощью запроса LINQ можно использовать исходную последовательность в качестве входных данных и изменять ее различными способами для создания новой выходной последовательности. Можно изменить саму последовательность, не изменяя элементов, при помощи сортировки и группировки. Но, возможно, наиболее мощной функцией запросов LINQ является возможность создания новых типов. Это выполняется в предложении select. Например, можно выполнить следующие задачи:
- Объединить несколько входных последовательностей в одну выходную последовательность, которая имеет новый тип.
- Создать выходные последовательности, элементы которых состоят только из одного или нескольких свойств каждого элемента в исходной последовательности.
- Создать выходные последовательности, элементы которых состоят из результатов операций, выполняемых над исходными данными.
- Создать выходные последовательности в другом формате. Например, можно преобразовать данные из строк SQL или текстовых файлов в XML.
Это только несколько примеров. Разумеется, эти преобразования могут объединяться различными способами в одном запросе. Более того, выходные последовательности одного запроса могут использоваться как входные последовательности для нового запроса.
10.1.1.5.1. Соединение нескольких входных последовательностей в одну выходную
Запрос LINQ можно использовать для создания выходной последовательности, содержащей элементы из нескольких входных последовательностей. В следующем примере показано объединение двух находящихся в памяти структур данных, но те же принципы могут применяться для соединения данных из источников XML, SQL или DataSet. Предположим, что существуют два следующих типа классов:
class Student { public string First { get; set; } public string Last {get; set;} public int ID { get; set; } public string City { get; set; } public List<int> Scores; } class Teacher { public string First { get; set; } public string Last { get; set; } public int ID { get; set; } public string City { get; set; } }
В следующем примере показан запрос:
class DataTransformations { static void Main() { // Создание первого источника данных List<Student> students = new List<Student>() { new Student {First="Светлана", Last="Омельченко", ID=111, City="Москва", Scores= new List<int> {5, 4, 5, 3}}, new Student {First="Кристина", Last="Лаврова", ID=112, City="Тюмень", Scores= new List<int> {5, 3, 3, 4}}, new Student {First="Иван", Last="Моргунов", ID=113, City="Новосибирск", Scores= new List<int> {5, 5, 5, 4}}, }; // Создание второго источника данных List<Teacher> teachers = new List<Teacher>() { new Teacher {First="Анна", Last="Виннер", ID=945, City = "Москва"}, new Teacher {First="Алексей", Last="Иващенко", ID=956, City = "Санкт-Петербург"}, new Teacher {First="Михаил", Last="Антонов", ID=972, City = "Смоленск"} }; // Создание запроса var peopleInMoscow = (from student in students where student.City == "Москва" select student.Last) .Concat(from teacher in teachers where teacher.City == "Москва" select teacher.Last); Console.WriteLine("Следующие студенты и учителя живут в Москве:"); // Выполнение запроса foreach (var person in peopleInMoscow) { Console.WriteLine(person); } Console.WriteLine("Нажмите любую кнопку для выхода!"); Console.ReadKey(); } } /* На выходе будет получено: Следующие студенты и учителя живут в Москве: Омельченко Виннер */