Лекция № 23 - 26

Технология LINQ – запросы, интегрированные в язык

План лекции

  1. Введение

  2. Автоматическое создание методов для свойств объектов

  3. Инициализация объектов и коллекций

  4. Типизация переменных и массивов

  5. Методы-расширения

  6. Лямбда-выражения

  7. Первое приложение LINQ

  8. Стандартные операторы запросов

  9. LINQ to SQL

  10. LINQ to XML

  11. LINQ to DataSets и LINQ to Entities


1. Введение


Несколько лет назад Андерсу Хейлсбергу (главный проектировщик C#) и Питеру Гольду пришла идея расширить C# с целью улучшения интеграции методов запроса данных. Питер, который в то время руководил разработкой компилятора для C#, исследовал возможность расширения компилятора для C#, в частности, с целью поддержки надстроек, которые могли бы проверять синтаксис зависящих от области языков, таких как SQL. Андерс, с другой стороны, вынашивал идею более глубокого и специфического уровня интеграции. Он размышлял о множестве «операторов последовательности», которые применялись бы к любой коллекции, реализующей IEnumerable, а также к удаленным запросам для типов, реализующих IQueryable. В конечном итоге идея операторов последовательности завоевала более значительную поддержку, и в начале 2004 г. Андерс представил доклад об этой идее на совещание Thinkweek, проводимое Биллом Гейтсом. Реакция была ошеломляюще одобрительной. Завеса тайны над C# 3.0 была приоткрыта примерно три года назад, когда создатели и идейные вдохновители этого проекта, Дон Бокс и Андрес Хэйлсберг, выступили на PDC 2005 и поведали миру о том, что же это такое. С того момента была проведена большая работа по созданию специального унифицированного языка запросов внутри C# и Visual Basic, который может использоваться для доступа к различным структурам данных. Он был выпущен вместе с новой версией Visual Studio, официальный релиз которой состоялся 27 февраля 2008 года в США и 18 марта 2008 года в России.

LINQ представляет собой набор расширений языка, поддерживающий формирование запросов данных способом, безопасным по типам. Запрашиваемые данные могут быть представлены в форме XML (запросы LINQ к XML), баз данных (ADO.NET с поддержкой LINQ, куда входят LINQ к SQL, LINQ к наборам данных и LINQ к экземплярам), объектов (LINQ к объектам) и т.д. Архитектура LINQ показана на рис.1.

Рисунок 1 Архитектура LINQ

Рассмотрим пример интегрированного запроса:

var contacts =

from c in customers

where c.State == "WA"

select new { c.Name, c.Phone };

Здесь из некоторого источника данных customers выбираются имена и телефоны клиентов, проживающих в штате Вашингтон. Запрос очень похож на язык SQL, однако использует строго типизированный синтаксис.

Этот же запрос можно записать и в другом виде – используя лямбда-выражения и методы расширения (см. рис. 2). Именно к такому виду приведёт написанный ранее код компилятор. Ниже будут рассмотрены все эти нововведения в язык.

Рисунок 2 Два способа написания запроса LINQ на языке C# 3.0


2. Автоматическое создание методов для свойств объектов



Часто методы доступа к свойствами объектов — get и set — имеют очень простую реализацию и служат для получения значения соответствующего поля внутри объекта (get) или присвоения этому полю нового значения (set). Создание свойств со стандартными методами доступа может существенно упростить написание кода и сделать его более понятным, т.к. в этом случае компилятор C# автоматически генерирует весь необходимый код. Рассмотрим следующий пример. Пусть у нас есть класс Point, содержащий два свойства:

public class Point

{

private int _x;

private int _y;

public int X { get { return _x; } set { _x = value; } }

public int Y { get { return _y; } set { _y = value; } }

}

Для упрощения описания этого класса мы можем воспользоваться автоматическим созданием свойств и в этом случае описание класса Point будет выглядеть так:

public class Point

{

public int X {get ; set ;}

public int Y {get ; set ;}

}

Так как теперь компилятор берет на себя всю работу по реализации методов get и set, мы можем инициализировать наш объект как обычно:

GroundZero p = new Point();

p.X = 0;

p.Y = 0;

Создание свойств описанным выше методом требует, чтобы у свойств были методы доступа get и set, но если мы захотим добавить собственные методы, мы можем воспользоваться подходом, описанным ниже.

Предположим, что у нас есть класс Customer, описанный следующим образом:

public class Customer

{

public string Name { get; set; }

public string City { get; set; }

}

Теперь предположим, что мы хотим добавить еще одно свойство, которое вне класса будет доступно только для чтения. Метод автоматического создания свойств позволяет решить эту задачу путем использования модификатора – в нашем примере это будет модификатор private для метода set:

public class Customer

{

public int CustomerID { get; private set; }

public string Name { get; set; }

public string City { get; set; }

}


3. Инициализация объектов и коллекций



Следующее расширение синтаксиса языка C#, которое мы рассмотрим, связано с упрощением инициализации объектов и коллекций. При объявлении объекта или коллекции в C# имеется возможность использования т.н. инициализатора, который будет задавать начальные значения членов вновь создаваемых объектов или коллекций. Новый синтаксис позволяет объединить в одном шаге и создание, и инициализацию объекта или коллекции. Рассмотрим пример использования инициализатора для объекта.

Пусть у нас есть класс Point, содержащий два свойства:

public class Point

{

private int _x;

private int _y;

public int X { get { return _x; } set { _x = value; } }

public int Y { get { return _y; } set { _y = value; } }

}

Инициализатор объекта состоит из последовательности инициализаторов членов класса, заключенных в фигурные скобки { } и разделенные запятыми. Каждый инициализатор члена класса присваивает значение полю или свойству объекта. Для нашего класса Point инициализация с использованием нового синтаксиса будет выглядеть так:

Point p = new Point { X = 3, Y = 99 };

Такой компактный синтаксис инициализации объекта семантически эквивалентен вызову конструктора класса и присвоению значения каждому члену класса.

Отметим, что при таком способе инициализации не обязательно указывать в списке все поля объекта – не указанные поля получат значения, присваиваемые по умолчанию.

В языке C# объекты, которые реализуют интерфейс System.Collections. Generic.IEnumerable<T> и имеют метод Add(), могут быть инициализированы с использованием инициализатора коллекций. Используя наш класс Point, мы можем создать коллекцию точек, описывающую какую-то геометрическую фигуру:

List<Point> Square = new List<Point>

{

new Point { X=0, Y=5 },

new Point { X=5, Y=5 },

new Point { X=5, Y=0 },

new Point { X=0, Y=0 }

};


4. Типизация переменных и массивов



Следующее расширение синтаксиса языка C#, которое мы рассмотрим, связано с типизацией переменных и массивов и позволяет определить тип локальной переменной или элемента массива по выражению, которое используется для инициализации. Для задания переменных, тип которых может быть определен компилятором автоматически, используется конструкция var, а для использования аналогичных возможностей для массивов - синтаксис new[]{…} - обратите внимание на отсутствие указания типа. Сначала приведем «стандартный» синтаксис, который мы использовали для задания и инициализации переменных и массивов в предыдущих версиях языка:

int i = 43;

string s = “...This is only a test...”;

int[] numbers = new int[] { 4, 9, 16};

Используя механизмы типизации для переменных и массивов, мы можем инициализировать переменные и массивы более простым способом:

var i = 43;

var s = “...This is only a test...”;

var numbers = new [] { 4, 9, 16 };

Механизм типизации для переменных и массивов имеет ряд ограничений. Например, так как тип переменной определяется по способу ее инициализации, при объявлении переменной с помощью конструкции var всегда должен присутствовать инициализатор. Например, при выполнении следующего кода мы получим ошибку:

var x; // Ошибка: тип не определен

x = new int[] { 1, 2, 3 };

Также мы всегда должны использовать синтаксис new[]{…} при создании инициализируемых массивов — при выполнении следующего кода мы получим ошибку:

var x = {1, 2, 3};

для исправления которой код нужно переписать следующим образом:

var x = new [] {1, 2, 3};

Используя конструкцию var мы можем упростить код некоторых комплексных выражений. Например, вместо использования в механизме итерации foreach четкого указания типа, как показано в следующем примере,

Console.WriteLine(“Customers”);

foreach (Customer c in customers)

Console.WriteLine(c);

мы можем использовать конструкцию var:

Console.WriteLine(“Customers:”);

foreach (var c in customers)

Console.WriteLine(c);


5. Методы-расширения



Методы-расширения представляют собой способ расширения существующих типов, а также типов, создаваемых пользователями. Используя этот способ разработчики могут добавлять к существующим типам новые методы, которые будут вызываться с использованием стандартного синтаксиса. Методы-расширения – это статические методы, объявляемые с использованием ключевого слова this в качестве модификатора первого параметра метода. Приведем следующий пример. Предположим, что нам нужна функция сравнения двух объектов типа Customer. В C# 2.0 мы могли бы написать следующий код:

public static class Extensions

{

public static bool Compare(Customer customer1, Customer customer2)

{

if (customer1.Name == customer2.Name && customer1.City == customer2.City )

{

return true;

}

return false;

}

}

Вызов этого метода может выглядеть так:

foreach (var c in customers)

{

if (Extensions.Compare(newCusomter, c))

{

Console.WriteLine(“Already in the list”);

return;

}

}

В C# 3.0 можно реализовать ту же функциональность с помощью метода-расширения, который будет в контексте объекта вызываться с использованием стандартного синтаксиса. Добавим ключевое слово this в качестве модификатора первого параметра метода Compare();

public static class Extensions

{

public static bool Compare(this Customer customer1, Customer customer2)

{

...

}

}

Тогда наш код проверки двух объектов будет выглядеть следующим образом:

foreach (var c in customers)

{

if (newOrder.Compare(c))

{

Console.WriteLine(“Already in the list”);

return;

}

...

Запомним следующее простое правило: методы-расширения доступны только в том случае, когда они объявлены в статическом классе и находятся в области видимости соответствующего пространства имен. Эти методы будут доступны в качестве дополнительных методов для типов, указанных в качестве первого параметра метода. Методы-расширения могут быть добавлены к любому типу, включая такие встроенные типы, как List<T> и Dictionary<K, V>. Рассмотрим пример расширения функциональности стандартного типа List<T> – добавим к нему метод Append(), который будет объединять два элемента типа List<T> в один:

public static class Extensions

{

public static List<T> Append<T>(this List<T> a, List<T> b)

{

var newList = new List<T>(a);

newList.AddRange(b);

return newList;

}

}

Вызов нового метода-расширения для стандартного типа может выглядеть так:



{

...

var addedCustomers = new List<Customer>

{

new Customer { Name = “Paolo Accorti”, City = “Torino” },

new Customer { Name = “Diego Roel”, City = “Madrid” }

};

customers.Append(addedCustomers);

...

}

Методы-расширения предоставляют в распоряжение разработчиков элегантный способ расширения функциональности типов таким образом, что добавленные функции становятся частью типа. Используя методы-расширения можно добавлять новую функциональность к уже откомпилированным классам, включая классы, созданные пользователем и стандартные классы .NET Framework.


6. Лямбда-выражения



Лямбда-выражения – это расширение механизмов анонимных методов, которое существенно упрощает автоматическое определение типов и преобразование как в делегаты, так и в деревья выражений.

В C# 2.0 появились анонимные методы, позволявшие вставлять блоки кода в тех местах, где было возможно использование делегатов. Например:

var innerPoints = points.FindAll(delegate(Point p)

{ return (p.X > 0 && p.Y > 0); });

Метод FindAll() ожидает параметр в виде делегата. В нашем примере делегат определяет, являются ли координаты x и y положительными, т. е. относится ли точка с заданными координатами к первому квадранту картезианской поверхности.

В C# 3.0 появились лямбда-выражения, которые позволяют использовать более простой синтаксис для написания анонимных методов. Таким образом, предыдущий пример можно переписать так:

var innerPoints = points.FindAll( p => p.X > 0 && p.Y > 0);

Лямбда-выражение пишется как список параметров, за которым следует символ =>, а за ним – код самого выражения. Например:

(int x) => { return x + 1; }

Параметры лямбда-выражения могут быть непосредственно или опосредованно типизованы:

(int x) => x + 1

В списке опосредованно типизованных параметров типы параметров получаются из контекста, в котором используется данное лямбда-выражение. Помимо этого, если в лямбда-выражении задан один типизованный параметр, список параметров можно не заключать в скобки:

x => x + 1

(x,y) => x * y;

Предположим, что мы хотим найти в списке объектов Customer все

объекты с определенным значением поля City. Для этого мы можем использовать метод FindAll() класса List<T>. Напишем метод FindCustomersByCity(), в котором будем использовать анонимные методы и делегаты используя синтаксис C# 2.0:

public static List<Customer> FindCustomersByCity(

List<Customer> customers,

string city)

{

return customers.FindAll(delegate(Customer c){

return c.City == city;

});

}

Вызов этого метода будет выглядеть следующим образом:

{

var customers = CreateCustomers();

foreach (var c in FindCustomersByCity(customers, “London”))

Console.WriteLine(c);

}

Теперь воспользуемся возможностями C# 3.0 и заменим анонимный метод эквивалентным лямбда"выражением:

public static List<Customer> FindCustomersByCity(

List<Customer> customers, string city)

{

return customers.FindAll(c => c.City == city);

}

Как мы увидели выше, использование лямбда-выражений в некоторых случаях упрощает написание кода. В приведенном выше примере список параметров состоит из нетипизованного параметра ‘c’ и, таким образом, явного указания типа не требуется. В качестве альтернативы мы можем написать следующий код:

return customers.FindAll((Customer c) => c.City == city);


7. Первое приложение LINQ

Чтобы начать рассмотрение LINQ, необходимо установить любую версию Visual Studio 2008. Рекомендуется использование Visual Studio 2008 Express ввиду его бесплатного распространения.

Создадим проект Console Application, при этом в окне выбора нового проекта следует выбрать .NET Framework 3.5, т.к. именно эта платформа включает в себя C# 3.0 (см. рис. 3).

Рисунок 3 Создание консольного приложения C# с поддержкой LINQ

Стандартные операторы запросов располагаются в сборке System.Core.dll в пространстве имен System.Linq (оно добавлено в проект по умолчанию) и представляют собой методы расширений статических классов Enumerable и Queryable. Применять их можно к объектам, реализующим интерфейсы IEnumerable<T> и IQueryable<T>. Это означает, что операторы применяются к самым различным классам, начиная с коллекций и массивов (последовательностей), размещенных в оперативной памяти, и заканчивая удаленными базами данных, использующими поставщики «LINQ - сущности» или «LINQ - SQL».

Первые приложения будут использовать поставщик «LINQ - объекты». На них очень просто научиться несложному синтаксису интегрированного языка запросов, чтобы потом перейти к более серьёзным приложениям с использованием XML и баз данных.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;


namespace TestLinq1

{

class Program

{

static void Main(string[] args)

{

//Определяем массив чисел

int[] intMass = {1,2,3,4,5,6,7,8,9,10};

//Выполняем запрос к массиву intMass и получим его элементы, которые больше 4, но меньше 8

var liteMass = from i in intMass

where i > 4 && i < 8

select i;

foreach(var val in liteMass)

Console.WriteLine(val);

Console.ReadKey();

}

}

}

При выполнении приложения на консоль будут выведены числа 5, 6 и 7. Сначала в приложении инициализирована переменная liteMass, представляющая собой перечисление из элементов, соответствующих запросу. Следует обратить внимание, что в отличие от языка запросов SQL в LINQ оператор select должен быть записан последним. Это связано с тем, как компилятор преобразует интегрированный запрос в выражение, использующее методы-расширения (см. рис.1). Действительно, сначала нужно взять объект с данными (from), затем применить к нему некоторый фильтр (where), а потом сформировать на основе полученного результата новый объект с некоторой структурой (select).

Также важным моментом в освоении технологии LINQ является понимание «ленивых», или отложенных вычислений. В примере выше исходный массив состоит всего из 10 элементов, но на практике запросы могут строиться к довольно большим удаленным источникам данных. Было бы нецелесообразно хранить всю выборку, соответствующую запросу, в оперативной памяти. Поэтому при инициализации объекта liteMass никаких вычислений не происходит и он, фактически, остаётся пустым до первого обращения к нему. Даже при возникновении такого обращения в операторе foreach вся выборка не загружается в память. При получении повторных результатов там находится только то, что нужно программе на данном шаге.

Несколько усложним приложение. Пусть теперь оно будет выводить все числа массива, но при этом указывать четность каждого из них. Тут на помощь приходят анонимные типы:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;


namespace TestLinq2

{

class Program

{

static void Main(string[] args)

{

//Определяем массив чисел

int[] intMass = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

/* Выполняем запрос к массиву intMass и выполняем преобразование

* в объект, содержащий два поля Value - число и

* Enev - значение boolean показывающее является ли число четным */

var liteMass = from i in intMass

select new {Value = i,Even = (i % 2==0)};

foreach (var val in liteMass)

Console.WriteLine("{0} - {1}", val.Value,val.Even);

Console.ReadKey();

}

}

}

Результатом интегрированного запроса будет коллекция некоторого объекта с двумя полями. На консоль в таком случае будет выведен следующий результат:

1 - False

2 - True

3 - False

4 - True

5 - False

6 - True

7 - False

8 - True

9 - False

10 - True


8. Стандартные операторы запросов

Теперь самое время познакомиться с некоторыми стандартными операторами запросов LINQ. Standard Query Operators – это методы-расширения, которые находятся в классе System.Linq.Enumerable. Они расширяют функционал объектов, наследуемых от интерфейса IEnumerable. Краткое описание всех стандартных операторов запросов представлено в таблице 1. Примеры использования некоторых из них приведены ниже.

Таблица 1 Стандартные операторы запросов LINQ

Оператор

Описание

Объединение

 

Aggregate

Применяет к последовательности пользовательский метод.

Average

Вычисляет среднее для числовой последовательности.

Count

Возвращает количество элементов в последовательности (целочисленное значение).

LongCount

Возвращает количество элементов в последовательности (значение в диапазоне LongInt).

Min

Возвращает наименьшее значение для числовой последовательности.

Max

Возвращает наибольшее значение для числовой последовательности.

Sum

Складывает члены числовой последовательности.

Конкатенация

 

Concat

Соединяет две последовательности в одну.

Преобразование

 

Cast

Преобразует элементы последовательности в элемены указанного типа.

OfType

Выбирает из элементов последовательности элемены указанного типа.

ToArray

Возвращает массив из элементов последовательности.

ToDictionary

Возвращает словарь из элементов последовательности.

ToList

Возвращает список из элементов последовательности.

ToLookup

Возвращает результаты поиска по последовательности.

ToSequence

Возвращает последовательность IEnumerable.

Элемент

 

DefaultIfEmpty

Создает стандартный элемент для пустой последовательности.

ElementAt

Возвращает элемент последовательности по указанному индексу.

ElementAtOrDefault

Возвращает элемент по указанному индексу или стандартный элемент (если индекс вышел за пределы диапазона).

First

Возвращает первый элемент последовательности.

FirstOrDefault

Возвращает первый элемент последовательности или стандартный элемент (если нужный элемент не найден).

Last

Возвращает последний элемент последовательности.

LastOrDefault

Возвращает последний элемент последовательности или стандартный элемент (если нужный элемент не найден).

Single

Возвращает единственный элемент последовательности.

SingleOrDefault

Возвращает единственный элемент последовательности или стандартный элемент (если нужный элемент не найден).

Равенство

 

SequenceEqual

Проверяет эквивалентность двух последовательностей.

Создание

 

Empty

Создает пустую последовательность.

Range

Создает последовательность в соответствии с заданным диапазоном.

Repeat

Создает последовательность, повторяя значение заданное количество раз.

Группировка

 

GroupBy

Группирует элементы последовательности указанным образом.

Присоединение

 

GroupJoin

Выполняет группированное соединение двух последовательностей.

Join

Выполняет внутреннее соединение двух последовательностей.

Упорядочение

 

OrderBy

Упорядочивает элементы последовательности по заданным значениям в порядке возрастания.

OrderByDescending

Упорядочивает элементы последовательности по заданным значениям в порядке убывания.

ThenBy

Упорядочивает элементы уже упорядоченной последовательности в порядке возрастания.

ThenByDescending

Упорядочивает элементы уже упорядоченной последовательности в порядке убывания.

Reverse

Зеркально отображает порядок расположения элементов в последовательности.

Разделение на части

 

Skip

Возвращает последовательность, в которой указанное число элементов пропущено.

SkipWhile

Возвращает последовательность, в которой пропущены элементы, не соответствующие указанному условию.

Take

Возвращает последовательность, в которую включается указанное число элементов.

TakeWhile

Возвращает последовательность, в которую включаются элементы, соответствующие указанному условию.

Проекция

 

Select

Создает проекцию части последовательности.

SelectMany

Создает проекцию части последовательности по принципу «один ко многим».

Кванторы

 

All

Определяет соответствие всех элементов последовательности указанным условиям.

Any

Определяет, есть ли в последовательность элементы, удовлетворяющие указанным условиям.

Contains

Определяет, есть ли в последовательности указанный элемент.

Ограничение

 

Where

Сортирует члены последовательности.

Настройка

 

Distinct

Возвращает последовательность без повторяющихся элементов.

Except

Возвращает последовательность, представляющую собой разность двух других последовательностей.

Intersect

Возвращает последовательность, представляющую собой пересечение двух других последовательностей.

Union

Возвращает последовательность, представляющую собой объединение двух других последовательностей.


Рассмотрим некоторые примеры использования стандартных операторов запросов.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;


namespace TestLinq3

{

class Program

{

static void Main(string[] args)

{

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

/* будем пропускать элементы массива, пока

* индекс элемента меньше или равен его значению */

var laterNumbers = numbers.SkipWhile((n, index) => n >= index);

foreach (var n in laterNumbers)

Console.WriteLine(n);

Console.ReadKey();

}

}

}

Здесь используется оператор SkipWhile. Пока условие, переданное параметром в оператор, выполняется, элементы будут фильтроваться. В данном примере условием является лямбда-выражение, которое сравнивает величину элемента с его индексом. На экран будет выведено 8 строк, содержащих числа 1, 3, 9, 8, 6, 7, 2, 0. Следующий пример демонстрирует работу операторов упорядочивания.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;


namespace TestLinq4

{

class Program

{

static void Main(string[] args)

{

List<string> words = new List<string> { "cherry", "apple", "blueberry" };


var sortedWords =

from w in words

orderby w descending

select w;

foreach (var w in sortedWords)

Console.WriteLine(w);

Console.ReadKey();

}

}

}

Здесь слова сортируются по алфавиту в обратном порядке (оператор OrderByDescending). На консоль будет выведено 3 строки: cherry, blueberry, apple. В следующем примере используется оператор преобразования:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;


namespace TestLinq5

{

class Program

{

static void Main(string[] args)

{

object[] numbers = { null, 1.0, "two", 3, 4.0f, 5, "six", 7d };

var doubles = numbers.OfType<double>();

foreach (var d in doubles)

Console.WriteLine(d);

Console.ReadKey();

}

}

}

Массив numbers содержит объекты разных типов. Оператор OfType выбирает элементы, соответствующие типу double. В результате на консоль будут выведены числа 1 и 7. Следующий пример демонстрирует применение операторов настройки и объединения.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;


namespace TestLinq6

{

class Program

{

static void Main(string[] args)

{

int[] factorsOf300 = { 2, 2, 3, 5, 5 };

int uniqueFactors = factorsOf300.Distinct().Count();

Console.WriteLine(uniqueFactors);

Console.ReadKey();

}

}

}

Вначале из последовательности чисел выбираются неповторяющиеся (Distinct), а затем подсчитывается их количество (Count). В результате на консоль будет выведено число 3.

Как можно увидеть из этих примеров, синтаксис запросов LINQ очень похож на запросы SQL и XQuery. Однако до этого действия производились лишь над массивами и коллекциями (поставщик «LINQ - объекты»). Теперь, имея начальные представления о стандартных операторах запросов, можно переходить к рассмотрению других поставщиков LINQ.


9. LINQ to SQL

Отображение реляционных данных на объектную модель всегда было одной из наиболее сложных проблем при построении объектно-ориентированных систем. В большинстве случаев, запросы к базе данных пишутся на языке SQL, а их результат конвертируется в объекты (см. рис. 4). Разработчик вынужден одновременно работать с двумя различными представлениями данных, что значительно увеличивает трудозатраты на создание и поддержку программного продукта и увеличивает вероятность ошибок.

LINQ to SQL – простая, но достаточно мощная система объектно-реляционного отображения (ORM). По сравнению с традиционной технологией ADO.NET применение LINQ to SQL позволяет значительно упростить код, снизить вероятность ошибок и сократить время разработки проекта (см. рис. 5). Наибольший выигрыш при этом получат разработчики Web приложений, для которых, в новой версии ASP.NET предусмотрен специальный источник данных, позволяющий делать запросы непосредственно в Web странице.

Рисунок 4 Работа с реляционными данными по технологии ADO.NET


Рисунок 5 Работа с реляционными данными по технологии LINQ

Для создания объектной модели для базы данных, классы должны быть приведены в соответствие с сущностями, хранящимися в базе данных. Можно выделить три способа реализации такого приведения – можно задавать атрибуты для существующих объектов, можно использовать специальное средство, позволяющее автоматически сгенерировать объекты и использовать утилиту командной строки SQLMetal.

Используем первый способ приведения в первом приложении LINQ to SQL. Для этого нужно создать новый проект .NET Framework 3.5 и скопировать базу данных Northwind в папку этого проекта.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data.Linq;

using System.Data.Linq.Mapping;

using System.Data.SqlClient;



namespace TestLinq7

{

[Table(Name = "Customers")]

public class Customer

{

[Column]

public string CustomerID { get; set; }

[Column]

public string City { get; set; }

public override string ToString()

{

return CustomerID + "\t" + City;

}

}

class Program

{

static void Main(string[] args)

{

DataContext db = new DataContext

(@"Data Source=.\SQLEXPRESS;

AttachDbFilename=|DataDirectory|\NORTHWND.MDF;

Integrated Security=True;

User Instance=True");

var results = from c in db.GetTable<Customer>()

where c.City == "London"

select c;

foreach (var c in results)

Console.WriteLine("{0}\t{1}", c.CustomerID, c.City);

Console.ReadKey();

}

}

}

Внимание! В некоторых случаях следует вручную добавлять сборку System.Data.Linq для компиляции программы. Для этого нужно в окне Solution Explorer щелкнуть правой кнопкой мыши по папке References, нажать Add Reference… и на вкладке .NET выбрать System.Data.Linq.

В этом приложении есть класс Customer, который отображает таблицу Customers, и имеет поля CustomerID и City, отображающие поля этой таблицы. Объект класса DataContext задает входную точку в базу данных и имеет метод GetTable, который возвращает коллекцию определённого типа, в данном случае типа Customer. При этом сам запрос LINQ никак не изменяется: в нём также присутствуют ключевые слова from, where, select и он также возвращает отложенный запрос.

В результате выполнения программы на экран будут выведены идентификаторы и города проживания тех заказчиков, которые живут в Лондоне.

На рис. 6 приведен маппинг LINQ to SQL.

Рисунок 6 Маппинг LINQ to SQL

Следующее приложение демонстрирует автоматическое создание объектов отображения и работу с хранимыми процедурами. Также создадим новый проект .NET Framework 3.5 и скопируем базу данных Northwind в папку этого проекта. Теперь нажмём Add New Item в панели Standard и выберем LINQ to SQL classes (см. рис. 7).

Рисунок 7 Добавление в проект файла отображения

Затем в окне Server Explorer следует развернуть дерево базы данных Northwind и перетащить нужные таблицы и хранимые процедуры в окно файла отображения на панель методов. Для данного примера будет достаточно перетащить хранимую процедуру Ten Most Expensive Products (см. рис. 8).

Рисунок 8 Создание объекта отображения хранимой процедуры

Хранимая процедура извлекает из таблицы Products 10 самых дорогих продуктов и их цены:


ALTER procedure "Ten Most Expensive Products" AS

SET ROWCOUNT 10

SELECT Products.ProductName AS TenMostExpensiveProducts, Products.UnitPrice

FROM Products

ORDER BY Products.UnitPrice DESC


Для того, чтобы вызвать эту процедуру из программы на языке C# и вывести результаты, достаточно написать всего 3 строки кода:


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data.Linq;

using System.Data.Linq.Mapping;


namespace TestLinq8

{

class Program

{

static void Main(string[] args)

{

var db = new northwindDataContext();

foreach (var r in db.Ten_Most_Expensive_Products())

Console.WriteLine(r.TenMostExpensiveProducts + "\t" + r.UnitPrice);

Console.ReadKey();

}

}

}


Обратите внимание, что входная точка в базу данных теперь создаётся конструктором класса northwindDataContext (в общем случае класс будет называться так: {имя файла отображения}DataContext), которому больше не нужно передавать параметром строку соединения в явном виде.

Все прошлые примеры извлекают информацию из источников данных. Однако с помощью технологии LINQ можно выполнять кроме чтения ещё и операции создания, обновления и удаления, т. е. выполнять все четыре типа операций, которые обычно называют CRUD-операциями (Create, Read, Update, Delete).

В следующем примере сначала происходит подсчет количества поставщиков, работающих в компании ABC, затем происходит добавление одного нового поставщика, а потом его удаление из базы.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data.Linq;

using System.Data.Linq.Mapping;


namespace TestLinq9

{

class Program

{

static void Main(string[] args)

{

var db = new northwindDataContext();

var query = from c in db.Suppliers

where c.CompanyName == "ABC"

select c;

int count = query.Count();

Console.WriteLine("Number of suppliers in ABC: {0}", count);

Supplier supplier = new Supplier{ CompanyName = "ABC" };

db.Suppliers.InsertOnSubmit(supplier);

db.SubmitChanges();

count = query.Count();

Console.WriteLine("Number of suppliers in ABC: {0}", count);

supplier = db.Suppliers.FirstOrDefault(s => s.CompanyName == "ABC");

if (supplier != null)

{

db.Suppliers.DeleteOnSubmit(supplier);

db.SubmitChanges();

}

count = query.Count();

Console.WriteLine("Number of suppliers in ABC: {0}", count);

Console.ReadKey();

}

}

}


В этом примере следует обратить внимание на 2 вещи. Во-первых, в LINQ to SQL больше не существует методов Add() и Remove(). Раньше они могли вводить в заблуждение разработчиков, так как на самом деле не вносили никаких изменений в базу данных, пока не происходил вызов метода SumbitChanges(). С выходом Visual Studio 2008 RTM за добавление строки отвечает метод InsertOnSubmit(), а за удаление строки – DeleteOnSubmit(). По названиям этих методов становится понятно, что они внесут свои изменения только при подтверждении – вызове метода SumbitChanges().

Во-вторых, в примере выше хорошо отражен принцип многократно используемых запросов. Переменная query содержит запрос, результатом которого будет коллекция полей таблицы Suppliers. Он выполнится только при вызове Count(), а коллекция не будет храниться в оперативной памяти. Поэтому при изменении данных в базе результат выполнения запроса также будет меняться.


10. LINQ to XML

Механизмы доступа к XML (т. н. XLINQ или XML LINQ) позволяют работать с кэшем XML, находящимся в памяти, а также предоставляют простые способы создания XML-документов и их фрагментов. В дополнение к предоставлению поддержки XML запросов LINQ, Microsoft компенсировала многие недостатки стандартного DOM XML API (см. рис. 9).

Рисунок 9 Стандартный XML DOM

Создание полного дерева XML с помощью единственного оператора стало реальностью, благодаря функциональному конструированию (см. рис. 10). Функциональное конструирование – термин, используемый для описания возможности создания полной иерархии XML в единственном операторе.

Рисунок 10 Улучшенный XML DOM

Конечно, это не стало бы частью LINQ, если бы новый XML API не поддерживал запросы LINQ. Именно для этого было добавлено несколько специфичных для XML операций запросов, реализованных в виде расширяющих методов. Комбинация этих новых XML-специфичных операций со стандартными операциями запросов LINQ to Objects создает мощное элегантное решение для нахождения любых нужных данных в дереве XML.

Следующая программа как раз является примером функционального конструирования.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Xml;

using System.Xml.Linq;


namespace TestLinq10

{

class Program

{

static void Main(string[] args)

{

string fileName = "base.xml";


int trackId = 1;

XDocument doc = new XDocument(

new XElement("library",

new XElement("track",

new XAttribute("id", trackId++),

new XAttribute("genre", "Rap"),

new XAttribute("time", "3:24"),

new XElement("name", "Who We Be RMX (feat. 2Pac)"),

new XElement("artist", "DMX"),

new XElement("album", "The Dogz Mixtape: Who's Next?!")

),

new XElement("track",

new XAttribute("id", trackId++),

new XAttribute("genre", "Rap"),

new XAttribute("time", "5:06"),

new XElement("name", "Angel (ft. Regina Bell)"),

new XElement("artist", "DMX"),

new XElement("album", "...And Then There Was X")

),

new XElement("track",

new XAttribute("id", trackId++),

new XAttribute("genre", "Break Beat"),

new XAttribute("time", "6:16"),

new XElement("name", "Dreaming Your Dreams"),

new XElement("artist", "Hybrid"),

new XElement("album", "Wide Angle")

),

new XElement("track",

new XAttribute("id", trackId++),

new XAttribute("genre", "Break Beat"),

new XAttribute("time", "9:38"),

new XElement("name", "Finished Symphony"),

new XElement("artist", "Hybrid"),

new XElement("album", "Wide Angle")

)

)

);


doc.Save(fileName);

}

}

}

В результате получается следующий xml-файл:


<?xml version="1.0" encoding="utf-8"?>

<library>

<track id="1" genre="Rap" time="3:24">

<name>Who We Be RMX (feat. 2Pac)</name>

<artist>DMX</artist>

<album>The Dogz Mixtape: Who's Next?!</album>

</track>

<track id="2" genre="Rap" time="5:06">

<name>Angel (ft. Regina Bell)</name>

<artist>DMX</artist>

<album>...And Then There Was X</album>

</track>

<track id="3" genre="Break Beat" time="6:16">

<name>Dreaming Your Dreams</name>

<artist>Hybrid</artist>

<album>Wide Angle</album>

</track>

<track id="4" genre="Break Beat" time="9:38">

<name>Finished Symphony</name>

<artist>Hybrid</artist>

<album>Wide Angle</album>

</track>

</library>


Теперь для обработки и вывода данных из документа XML можно использовать новый XML API. Следующий пример демонстрирует форматированный вывод на экран информации о треках на альбоме, используя только что созданный base.xml.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Xml;

using System.Xml.Linq;


namespace TestLinq11

{

class Program

{

static void Main(string[] args)

{

string fileName = "base.xml";

XDocument doc = XDocument.Load(fileName);


foreach (var el in doc.Root.Elements())

{

Console.WriteLine("{0} {1}", el.Name, el.Attribute("id").Value);

Console.WriteLine(" Attributes:");

foreach (var attr in el.Attributes())

Console.WriteLine(" {0}", attr);

Console.WriteLine(" Elements:");

foreach (var element in el.Elements())

Console.WriteLine(" {0}: {1}", element.Name, element.Value);

Console.WriteLine("");

}

Console.ReadKey();

}

}

}

Внешний цикл организован по дочерним элементам для корневого (т.е. по трекам), а два внутренних – по атрибутам и также по дочерним элементам, но теперь уже для элемента track.

Однако LINQ to XML не ограничивается лишь нововведениями в API. Синтаксис запросов LINQ здесь такой же, как при работе с объектами и базами данных. Третий пример в этом разделе сортирует треки по продолжительности в обратном порядке и выводит на экран.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Xml;

using System.Xml.Linq;


namespace TestLinq11

{

class Program

{

static void Main(string[] args)

{

string fileName = "base.xml";

XDocument doc = XDocument.Load(fileName);


var tracks =

from t in doc.Root.Elements("track")

let time = DateTime.Parse(t.Attribute("time").Value)

orderby time descending

select t;

foreach (var t in tracks)

Console.WriteLine("{0} - {1}",

t.Attribute("time").Value, t.Element("name").Value);

Console.ReadKey();

}

}

}

Кроме уже знакомых конструкций запроса, в этом примере встречается новое ключевое слово let. Оно объявляет переменную внутри запроса LINQ. Здесь можно сказать, что это полностью соответствует способу объявления переменной в функциональном языке F#.


  1. LINQ to DataSets и LINQ to Entities

ADO.NET Entity Framework – инфраструктура формирования концептуального объектного представления данных с помощью сущностей, которая позволяет абстрагировать данные от схемы хранения. На текущий момент она доступна в версии Beta 3, а её выход намечен на лето 2008 года вместе с пакетом обновления 1 для Microsoft .NET Framework 3.5. Ввиду того, что данный инструмент доступа к данным ещё находится в разработке, он не рассматривается в этой лабораторной работе.

LINQ to DataSets позволяет строить запросы, интегрированные в язык, к элементам потребителям данных на основе класса DataSet. Область применения этого поставщика в большинстве случаев сводится к расширению уже существующих приложений. Поэтому он также не рассматривается в этой работе.

Многие сторонние разработчики также активно развивают технологию LINQ. Уже реализованы такие поставщики как LINQ to WebQueries, LINQ to Amazon, LINQ to RDF Files, LINQ to MySQL, LINQ to NHibernate, LINQ to LDAP, LINQ to Google Desktop, LINQ to SharePoint, LINQ to Streams, LINQ to Expressions, LINQ to JSON и другие.






Hosted by uCoz