пятница, декабря 28, 2007

Entity Framework – шесть способов выполнить объектный запрос

Должен вам сказать, что Entity Framework (EF) представляет собой непаханое поле для блогописательства. И в следующем году я планирую целую серию постов на эту тему. Выход EF анонсирован на февраль 2008 года, и учитывая его сложную судьбу, нам всем стоит держать кулаки за то, чтобы это событие не сорвалось. Потому что, после довольно детального изучения сабжа, я вам должен сказать, что EF это не просто ORM, это большой удар по разгильдяйству и бездорожью в разработке на .Net.
А пока, в качестве иллюстрации возможностей EF рассмотрим различные способы выполнения объектного запроса. Во всех случаях мы будем выполнять запрос, который должен вернуть объект класса Person из корпоративной базы Orgchart по его персональному идентификатору (PIN).
Первый способ, назовем его «классический». Для работы с объектами EF используется контекст, который представляет собой экземпляр класса, унаследованного от System.Data.Objects.ObjectContext. Такой класс (как и все классы объектов домена) генерируется визардом по EDM описанию. Для выполнения объектных запросов используется класс System.Data.Objects.ObjectQuery:


static void Test()
{
// это значение для выборки
int pin = 1840;
// для работы с EF создаем ObjectContext
using (Orgchart dataContext = new Orgchart())
{
//создаем запрос
ObjectQuery<Person> query = new ObjectQuery<Person>(
"select value p from Orgchart.Persons as p where p.PIN=@pin",
dataContext);
// добавляем параметр
query.Parameters.Add(new ObjectParameter("pin", pin));
// выполняем запрос (здесь мог быть foreach)
Person p = query.First();
if (p != null) Console.WriteLine("Name:{0}", p.LastNameRus);
}
}


Все довольно очевидно. Сначала создаем контекст “dataContext”. Затем создаем запрос, передавая ему строку запроса на eSQL (entity SQL) и контекст, и передаем запросу параметр. Обратите внимание, что в запросе параметр казан с символом @, а в имени параметра этот символ не используется. Запрос исполняется при вызове метода First() (поскольку нам нужен только один экземпляр Person, а вообще тут мог быть foreach).

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


static void Test()
{
// это значение для выборки
int pin = 1840;
// для работы с EF создаем ObjectContext
using (Orgchart dataContext = new Orgchart())
{
// dataContext.Persons сгенерирован визардом
Person p = dataContext.Persons.Where("it.PIN=@pin", new ObjectParameter("pin", pin)).First();
if (p != null) Console.WriteLine("Name:{0}", p.LastNameRus);
}
}


Здесь мы воспользовались методом ObjectQuery.Where() поэтому нам не надо указыать весь объектный запрос а только условие выборки. Кстати, форма записи «it.PIN», где it отсылает нас к текущему объекту выборки, а PIN это имя свойства этого объекта, пока (beta 3) нигде не задокументирована :).

Третий способ практически аналогичен второму, но использует другую реализацию ObjectQuery.Where(), которая в качестве параметра принимает лямбду:


Person p = dataContext.Persons.Where<Person>(c => c.PIN == pin).First<Person>();
if (p != null) Console.WriteLine("Name:{0}", p.LastNameRus);


Несмотря на минимальные отличия от прошлого варианта, эффект колоссальный. Во первых нам не надо передавать параметр. Но главное, - наше условие выборки в виде лямбды проверяется компилятором, в частности правильно ли мы указали имя свойства и правильный ли у нас тип параметра. В первых двух способах было не возможно ничего подобного. Если бы мы ошиблись в тексте запроса, то узнали об этом только в результате ошибки runtime. Здесь же мы получим ошибку компиляции. И это хорошо.

Четвертый способ по своей сути является всего лишь альтернативной формой записи предыдущего, но это уже LINQ:


p = (from c in dataContext.Persons where c.PIN == pin select c).First<Person>();
if (p != null) Console.WriteLine("Name:{0}", p.GetFullName());


Чисто декларативный синтаксис. Указываем откуда выбрать, как и что. Красота.

Пятый способ. Иногда нас интересует не весь объект, а всего несколько его свойств. Тут на помощь приходят анонимные типы:


var x = (from c in dataContext.Persons where c.PIN == pin select new { Name = c.LastNameRus, Pin=c.PIN }).First();
if (x != null) Console.WriteLine("Name:{0}", x.Name);


Вместо переменной типа Person мы получаем переменную var x, которая имеет свойства LastNameRus и Pin и ничего более.

Ну и наконец, последний, шестой способ. Он кардинально отличается от предыдущих пяти тем, что использует классы из пространства System.Data.EntityClient. В частности, EntityCommand и EntityDataReader. Я бы назвал это режимом обратной совместимости, потому что он возвращает нас в классический ADO.NET.
EntityConnection - это наследник DBConnection, EntityCommand – наследник DBCommand, а EntityDataReader – наследник DBDataReader. Схема работы классическая для ADO.NET: открываем соединение, создаем команду, передаем ей запрос, исполняем команду – получаем DataReader, читаем данные из ридера:


static void Test()
{
// это значение для выборки
int pin = 1840;
// создаем соединение
using (EntityConnection connection = new EntityConnection(@"metadata=.\Model1.csdl|.\Model1.ssdl|.\Model1.msl;provider=System.Data.SqlClient;provider connection string='Data Source=(local);Initial Catalog=Orgchart;Integrated Security=True'"))
{
// открываем соединение
connection.Open();
// создаем комманду
EntityCommand command = new EntityCommand("select value p from Orgchart.Persons as p where p.PIN=@pin", connection);
// добавляем параметр
command.Parameters.Add(new EntityParameter("pin", System.Data.DbType.Int32)).Value = pin;
// получаем reader, используя комманду
using (EntityDataReader reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess))
{
if (reader.Read())
{
Console.WriteLine("Name:{0}", reader["LastNameRus"]);
}
}
}
}


Однако при всем при этом, заметьте, мы работаем не с БД и SQL, а с объектной моделью EF и eSQL. Но прямо скажем, не самый удобный способ из рассмотренных. Зачем это надо? Ну, наверно кому-то надо.

Ну а выводы?
По русской традиции, каждый делает выводы для себя сам.
По американской традиции, не каждый может сделать правильные выводы :), и поэтому summary:
- Entity Framework это ORM от Microsoft который мы ждем уже много лет;
- От прочих ORM его отличают по крайней мере две вещи: здесь есть LINQ, и он обратно совместим с ADO.NET

P.S. Мне был бы интересен ваш фидбэк, поскольку я собираюсь сделать еще несколько постов на тему практических вопросов использования Entity Framework. Интересна ли тема? Если да, то какие вопросы интересуют в первую очередь?

8 комментариев:

eye-ru комментирует...

Тема крайне интересная, но хотелось бы увидеть сравнение производительности с существующими ORM решениями под .NET, например с Nhibernate.

Анонимный комментирует...

А есть ли там транзакции? Тема супер. Буду ждать продолжения.

Sergey Rozovik комментирует...

Транзакции конечно есть. Напишу.
Cравнение производительности с NHibernate - интересно, конечно, но грамотный тест поставить достаточно сложно.

igor toporets комментирует...

да, конечно, интересно!

Особо интересно как происходит работа с данными при изменении схемы данных, например в результате развития системы :)

Что предлагает EF для этого распространённого в жизни приложений сценария?

Анонимный комментирует...

На дворе уже 01.07.2008, а толкового описания системы так и не появилось.

Анонимный комментирует...

на сегодня 28.10.2008 последний релиз Nhibernate-2007-11-26. версия 1.2. год почти прошел! Эта версия не поддерживает "ленивых свойств" у объекта. только у коллекций. для Джавы уже есть решение. Для .Net - нет. :) похоже проект Nhibernate умирает, и не сможет конкурировать с EF...

Андрей комментирует...

Привет Сергей.

Тема интересная - сейчас используем Ling2Sql в силу того, что там очень просто делать маппинг: объект-процедура.
А как это работает с EF? И ещё одна итересная особенность это наследования объектов - часто приходится работать с таблицами расширения типов данных.

Sergey Rozovik комментирует...

> там очень просто делать маппинг: объект-процедура.
А как это работает с EF?

Писал я уже про это вот здесь