
- •10. Проектирование баз данных и работа с ними Веб-приложений. Linq, ado.Net Entities, dd
- •10.1. Доступ к данным в .Net
- •10.1.1.2. Введение в запросы linq
- •10.1.1.2.1. Источник данных
- •10.1.1.2.2. Запрос
- •10.1.1.2.3. Выполнение запроса
- •10.1.1.3. Linq и обобщения
- •10.1.1.3.1. Переменные iEnumerable в запросах linq
- •10.1.1.3.2. Использование компилятора для обработки объявлений универсальных типов
- •10.1.1.4. Основные операции запроса
- •10.1.2. Linq to sql
- •10.1.2.2. Возможности linq to sql
- •10.1.3. Ado.Net Entity Framework
- •10.1.3.9. Использование linq to Entities
- •10.3. Краткие итоги
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.
Примечание. Для демонстрации данного принципа в следующих примерах используются явные типы. Также можно использовать неявную типизацию для custQuery, group и customer, позволяя компилятору определить точный тип:
// 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 peopleInSeattle)
{
Console.WriteLine(person);
}
Console.WriteLine("Нажмите любую кнопку для выхода!");
Console.ReadKey();
}
}
/* На выходе будет получено:
Следующие студенты и учителя живут в Москве:
Омельченко
Виннер
*/
10.1.1.5.2. Выбор подмножества каждого исходного элемента
Существует два основных способа выбора подмножества каждого элемента в исходной последовательности.
Чтобы выбрать только один член исходного элемента, используйте обращение к свойствам. В следующем примере предполагается, что объект Customer содержит несколько открытых свойств, включая строку с именем City. При выполнении этот запрос создаст выходную последовательность строк:
var query = from cust in Customers
select cust.City;
Для создания элементов, содержащих более одного свойства исходного элемента, можно использовать инициализатор объектов либо с именованным объектом, либо с анонимным типом. В следующем примере показано использование анонимного типа для инкапсуляции двух свойств из каждого элемента Customer:
var query = from cust in Customer
select new {Name = cust.Name, City = cust.City};
10.1.1.5.3. Преобразование находящихся в памяти объектов в XML
Запросы LINQ упрощают преобразования данных между структурами данных в памяти, базами данных SQL, наборами данных ADO.NET и потоками или документами XML. В следующем примере объекты в находящейся в памяти структуре данных преобразуются в XML-элементы:
class XMLTransform
{
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}},
};
// Создание запроса
var studentsToXML = new XElement("Root",
from student in students
let x = String.Format("{0},{1},{2},{3}", student.Scores[0],
student.Scores[1], student.Scores[2], student.Scores[3])
select new XElement("student",
new XElement("First", student.First),
new XElement("Last", student.Last),
new XElement("Scores", x)
)
);
// Выполнение запроса
Console.WriteLine(studentsToXML);
Console.WriteLine("Нажмите любую кнопку для выхода!");
Console.ReadKey();
}
}
Код формирует следующие выходные XML-данные:
<Root>
<student>
<First>Светлана</First>
<Last>Омельченко</Last>
<Scores>5,4,5,3</Scores>
</student>
<student>
<First>Кристина</First>
<Last>Лаврова</Last>
<Scores>5,3,3,4</Scores>
</student>
<student>
<First>Иван</First>
<Last>Моргунов</Last>
<Scores>5,5,5,4</Scores>
</student>
</Root>
10.1.1.5.4. Выполнение операций над исходными элементами
Выходная последовательность может не содержать какие-либо элементы или свойства элементов из исходной последовательности. Результатом может быть последовательность значений, вычисляемых с использованием исходных элементов в качестве входных аргументов. При выполнении следующего простого запроса выводится последовательность строк, значения которых рассчитаны на основе исходной последовательности элементов типа double.
Примечание. Вызов методов в выражениях запроса не поддерживается, если запрос будет перенесен в какой-либо другой домен. Например, невозможно вызвать обычный C# метод в LINQ to SQL, так как в SQL Server для него отсутствует контекст. Тем не менее, хранимые процедуры можно сопоставить методам и вызывать последние:
class FormatQuery
{
static void Main()
{
// Источник данных
double[] radii = { 1, 2, 3 };
// Запрос
IEnumerable<string> query =
from rad in radii
select String.Format("Area = {0}", (rad * rad) * 3.14);
// Выполнение запроса
foreach (string s in query)
Console.WriteLine(s);
Console.WriteLine("Нажмите любую кнопку для выхода!");
Console.ReadKey();
}
}
/* На выходе будет получено:
Area = 3.14
Area = 12.56
Area = 28.26
*/
10.1.1.6. Связи типов в операциях запроса
Для эффективного написания запросов следует понимать, как типы переменных связаны друг с другом в полной операции запроса. Понимание этих связей облегчит усвоение примеров LINQ и примеров кода в документации. Более того, можно будет представить, что происходит в фоновом режиме при неявном типизировании переменных с помощью var.
Операции запросов LINQ строго типизированы в источнике данных, в самом запросе и при выполнении запроса. Тип переменных в запросе должен быть совместим с типом элементов в источнике данных и с типом переменной итерации в операторе foreach. Строгая типизация гарантирует перехват ошибок во время компиляции, когда их можно будет исправить прежде, чем с ними столкнутся пользователи.
10.1.1.6.1. Запросы, не выполняющие преобразование исходных данных
На рис. 10.4 показана операция запроса LINQ to Objects, не выполняющая преобразование данных. Источник содержит последовательность строк, результат запроса также является последовательностью строк.
Рис. 10.4. Операция запроса LINQ to Objects, не выполняющая преобразование данных
Источник: Связи типов в операциях запроса (LINQ) [2]
Аргумент типа источника данных определяет тип переменной диапазона.
Тип выбранного объекта определяет тип переменной запроса. Здесь name является строкой. Следовательно, переменная запроса является IEnumerable<string>.
Итерация переменной запроса выполняется в операторе foreach. Поскольку переменная запроса является последовательностью строк, переменная итерации также является строкой.
10.1.1.6.3. Запросы, выполняющие преобразование исходных данных
На рис. 10.5 показана операция запроса LINQ to SQL, выполняющая простое преобразование данных. В качестве входных данных запрос получает последовательность объектов Customer и выбирает в результате только свойство Name. Поскольку Name является строкой, запрос создает последовательность строк в качестве выходных данных.
Рис. 10.5. Операция запроса LINQ to Objects, выполняющая простое преобразование данных
Источник: Связи типов в операциях запроса (LINQ) [2]
Аргумент типа источника данных определяет тип переменной диапазона.
Оператор select возвращает свойство Name вместо целого объекта Customer. Поскольку Name является строкой, аргумент типа custNameQuery является string, а не Customer.
Поскольку custNameQuery является последовательностью строк, переменная итерации цикла foreach также должна быть string.
На рис. 10.6 показано немного более сложное преобразование. Оператор select возвращает анонимный тип, захватывающий только два члена исходного объекта Customer.
Рис. 10.6. Операция запроса LINQ to Objects, выполняющая сложное преобразование данных
Источник: Связи типов в операциях запроса (LINQ) [2]
Аргумент типа источника данных всегда является типом переменной диапазона в запросе.
Так как оператор select создает анонимный тип, переменная запроса должна быть неявно типизирована с помощью var.
Поскольку тип переменной запроса неявный, переменная итерации в цикле foreach также должна быть неявной.
10.1.1.6.5. Разрешение компилятору определять сведения о типе
Несмотря на то, что необходимо обладать знаниями об отношениях типов в операции запроса, существует возможность передачи выполнения всех действий компилятору. Ключевое слово var можно использовать для любой локальной переменной в операции запроса (рис. 10.7). Компилятор в этом случае будет указывать строгий тип для каждой переменной в операции запроса.
Рис. 10.7. Определение компилятором сведений о типе
Источник: Связи типов в операциях запроса (LINQ) [2]
10.1.1.7. Синтаксис запроса или синтаксис метода
В предыдущих примерах большинство запросов написаны как выражения запросов с помощью декларативного синтаксиса запроса, представленного в C# 3.0. Однако в самой общеязыковой среде выполнения (CLR) .NET отсутствует понятие синтаксиса запроса. Таким образом, во время компиляции выражения запроса преобразуются в то, что понятно CLR – вызовы методов. Эти методы называются стандартными операторами запросов, и они имеют такие имена, как Where, Select, GroupBy, Join, Max, Average и т. д. Их можно вызывать непосредственно, используя синтаксис методов вместо синтаксиса запросов.
В целом, рекомендуется синтаксис запросов, так как обычно он более прост и легко читается; однако между синтаксисом методов и синтаксисом запросов нет семантической разницы. Кроме того, некоторые запросы, например такие, которые извлекают количество элементов, соответствующих указанному условию, или которые извлекают элемент, имеющий максимальное значение в исходной последовательности, могут быть выражены только в виде вызовов методов. В справочной документации по стандартным операторам запросов в пространстве имен System.Linq обычно используется синтаксис методов. Поэтому, даже на начальном этапе написания запросов LINQ полезно знать, как использовать синтаксис методов в запросах и самих выражениях запроса.
10.1.1.7.1. Методы расширения стандартных операторов запросов
В следующем примере показано простое выражение запроса и семантически эквивалентный ему запрос, написанный как запрос на основе метода:
class QueryVMethodSyntax
{
static void Main()
{
int[] numbers = { 5, 10, 8, 3, 6, 12 };
//Синтаксис запроса:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;
//Синтаксис метода:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);
foreach (int i in numQuery1)
{
Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
Console.WriteLine("Нажмите любую кнопку для выхода!");
Console.ReadKey();
}
}
/*
На выходе будет получено:
6 8 10 12
6 8 10 12
*/
Два примера имеют идентичные результаты. Тип переменной запроса одинаковый в обеих формах: IEnumerable<(Of <(T>)>).
Чтобы понять запрос на основе метода, рассмотрим его более детально. Обратите внимание, что в правой части выражения предложение where теперь выражено в виде метода экземпляра объекта numbers, который имеет тип IEnumerable<int>. Если вы знакомы с универсальным интерфейсом IEnumerable<(Of <(T>)> ), вам известно, что он не имеет метода Where. Однако при вызове списка завершения IntelliSense в IDE Visual Studio будет отображен не только метод Where, но и многие другие методы, такие как Select, SelectMany, Join и Orderby. Они все являются стандартными операторами запросов.
Несмотря на то, что кажется, как будто интерфейс IEnumerable<(Of <(T>)>) был переопределен для включения этих дополнительных методов, на самом деле это не так. Стандартные операторы запросов реализуются как новый тип методов, называемых методами расширения. Методы расширения "расширяют" существующий тип; их можно вызывать так, как если бы они были методами экземпляра типа. Стандартные операторы запросов расширяют IEnumerable<(Of <(T>)>), что позволяет написать numbers.Where(...).
Некоторые поставщики LINQ, например LINQ to SQL и LINQ to XML, реализуют свои собственные стандартные операторы запросов и дополнительные методы расширения для типов, отличных от IEnumerable<(Of <(T>)>).
10.1.1.7.2. Лямбда-выражения
Обратите внимание, что в предыдущем примере условное выражение (num % 2 == 0) передается в качестве встроенного аргумента методу Where: Where(num => num % 2 == 0). Это встроенное выражение называется лямбда-выражением. Оно является удобным способом написания кода, который в противном случае пришлось бы записывать в более громоздкой форме как анонимный метод, универсальный делегат или дерево выражений. В C# => является лямбда-оператором, который читается как "переходит". num слева от оператора является входной переменной, которая соответствует num в выражении запроса. Компилятор может определить тип num, так как ему известно, что numbers является универсальным типом IEnumerable<(Of <(T>)>). Основная часть лямбда-выражения представляет то же самое, что и выражение в синтаксисе запроса или в любом другом выражении или операторе C#; она может включать вызовы методов и дру гую сложную логику. Возвращаемым значением является просто результат выражения.
Приступая к работе с LINQ, нет необходимости широко использовать лямбда-выражения. Однако некоторые запросы могут выражаться только в синтаксисе методов, а некоторые из них требуют лямбда-выражений. После знакомства с лямбда-выражениями станет понятно, что они являются мощными и гибкими элементами в панели элементов LINQ.
10.1.1.7.3. Возможность компоновки запросов
Обратите внимание, что в предыдущем примере метод OrderBy вызывался от объекта, который являлся результатом вызова метода Where. Where создает отфильтрованную последовательность, а затем Orderby работает с ней, сортируя ее. Поскольку запросы возвращают IEnumerable, их можно компоновать в синтаксисе методов, объединяя вызовы методов в цепочки. При использовании синтаксиса запросов эти действия выполняет компилятор. Поскольку переменная запроса не сохраняет результаты запроса, ее можно изменить или в любое время использовать в качестве основы для нового запроса, даже после ее выполнения.