Visual Studio 2008. Краткий обзор ключевых новинок
Часть 2. LINQ — встроенные механизмы запросов
Механизмы доступа к XML-документам
Механизмы доступа к базам данных
В первой части данной статьи (см. CD-ROM) были рассмотрены языки программирования: новые версии C# и Visual Basic .NET — C# 3.0 и VB 9.0 соответственно.
Помимо уже описанных синтаксических расширений, языки C# 3.0 и Visual Basic .NET 9.0 обогатились встроенной поддержкой механизмов запросов на уровне языка — поддержкой технологии Language Integrated Query (LINQ). Если оставить в стороне теоретические рассуждения о важности и полезности наличия унифицированного механизма доступа к объектам, реляционным данным и XML-документам и дать краткое описание возможностей данной технологии, то LINQ позволяет решить две задачи: во-первых, унифицировать механизм запросов к различным объектам, будь то объекты типа массивов, коллекций и т.п., находящиеся в памяти, базы данных и XML-документы, а во-вторых, интегрировать средства написания таких запросов непосредственно в язык программирования. LINQ представляет собой набор расширений для языков C# 3.0 и Visual Basic .NET 9.0, а также унифицированную программную модель на уровне библиотеки классов .NET Framework. Далее мы рассмотрим, как LINQ поддерживается в C# 3.0 — аналогичные возможности языка Visual Basic .NET 9.0 мы предлагаем нашим читателям изучить самостоятельно.
Механизмы доступа к объектам
Наше знакомство с технологией LINQ мы начнем с рассмотрения механизмов доступа к объектам (OLINQ или Object LINQ). Любая коллекция, поддерживающая интерфейс System.Collections.Generic.IEnumerable или более общий интерфейс IEnumerable<T>, может рассматриваться в виде последовательности данных, а следовательно, может быть обработана с помощью стандартных операторов запросов LINQ. Эти стандартные операторы позволяют разработчикам создавать запросы, в том числе проекции в виде новых типов. Эта функциональность используется совместно с рассмотренной в предыдущей части статьи возможностью автоматического определения типов переменных по их инициализации.
Технология LINQ
Для того чтобы увидеть LINQ в действии, создадим следующее простое консольное приложение, содержащее метод NumQuery(), заполняющий коллекцию целочисленными значениями:
class Program
{
static void Main(string[] args)
{
}
static void NumQuery()
{
var numbers = new int[] { 1, 4, 9, 16, 25, 36 };
}
}
Обратим внимание на то, что в левой части присваивания не указан тип переменной numbers — вместо этого применяется новое ключевое слово var. Напомним, что это новая возможность в C# 3.0, которая позволяет указать компилятору на необходимость автоматического определения типа данных, содержащихся в указанной переменной. В нашем примере мы создаем объект типа Int32[], поэтому компилятор присвоит именно этот тип переменной numbers.
Добавим к методу NumQuery() следующий код, выполняющий запрос к коллекции в поисках целочисленных значений:
var evenNumbers = from p in numbers
where (p % 2) == 0
select p;
На данном шаге в правой части приведенного выше присваивания находится запрос в синтаксисе, поддерживаемом на уровне технологии LINQ. Как и на предыдущем шаге, для упрощения кода мы используем здесь автоматическое определение типов. Тип, возвращаемый как результат выполнения запроса, может быть не совсем очевиден — в данном примере это System.Collections.Generic.IEnumerable<Int32>. Отметим, что иногда не представляется возможным указать тип данных, возвращаемых в результате запроса, — в этом случае на помощь приходит механизм автоопределения типов.
Добавим код, выводящий результаты выполнения запроса на экран:
Console.WriteLine(«Result:»);
foreach (var val in evenNumbers)
Console.WriteLine(val);
Обратим внимание на то, что в выражении foreach мы также применяем нетипизованную переменную. Добавим вызов метода NumQuery() в метод Main(), выполним наше приложение и убедимся в том, что на экране выводятся числа 4, 16 и 36.
Теперь посмотрим, как использовать механизм запросов к более сложным, создаваемым пользователями типам данных. Создадим класс Customer, который реализуем следующим образом:
public class Customer
{
public string CustomerID { get; set; }
public string City { get; set; }
public override string ToString()
{
return CustomerID + «\t» + City;
}
}
Обратим внимание на то, что при объявлении класса Customer мы применяли еще одну новинку, появившуюся в C# 3.0, — автоматическое создание свойств для полей объекта. Также отметим, что у нашего класса нет конструктора — в предыдущих версиях разработчики должны были создать экземпляр объекта и вызвать по умолчанию конструктор, не имевший параметров, а затем отдельно задать значения полей объекта.
В объявление класса Program добавим новый метод — CreateCustomers(), создающий список клиентов (внимательные читатели могут заметить, что данные взяты из базы данных Northwind, ставшей уже классическим примером для демонстрации различных технологий компании Microsoft):
static IEnumerable<Customer> CreateCustomers()
{
return new List<Customer>
{
new Customer { CustomerID = “ALFKI”, City = “Berlin” },
new Customer { CustomerID = “BONAP”, City = “Marseille” },
new Customer { CustomerID = “CONSH”, City = “London” },
new Customer { CustomerID = “EASTC”, City = “London” },
new Customer { CustomerID = “FRANS”, City = “Torino” },
new Customer { CustomerID = “LONEP”, City = “Portland” },
new Customer { CustomerID = “NORTS”, City = “London” },
new Customer { CustomerID = “THEBI”, City = “Portland” }
};
}
Обратите внимание на то, что новая коллекция заполняется непосредственно внутри фигурных скобок. Даже несмотря на то, что тип коллекции — List<T>, а не массив, мы все равно имеем возможность добавлять элементы без непосредственного вызова метода Add(). Помимо этого элементы коллекции Customers создаются с помощью нового синтаксиса, называемого инициализатором объектов, — мы рассматривали эту функциональность в предыдущей статье данного цикла. Даже при отсутствии конструктора с двумя параметрами для класса Customer мы все равно имеем возможность создавать объекты этого типа как выражения, в которых внутри фигурных скобок задаются значения полей этого объекта.
Теперь напишем запрос к нашей коллекции, возвращающий клиентов, находящихся в Лондоне. Создадим метод ObjectQuery() и добавим вызов этого метода в метод Main():
static void ObjectQuery()
{
var results = from c in CreateCustomers()
where c.City == “London”
select c;
foreach (var c in results)
Console.WriteLine(c);
}
static void Main(string[] args)
{
ObjectQuery();
}
Если мы выполним наш пример, то получим список из трех клиентов. Итак, из данного примера видно, что применение LINQ-запросов делает работу с комплексными, задаваемыми пользователями типами такой же простой, как и с примитивными, встроенными типами.
Механизмы доступа к XML-документам
Механизмы доступа к XML (XLINQ или XML LINQ) позволяют работать с кэшем XML, находящимся в памяти, а также предоставляют простые способы создания XML-документов и их фрагментов. В следующем примере мы посмотрим, как считывать XML-документы в объект XDocument, как выполнять запросы к элементам этого объекта и как создавать документы и их элементы на лету.
Начнем с того, что добавим к нашему проекту ссылку на пространство имен System.Xml.Linq, которое позволит нам воспользоваться механизмами доступа к XML на уровне технологии LINQ:
using System.Xml.Linq;
В следующем примере мы выполним запрос к списку клиентов в поиске тех, кто находится в Лондоне. Но, в отличие от предыдущего упражнения, список клиентов будет находиться не в памяти, а в XML-файле на диске. Как мы увидим ниже, несмотря на то что источник данных о клиентах изменился, структура запроса остается той же, что и в предыдущем примере.
Изменим наш метод CreateCustomers() из предыдущего примера на:
static IEnumerable<Customer> CreateCustomers()
{
return
from c in XDocument.Load(“Customers.xml”)
.Descendants(“Customers”).Descendants()
select new Customer
{
City = (string)c.Attribute(“City”),
CustomerID = (string)c.Attribute(“CustomerID”)
};
}
Выполним наш пример и убедимся в том, что мы по-прежнему видим список из трех клиентов. Обратите внимание: структура запроса осталась неизмененной — мы заменили только метод CreateCustomers().
В следующем примере мы посмотрим, как возможности технологии LINQ помогут нам выполнять запросы к данным, находящимся в XML-файле, не загружая его в коллекцию. Предположим, что у нас нет класса Customers, в который мы загружаем данные из XML-файла. Ниже мы увидим, как обратиться к файлу напрямую, а не к коллекции, в которую загружены данные.
Добавим метод XMLQuery(), загружающий XML-документ и выводящий его содержимое на экран:
static void Main(string[] args)
{
XMLQuery();
}
public static void XMLQuery()
{
XDocument doc = XDocument.Load(“Customers.xml”);
Console.WriteLine(“XML Document:\n{0}”,doc);
}
Если мы запустим данное приложение, то увидим на экране содержимое файла. Вернемся в метод XMLQuery() и выполним тот же самый запрос, что и в предыдущих примерах, возвращающий список клиентов, находящихся в Лондоне:
public static void XMLQuery()
{
XDocument doc = XDocument.Load(“Customers.xml”);
var results = from c in doc.Descendants(“Customer”)
where (string)c.Attribute(“City”) == “London”
select c;
Console.WriteLine(“Results:”);
foreach (var contact in results)
Console.WriteLine(“\n{0}”, contact);
}
Обратим внимание на то, что объекты, возвращаемые в результате выполнения запроса и используемые в итераторе foreach, имеют тип XElement, а не Customers, как в предыдущих примерах.
В следующем примере мы посмотрим, как можно преобразовать результаты выполнения запроса в новый XML-документ. Предположим, что мы хотим создать новый XML-файл, в котором будут находиться клиенты, расположенные в Лондоне. Данный файл будет отличаться структурой от файла Customers.xml — в нем каждый элемент, описывающий клиента, будет хранить название клиента и название города в виде вложенных элементов, а не в виде атрибутов, как в оригинальном файле.
Преобразуем метод XMLQuery() в следующий:
public static void XMLQuery()
{
XDocument doc = XDocument.Load(«Customers.xml»);
var results = from c in doc.Descendants(“Customer”)
where (string)c.Attribute(“City”) == “London”
select c;
XElement transformedResults =
new XElement(“Londoners”,
from customer in results
select new XElement(“Contact”,
new XAttribute(“ID”, (string)customer.Attribute(“CustomerID”)),
new XElement(“Name”, (string)customer.Attribute(“ContactName”)),
new XElement(“City”, (string)customer.Attribute(“City”))));
Console.WriteLine(“Results:\n{0}”, transformedResults);
}
Временная переменная results служит для хранения данных, возвращаемых в результате выполнения запроса, перед тем как структура данных будет изменена.
Если мы запустим это приложение, то увидим на экране те же данные, что и в предыдущем примере, но в другом формате.
Теперь сохраним результаты наших манипуляций с данными в файле. Для этого добавим в метод XMLQuery() следующую строку:
transformedResults.Save(«Output.xml»);
Из приведенных примеров мы узнали, как осуществлять запросы к данным, хранящимся в XML-файлах, как трансформировать результаты запросов, выполняя преобразование структуры их представления и хранения, и как сохранять результаты таких преобразований в XML-файлах.
Механизмы доступа к базам данных
В этом разделе мы рассмотрим, как применять механизм LINQ to SQL, который является частью технологии LINQ. Механизм LINQ to SQL позволяет выполнять запросы и манипулировать объектами, ассоциированными с таблицами баз данных. Использование этого механизма позволяет избежать традиционных разногласий между таблицами, хранящимися в базах данных и объектах, представляющими бизнес-логику.
Для создания объектной модели для базы данных классы должны быть приведены в соответствие с сущностями, хранящимися в базе данных. Можно выделить три способа реализации такого приведения: задавать атрибуты для существующих объектов; применять специальное средство, позволяющее автоматически сгенерировать объекты; использовать утилиту командной строки SQLMetal. В наших примерах мы воспользуемся первыми двумя способами.
Начнем с того, что добавим к коду нашего примера ссылки на два пространства имен, которые позволят нам применять механизм LINQ to SQL:
using System.Data.Linq;
using System.Data.Linq.Mapping;
Теперь добавим к классу Customer ряд атрибутов для того, чтобы привести его в соответствие с таблицей базы данных Customers, которая содержит поля CustomerID и City. Ниже показано, как привести наш класс в соответствие с таблицей Customers в базе данных Northwind:
[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;
}
}
Теперь обратимся к методу ObjectQuery() и преобразуем метод поиска клиентов, находящихся в Лондоне, — ранее мы уже проделали это для данных, расположенных в памяти и XML-файле. Обратим внимание на то, что нам потребуется внести минимальные изменения для того, чтобы наш запрос мог быть обращен к данным, находящимся в базе данных. После создания соединения с базой данных мы можем получить необходимые нам записи из таблицы Customers, выбрать те записи, которые содержат клиентов, находящихся в Лондоне, и вернуть их в виде IEnumerable<Cumstomer>. Вот код модифицированного метода ObjectQuery():
static void ObjectQuery()
{
DataContext db = new DataContext
(@”Data Source=.\sqlexpress;Initial Catalog=Northwind”);
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);
}
Запустим наше приложение и убедимся в том, что оно работает, как ожидалось. В качестве дополнительной опции мы можем добавить код, показывающий SQL-запрос, выполняемый для получения данных. Вставьте следующую строку:
db.Log = Console.Out;
сразу после объявления переменной DataContext.
Теперь посмотрим, как выполнить те же действия, но используя автоматически созданный объект, отраженный на таблицу базы данных. Прежде всего удалим из нашего примера описание класса Cumstomer. Затем выберем команду Add | New Item для добавления к проекту нового элемента и в списке Templates выберем шаблон LINQ To SQL File. В поле Name укажем имя Northwind и нажмем кнопку Ok. Выберем команду View | Server Explorer (или нажмем последовательность клавиш Crtl+W, L), а затем нажмем кнопку Connect to database. В диалоговой панели Add Connection в поле Server name укажем имя сервера баз данных (например, .\sqlexpress), выберем базу данных Northwind в списке Select or enter a database name и нажмем кнопку Ok.
Следующий шаг — создание объекта представления. Откроем дерево под названием Data Conenctions, затем — папку Northwind, потом — папку Tables и откроем файл Northwind.dbml. Из папки с таблицами перетащим на панель методов таблицы Customers, Products, Employees и Orders. Затем из папки с хранимыми процедурами перетащим на панель методов хранимую процедуру Top Most Expensive Products. Нажмем комбинацию клавиш Ctrl+Shift+B для сборки приложения. Посмотрим на автоматически сгенерированный класс и обратим внимание на использование атрибутов. Отметим, что для баз данных с большим числом таблиц и хранимых процедур утилита SQLMetal предоставляет большее число возможностей для генерации объекта представления.
Вернемся к нашему методу ObjectQuery(). Каждая таблица представляет собой соответствующее свойство переменной db. В нашем примере запрос к базе данных будет практически аналогичным запросу, написанному в предыдущих примерах. Добавим в метод ObjectQuery() следующий код для получения списка клиентов, расположенных в Лондоне:
static void ObjectQuery()
{
var db = new NorthwindDataContext();
db.Log = Console.Out;
var results = from c in db.Customers
where c.City == “London”
select c;
foreach (var c in results)
Console.WriteLine(“{0}\t{1}”, c.CustomerID, c.City);
}
В приведенном выше примере мы создали объект NorthwindDataContext, который представляет собой строго типизированное соединение с базой данных. Отметим, что мы в явном виде не используем строку соединения с базой данных.
Запустим приложение на выполнение и убедимся в том, что оно работает, как и предполагалось. С помощью дизайнера можно создавать отображения и на другие таблицы. Класс Customer применяет отображение «один ко многим» на таблицу Orders. В следующем запросе показано, как извлечь данные из нескольких таблиц:
static void ObjectQuery()
{
var db = new NorthwindDataContext();
db.Log = Console.Out;
var results = from c in db.Customers
from o in c.Orders
where c.City == “London”
select new { c.ContactName, o.OrderID };
foreach (var c in results)
Console.WriteLine(«{0}\t{1}», c.ContactName, c.OrderID);
}
В приведенном выше примере показана конструкция select, создающая новый объект анонимного типа (новая возможность в C# 3.0). Созданный тип содержит два элемента данных — две строки с названиями, соответствующими оригинальным данным (в нашем случае это ContactName и OrderID). Использование анонимных типов чрезвычайно полезно при работе с запросами и существенно сокращает время на создание классов, содержащих результаты выполнения запросов.
В следующем примере мы рассмотрим, как с помощью технологии LINQ можно не только извлекать данные, но и выполнять операции создания, обновления и удаления, то есть все четыре типа операций, обычно называемых CRUD-операции (Create, Read, Update, Delete).
Добавим новый метод для модификации данных в базе данных — modifyData() и соответствующий вызов в методе Main():
static void Main(string[] args)
{
modifyData();
}
static void modifyData()
{
var db = new NorthwindDataContext();
var newCustomer = new Customer
{
CompanyName = “AdventureWorks Cafe”,
CustomerID = “ADVCA”
};
db.Customers.Add(newCustomer);
Console.WriteLine(“Number Created:{0}”,
db.Customers.Where( c => c.CustomerID == “ADVCA” ).Count());
}
Выполним наше приложение и отметим, что на экране показан 0, а не 1 и новая запись не отображена в результатах. Это связано с тем, что мы еще не вызвали метод submitChanges(). Теперь изменим данные в базе данных — модифицируем контактную информацию для первого клиента:
static void modifyData()
{
var db = new NorthwindDataContext();
var newCustomer = new Customer
{
CompanyName = “AdventureWorks Cafe”,
CustomerID = “ADVCA”
};
db.Customers.Add(newCustomer);
Console.WriteLine(“Number Created:{0}”,
db.Customers.Where( c => c.CustomerID == “ADVCA” ).Count());
var existingCustomer = db.Customers.First();
existingCustomer.ContactName = “New Contact”;
Console.WriteLine(“Number Updated:{0}”,
db.Customers.Where( c => c.ContactName == “New Contact” ).Count());
}
Как и в предыдущем примере, на экране будет показан 0, поскольку мы все еще не вызвали метод submitChanges(). Для того чтобы локальные изменения были занесены в базу данных, добавим к нашему методу modifyData()следующий код:
db.SubmitChanges();
Console.WriteLine(«Number Created:{0}»,
db.Customers.Where( c => c.CustomerID == «ADVCA» ).Count());
Console.WriteLine(«Number Updated:{0}»,
db.Customers.Where( c => c.ContactName == «New Contact» ).Count());
Выполним наше приложение и убедимся в том, что оно работает как и ожидалось. Отметим, что после занесения нового клиента в базу он не может быть занесен еще раз из-за ограничений на первичные ключи, а следовательно, данный пример можно запустить только один раз.
Теперь посмотрим, как с помощью технологии LINQ можно вызывать хранимые процедуры — помните, что в дизайнере мы добавили одну хранимую процедуру? Создадим новый метод, который будет выводить результаты выполнения хранимой процедуры Top Most Expensive Products:
static void InvokeSproc()
{
var db = new NorthwindDataContext();
foreach (var r in db.Ten_Most_Expensive_Products())
Console.WriteLine(r.TenMostExpensiveProducts + “\t” + r.UnitPrice);
}
Изменим код метода Main() на следующий:
static void Main(string[] args)
{
InvokeSproc();
}
Выполним наше приложение и изучим результаты. Отметим, что, когда по каким-то причинам база данных не может быть применена через динамические запросы на языке SQL, мы можем воспользоваться языком C# 3.0 и технологией LINQ для вызова хранимых процедур, предоставляющих доступ к данным.
Запросы, сделанные нами ранее, в основном выполняли операции фильтрации данных, но возможности LINQ этим отнюдь не ограничиваются. Например, для сортировки клиентов, находящихся в Лондоне, мы можем использовать конструкцию orderby:
static void ObjectQuery()
{
var db = new NorthwindDataContext();
db.Log = Console.Out;
var results = from c in db.Customers
where c.City == «London»
orderby c.ContactName descending
select new { c.ContactName, c.CustomerID };
foreach (var c in results)
Console.WriteLine(“{0}\t{1}”, c.CustomerID, c.ContactName);
}
Для того чтобы найти число клиентов, находящихся в каждом городе, мы можем воспользоваться конструкцией group by:
static void ObjectQuery()
{
var db = new NorthwindDataContext();
db.Log = Console.Out;
var results = from c in db.Customers
group c by c.City into g
orderby g.Count() ascending
select new { City = g.Key, Count = g.Count() };
foreach (var c in results)
Console.WriteLine(“{0}\t{1}”, c.City, c.Count);
}
Конструкция group by создает переменную типа IGrouping<string, Customer>, где строка содержит название города.
Часто при написании запросов возникает необходимость в поиске по двум таблицам. Обычно для этих целей применяется операция join, поддерживаемая в LINQ и C# 3.0. В методе ObjectQuery() заменим существующий запрос на приведенный ниже. Вспомним, что мы отображали все заказы для каждого клиента, находящегося в Лондоне. Теперь же отобразим число заказов, размещенных каждым клиентом:
static void ObjectQuery()
{
var db = new NorthwindDataContext();
db.Log = Console.Out;
var results = from c in db.Customers
join e in db.Employees on c.City equals e.City
group e by e.City into g
select new { City = g.Key, Count = g.Count() };
foreach (var c in results)
Console.WriteLine(“{0}\t{1}”, c.City, c.Count);
}
В этой части статьи мы рассмотрели основные возможности технологии LINQ, которая может использоваться для работы с данными, хранимыми в структурах и объектах, в базах данных и XML-документах. Более подробно о технологии LINQ и ее возможностях рассказывается в разделе, посвященном этой технологии, на сайте MSDN.