Показаны сообщения с ярлыком Entity Framework. Показать все сообщения
Показаны сообщения с ярлыком Entity Framework. Показать все сообщения

вторник, 17 мая 2011 г.

Linq To Entities vs. Linq To Objects на примере группировки

LINQ - удобная, красивая, но при этом довольно коварная абстракция. Самые неожиданные вещи обычно происходят на стыке какой-либо реализации LINQ и LINQ To Objects. Сегодня на одном примере я рассмотрю совместную работу LINQ To Entities (Entity Framework) и LINQ To Objects.

За основу возьмем метод репозитория, который принимает на вход список идентификаторов клиентов и возвращает сгруппированный по этим идентификаторам набор заказов (таблица Orders содержит поля OrderId, OrderDate и CustomerId):

public IDictionary<long, List<Order>> GetOrdersByCustomersIds(IList<long> customersIds)
{
using (var ctx = new RepositoryContext())
{
return ctx.Orders.
Where(o => customersIds.Contains(o.Id)).
GroupBy(o => o.CustomerId).
ToDictionary(o => o.Key, o => o.ToList());
}
}


Минуточку! А как это работает? Ведь при выполнении GROUP BY запроса мы можем выбрать лишь поля, по которым происходит группировка, а также агрегированные значения. Стандартным решением этой проблемы является JOIN данных таблицы и результатов группировки. Примерно так:

SELECT o1.*, MinTotal
FROM Orders as o1
INNER JOIN
(SELECT o2.CustomerId, Min(o2.Total) as MinTotal
FROM Orders o2
GROUP BY o2.CustomerId) as o3
ON o1.CustomerId = o3.CustomerId
Where o1.CustomerId in (1, 2, 3, 4, 5)

Что-то в этом духе и должен сгенерировать EF-провайдер. Давайте убедимся в этом. У меня под рукой был MySQL .NET Connector (официальный ADO.NET-провайдер для MySQL), поэтому я воспользовался им и получил следующий сгенерированный запрос (передав на вход список из идентификаторов от 1 до 5):

SELECT `Project2`.`C1`,
`Project2`.`CustomerId`,
`Project2`.`C2`,
`Project2`.`CustomerId1`,
`Project2`.`Id`,
`Project2`.`OrderDate`
FROM
(SELECT `Distinct1`.`CustomerId`,
1 AS `C1`,
`Extent2`.`CustomerId` AS `CustomerId1`,
`Extent2`.`Id`,
`Extent2`.`OrderDate`,
CASE WHEN (`Extent2`.`CustomerId` IS NULL) THEN (NULL) ELSE (1) END AS `C2`
FROM
(SELECT DISTINCT `Extent1`.`CustomerId`
FROM `orders` AS `Extent1`
WHERE ((1 = `Extent1`.`Id`) OR (2 = `Extent1`.`Id`)) OR (((3 = `Extent1`.`Id`) OR (4 = `Extent1`.`Id`)) OR (5 = `Extent1`.`Id`))) AS `Distinct1`
LEFT OUTER JOIN `orders` AS `Extent2`
ON (((1 = `Extent2`.`Id`) OR (2 = `Extent2`.`Id`)) OR (((3 = `Extent2`.`Id`) OR (4 = `Extent2`.`Id`)) OR (5 = `Extent2`.`Id`))) AND (`Distinct1`.`CustomerId` = `Extent2`.`CustomerId`)) AS `Project2`
ORDER BY `CustomerId` ASC, `C2` ASC

Немного хуже ручной реализации, но в целом прослеживается озвученная выше мысль.

Стоп! А зачем мы используем группировку на уровне базы данных? Группировка оправдана в случае использования функций агреграции (как в приведенной выше ручной реализации запроса). В нашем же случае группировка - лишь удобное представление полученных данных. Давайте слегка модифицируем метод репозитория и перенесем процесс группировки на уровень LINQ To Objects:

public IDictionary<long, List<Order>> GetOrdersByCustomersIds(IList<long> customersIds)
{
using (var ctx = new RepositoryContext())
{
return ctx.Orders.
Where(o => customersIds.Contains(o.Id)).
AsEnumerable().
GroupBy(o => o.CustomerId).
ToDictionary(o => o.Key, o => o.ToList());
}
}

Для полноты картины посмотрим, какой запрос сгенерирует EF-провайдер:

SELECT `Extent1`.`CustomerId`,
`Extent1`.`Id`,
`Extent1`.`OrderDate`
FROM `orders` AS `Extent1`
WHERE ((1 = `Extent1`.`Id`) OR (2 = `Extent1`.`Id`)) OR (((3 = `Extent1`.`Id`) OR (4 = `Extent1`.`Id`)) OR (5 = `Extent1`.`Id`))

Определенно этот запрос эффективнее предыдущего.

Вот, собственно, и все. Ничего особенного - лишь хотел заострить ваше внимание на коварности перехода от LINQ To X к LINQ To Objects после того, как сам попал в эту ловушку. Будьте бдительны!

P. S. Несмотря на то, что я использовал MySQL .NET Connector, категорически не рекомендую применять этот провайдер в продакшене: это не провайдер, а коцентрированный сгусток багов, которые не фиксятся годами.

PP. S. Кросспост на Хабре: http://habrahabr.ru/blogs/net/119624/

Entity Framework и MySQL

Если вы вдруг соберетесь использовать Entity Framework в связке MySQL, ни за что, на при каких обстоятельствах не используйте родной провайдер MySQL .NET Connector. Это не ADO.NET-провайдер, а кишащее критичными багами, которые не фиксятся годами, недоразумение (по крайней мере в области поддержки Entity Framework).

Из сторонних альтернатив я бы посоветовал продукт dotConnect for MySQL от компании DevArt. Он платный, но стоит вполне адекватных денег.

Я некоторое время назад сделал неправильный выбор, остановившись на стандартном провайдере. Надеюсь, вы не повторите моей ошибки.

суббота, 9 апреля 2011 г.

Google Analytics и Entity Framework

Заглянул в Google Analytics - оказывается, большинство читателей приходит в мой блог в поисках информации об Entity Framework. Неудивительно, ведь по запросу "Entity Framework" мой блог на первой странице в Google сразу за Википедией и MSDN.

С одной стороны, приятно, с другой, - все-таки это неправильно: мои статьи устарели и не заслуживают такой высокой позиции. А ведь интересная задачка: как сделать так, чтобы в рамках одной тематики новые, актуальные статьи ранжировались выше устаревших (которые уже успели обрасти ссылочной массой и соответствующим рейтингом в поисковой системе). Я не вижу очевидного решения этой проблемы (судя по всему, у ребят из Google с этим тоже не все гладко). А у вас есть идеи?

воскресенье, 4 октября 2009 г.

LINQ. First vs. Single. Часть вторая.

Неожиданно предыдущий пост про LINQ породил несколько вопросов, которые в свою очередь породили еще несколько :) О них-то мы сегодня и поговорим.

Основная мысль того поста была настолько очевидной, что я даже сомневался, писать или нет. Когда же пост уже был написан, решил сделать его хоть немного менее унылым, снабдив исходниками реализаций методов First и Single LINQ To Objects. При этом реализация метода Single мне показалась неоптимальной. За комментариями по этому поводу я обратился на форумы MSDN. К сожалению, я не сразу понял мысль коллег про "exceptional cases"... но в целом дискуссия получилась довольно конструктивной.

Итак, напомню, как выглядел код, полученный при помощи Reflector'a:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}
throw Error.MoreThanOneMatch();

А почему бы не добавить в тело условия if (predicate(local2)) проверку на равенство num двум:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
if (num == 2)
throw Error.MoreThanOneMatch();
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}

С одной стороны, этот код привносит условие, которое тоже требует машинного времени для своего выполнения. Но, во-первых, условие if (num == 2) - элементарное, а во-вторых, при любом раскладе оно выполнится не более двух раз. Что же мы при этом экономим? Экономим мы k проходов Enumenator'а (который лежит в основе foreach) и столько же выполнений предиката (только в случае наличия дубликатов, ибо если элемент уникальный, нам все равно придется пройти коллекцию полностью). Очевидно, что стандартная реализация проигрывает.

В принципе у текущей реализации есть оправдание. Вызывая метод Single, мы заранее предполагаем, что в коллекции будет находиться один подходящий элемент (иначе воспользовались бы методом First), поэтому данная реализация проигрывает лишь в редких случаях, отсрачивая генерацию исключения. Сюда же можно добавить, что цена генерации исключения в большинстве случаев будет гораздо выше издержек алгоритма.

В целом, конечно, данный вопрос представляет собой, в большей степени, академический интерес (кстати, неплохая иллюстрация закона дырявых абстракций): на практике в 99.9% случаев проблем из-за текущей реализации метода Single не будет. Если же для Вас метод Single стал проблемой, замените его на собственный extension method.

На этом с LINQ To Objects на сегодня закончим - поговорим о LINQ To Entities и его реализации метода Single. Напомню, что в EF v1 метод Single не поддерживался: приходилось либо довольствоваться First, либо использовать разные костыли, например, вот этот. Проблема приведенного по ссылке workaround'а в том, что при вызове query.Count из базы будут возвращены все удовлетворяющие запросу записи (в том числе и дублирующиеся). В случае, если мы обращаемся к "тяжелым" данным, результат может быть плачевным.

Так как же с Single обстоят дела в EF 4? Добравшись до VS 2010, я был приятно удивлен - работает. Но маленький параноик в моей голове убедил меня (спасибо ему за это) открыть Profiler и посмотреть на генерируемый SQL-код. А вот и он:

SELECT TOP (2)
[Extent1].[AddressId] AS [AddressId],
[Extent1].[City] AS [City],
[Extent1].[Street] AS [Street],
[Extent1].[PostalCode] AS [PostalCode],
[Extent1].[Apartment] AS [Apartment]
FROM [dbo].[Addresses] AS [Extent1]
WHERE 1 = [Extent1].[AddressId]

Данный код при наличии дубликатов возвращает две записи. Это, конечно, чуть лучше, чем решение, обсуждавшееся выше (там возвращалось неограниченное число дубликатов), но хотелось бы большего: хотелось бы, чтобы вопрос наличия дубликатов решался на стороне СУБД (например, при помощи подзапросов), а возвращалась бы либо одна (уникальная) запись, либо вообще ничего (если есть дубликаты).

За комментариями я в очередной раз обратился на форумы MSDN (прекрасное место :) ). Прежде всего, меня интересовало, финальное ли это решение для EF 4. Как Вы можете судить по ответу, шансов на то, что это поведение изменится до релиза .NET 4, мало.

На этой печальной ноте и закончим сегодняшний разговор о LINQ.

пятница, 2 октября 2009 г.

LINQ. First vs. Single

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

Для получения первого элемента, удовлетворяющего предикату, в LINQ используются два основных метода: First и Single. В различных реализациях LINQ детали могут меняться, поэтому для определенности рассмотрим логику этих методов на примере LINQ To Objects: First просто возвращает первый элемент (а если его нет - генерирует исключение), а Single возвращает единственный элемент (а исключение генерирует не только если элементов нет, но и если их больше одного). Однако несмотря на то, что названия методов вполне отражают различия между ними, порой программисты относятся к этим двум методам как к равноценным и взаимозаменяемым.

Давайте немного углубимся в алгоритмы работы методов: First находит первый элемент - и возвращает его, Single же на этом не останавливается и продолжает поиск второго элемента, дабы либо убедиться, что его нет, либо сгенерировать исключение, если он все же есть. Как следствие, First всегда быстрее Single. Таким образом, для получения первого элемента всегда нужно использовать First, Single же пригодится лишь в тех случаях, когда необходимо быть уверенным, что полученное значение является единственным (как бы банально это ни звучало, порой об этом забывают).

Вооружимся Reflector'ом и от теории перейдем к практике.

Для начала убедимся, что методы First и Single работают именно по описанным выше алгоритмам. Для этого рассмотрим выдержки кода метода First:

foreach (TSource local in source)
{
if (predicate(local))
{
return local;
}
}
throw Error.NoMatch();

и Single:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}
throw Error.MoreThanOneMatch();

Если с методом First все ясно, то к реализации метода Single у меня есть вопросы. В ходе цикла num не проверяется на равенство двум, поэтому выполнение цикла в любом случае продолжается до конца коллекции. Сразу представляется параноидальная ситуация, когда Single'ом с "тяжелым" предикатом обрабатывается огромная коллекция с объектами с "тяжелой" перегрузкой Equals, и все ужасно тормозит... Если в комментариях не последует оперативного разъяснения (может, я чего проглядел, а может, это известная "фича"), обращусь за оным в MSDN'овские форумы.

В заключение скажу пару слов об Entity Framework, а точнее о LINQ To Entities.

В EF v. 1 LINQ To Entities не поддерживал метод Single. Особенно неожиданным это открытие становилось для тех, кто переходил с, казалось бы, менее функционального LINQ To SQL: EF оказался заложником своей универсальности (по сравнению с заточенным под одну СУБД LINQ To SQL). Хотя на практике горевать по этому поводу приходилось редко (особенно на фоне более "выдающихся" ограничений EF v. 1), потому что, как правило, такие запросы выполняются по первичному ключу, уникальность которого гарантируется на уровне БД. Сейчас нет под рукой VS 2010... но как доберусь до нее - проверю, реализовали ли эту возможность (соответствующие планы у команды EF были) в текущем public-build'е EF4 и обновлю пост.

Update. Продолжение в этом посте.

пятница, 10 июля 2009 г.

PRO-ключ к плагину Huagati DBML/EDMX Tools в подарок

Вчера мне на gmail пришло сообщение от Huagati Systems Co., Ltd. с ключом для плагина Huagati DBML/EDMX Tools (о котором я писал ранее). Судя по содержанию письма, это был ключ на PRO-версию плагина (стоимостью $119.95). Промелькнула мысль, что это благодарность за обзор продукта (хотя верилось с трудом). Когда же я в следующий раз посетил Google Analytics, увидел, что на мой пост ссылается в своем твиттере один из сотрудников Huagati Systems. Тут уж я не выдержал - написал в саппорт с целью узнать, не по ошибке ли мне был выслан ключ. Оказалось, действительно, серийник был выслан в знак благодарности за обзор (хотя рекламных целей я не преследовал; честно-честно :) ).

К чему все это? Дело в том, что мне этот ключ без надобности, и я с удовольствием подарю его (с разрешения Huagati Systems) первому попросившему (пожалуйста, пишите только если Вам действительно нужна Pro-лицензия). Напомню, что PRO-версия, во-первых, снимает ограничение на количество сущностей в модели, а во-вторых, содержит сборку, которая позволяет использовать функционал плагина в runtime.

вторник, 23 июня 2009 г.

Вышел Entity Framework CTP 1

Вчера был анонсирован Entity Framework CTP 1, включающий следующие возможности:
1. Улучшенная поддержка N-tier
2. POCO template (для автоматической генерации POCO-cущностей по модели)
3. Поддеркжа Code only подхода (вдобавок к существующим Model First и Code First).

Скачать EF4 CTP 1

четверг, 18 июня 2009 г.

Entity Framework 4: Часть 1. Работа над ошибками

В предыдущей статье по EF4 я, не найдя возможностей кастомизации процесса генерации DDL, предположил, что они не включены в beta 1 и оказался неправ. Оказывается, этот вопрос уже освещен в MSDN.

понедельник, 8 июня 2009 г.

Entity Framework 4: Часть 1. Pluralization, генерация DDL и удаление сущностей в дизайнере

В предыдущих статьях по Entity Framework мне удавалось обходить недостатки EF v. 1 стороной. Видимо, делал я это не так искусно, как мне казалось :) , поэтому меня стали подозревать в необъективности. Действительно, Vote of No Confidence родился не на ровном месте (хотя более информативен не сам Vote of No Confidence, а ответ на него Tim Mallalieu) - EF v. 1 содержит целый ряд существенных недоработок: отсутствие поддержки POCO, ограниченный маппинг хранимых процедур, принудительный и безальтернативный маппинг вторичных ключей как ассоциаций и т. д. Хотя указанные ограничения можно обойти... серьезная ORM должна поддерживать эти возможности без ритуальных танцев (как, например, это делает NHibernate). Так почему же я не касался этих моментов раньше? Я посчитал, что обсуждение недостатков EF v. 1 будет гораздо интереснее в рамках обзора EF4 (ранее известного как EF v. 2): так мы сможем рассмотреть не только проблемы EF v. 1, но и их решения, предлагаемые EF4.

Итак, предварительно скачав Visual Studio 2010 beta 1 (в состав которой входит EF4 beta 1) я приступаю к написанию серии статей по EF4.

Для начала создадим новое консольное приложение и добавим в него новую модель для базы данных, которую мы использовали в цикле статей "Влюбляемся в Entiy Framework" (бэкап базы прикреплен к этому посту). Щелкаем правой кнопкой по имени проекта -> Add ->New Item...
Первое, что бросается в глаза, - наличие двух темплейтов, касающихся EF. ADO.NET. EntityObject Generator мы пока проигнорируем (вернемся к нему в следующей статье) и создадим модель, воспользовавшись знакомым нам из EF v. 1 темплейтом ADO.NET Entity Data Model.

На первом шаге дизайнера выбираем Generate From Database, а на втором - указываем строку соединения и задаем ей имя SecondModelEntities. Здесь все без изменений. На следующем шаге выбираем таблицы Addresses и Persons и устанавливаем чекбокс "Pluralize or singularize generated object names", о назначении которого мы поговорим далее:
Кликаем Finish - модель создана.

Pluralization/Singularization
Дизайнер LINQ To Sql c самой первой версии поддерживал замечательную возможность: он учитывал тот факт, что обычно таблицы в базе данных именуются во множественном числе (Users, Products, Customers), и поэтому при именовании сущностей и "единичных" сторон ассоциаций использовал "сингулязированные" (затрудняюсь лаконично перевести этот термин) имена (в единственном числе: User, Product, Customer). Мелочь, а приятно: избавляет от унылой рутинной работы (особенно при первоначаольном заполнении модели).

Разработчики EF пошли дальше.

Во-первых, EF поддерживает не только преобразование из множественного числа в единственное, но и обратное - pluralization (на тот случай, если у Вас сущности в базе именуются в единственном числе, что тоже не редкость).

Во-вторых, в отличие от LINQ To SQL, в EF Pluralization/Singularization (далее - P/S) можно отключить. Зачем? Если не брать в расчет мазохистские соображения, P/S мешает людям, именующим сущности базы данных не на английском языке (а стандартная реализация, вполне логично, поддерживает только его). Причем, если EF видит в имени сущности символ из другого языка, P/S не применяется впринципе (имя сущности в этом случае используется as is). Проблемы возникают, когда сущность именуется на другом языке, но при этом содержит только латинские символы: так, немецкое слово Kunden (нем. заказчики) будет "плюрализовано" до Kundens, хотя на самом деле должно было быть "сингуляризовано" до Kunde. Жалобы разработчиков, использующих LINQ To SQL (а там отключить Singularization нельзя), материализовались в возможность отключения P/S в EF. Кстати, в ситуации с не английскими языками EF предлагает и более конструктивный подход - написание собственного P/S-сервиса.

Итак, в-третьих, P/S-сервис в EF4 не вещь в себе, как в LINQ To SQL: он может быть расширен путем реализации наследников абстрактного класс PluralizationService. Возьмем за основу код из поста в блоге EF Design:

PluralizationService pluralizationService = PluralizationService.CreateService(new CultureInfo("en-US"));
ICustomPluralizationMapping mapping = pluralizationService as ICustomPluralizationMapping;
if (mapping != null)
mapping.AddWord("Kunde", "Kunden"); //Обратите внимание, что метод ICustomPluralizationMapping.Add в beta 1 был переименован в AddWord
Рассмотрим этот код подробнее.

Сначала вызывается статический метод PluralizationService.CreateService, который возвращает наследника PluralizationService. Убедимся в этом при помощи Reflector'а:

public static PluralizationService CreateService(CultureInfo culture)
{
EDesignUtil.CheckArgumentNull<cultureinfo>(culture, "culture");
if (culture.TwoLetterISOLanguageName != "en")
{
throw new NotImplementedException("We don't support locales other than english yet");
}
return new EnglishPluralizationService();
}
Как видим, передавать в параметре culture не английскую культуру не имеет смысла - будет сгенерировано исключение. А вот реализация Pluralization-сервиса для английского языка возложена на класс EnglishPluralizationService. С реализацией этого класса Вы можете ознакомиться при помощи Reflector'а... а мы же пойдем дальше.

После вызова метода CreateService мы добавляем новое исключение при помощи метода ICustomPluralizationMapping.AddWord. В приведенном выше посте в блоге EF Design добавляется исключение child-children. Сделано это, видимо, с целью демонстрации и не более: это исключение уже есть в реализации класса EnglishPluralizatonService. Мы же добавим исключение kunde-kunden. Зачем? Таким образом мы сэмитируем поддержку немецкого языка. Если Вам вдруг понадобится работать с базой, в которой сущности именуются не на английском языке, а реализовывать наследника PluralizationService в Ваши планы не входит (ведь это не самая простая и тривиальная задача), можно обойтись добавлением в исключения имен сущностей, имеющихся в Вашей базе. Однако, как я уже отмечал, это сработает лишь для имен, состоящих из латинских букв: если EnglishPluralizationService находит в имени нелатинский символ, он не применяет к нему никаких преобразований и возвращает его as is.

Итак, мы создали экземпляр PluralizationService и добавили в него свое исключение - теперь необходимо сгенерировать модель при помощи нашего экземпляра PluralizationService. В дизайнере жестко прошито создание модели при помощи EnglishPluralizationService со стандартным набором исключений, поэтому модель нам придется создавать вручную. О том, как вручную генерировать SSDL, CSDL, MSL и классы, отлично написано в этом блог-посте.

Для того чтобы поэкспериментировать с созданием EDM при помощи API EF, создадим модель ThirdModel и на первом шаге дизайнера выберем Empty Model. В базе данных создадим новую таблицу Kunden с единственным атрибутом KundeId. Также создадим консольное приложение, которое будет генерировать SSDL, CSDL и MSL-файлы. После получения этих файлов мы подставим их в пустую (пока) модель. Таким образом, использовав кастомизированный PluralizationService для генерации первичной модели, мы лишим себя "удовольствия" переименования множества сущностей и навигационных свойств.

В целом, данный эксперимент не имеет большого практического смысла. Он, скорее, нацелен на получение опыта использования низкоуровнего API EF (который нам пригодится в следующей статье).

Для начала напишем код создания файлов со схемами:

static void Main(string[] args)
{
string connectionString = "Data Source=DEVELOPER;Initial Catalog=FirstModel;Integrated Security=True;MultipleActiveResultSets=True";
string provider = "System.Data.SqlClient";

string conceptualSchemaNamespaceName = "ThirdModel";
string storeSchemaNamespaceName = conceptualSchemaNamespaceName + ".Store"; //Гарантирует отсутствие конфликтов имен между CSDL и SSDL

//генерируем SSDL
EntityStoreSchemaGenerator storeGenerator = new EntityStoreSchemaGenerator(provider, connectionString, storeSchemaNamespaceName);
storeGenerator.GenerateStoreMetadata();
storeGenerator.WriteStoreSchema(@"StoreSchema.ssdl");
EntityContainer storeEntityContainer = storeGenerator.EntityContainer;

//инициализируем кастомизированный PluralizationService
PluralizationService pluralizationService = PluralizationService.CreateService(new CultureInfo("en"));
ICustomPluralizationMapping mapping = pluralizationService as ICustomPluralizationMapping;
if (mapping != null)
mapping.AddWord("Kunde", "Kunden"); //Обратите внимание, что метод ICustomPluralizationMapping.Add в beta 1 был переименован в AddWord

//генерируем CSDL и MSL
string entityContainerName = "ThirdModelEntities";
EntityModelSchemaGenerator generator = new EntityModelSchemaGenerator(storeEntityContainer, conceptualSchemaNamespaceName, entityContainerName, pluralizationService);
generator.GenerateMetadata();
generator.WriteModelSchema(@"ConceptualSchema.csdl");
generator.WriteStorageMapping(@"MappingSchema.msl");
}
После выполнения этого кода в папке bin/Debug проекта появятся три файла: StoreSchema.ssdl, ConceptualSchema.csdl и MappingSchema.msl. Откроем ConceptualSchema и убедимся, что таблица Kunden была "сингуляризована" до Kunde:

<EntityType name="Kunde">
<Key>
<PropertyRef name="KundeId">
</PropertyRef>
<Property nullable="false" type="Int32" name="KundeId">
</Property>
</Key>
</EntityType>
Обратите внимание, что приведенный код добавляет в SSDL все доступные сущности базы данных. Однако при необходимости методу storeGenerator.GenerateStoreMetadata можно передать в качестве параметра список фильтров.

Получив три составляющие EDM схемы, скопируем их в нашу пустую модель (открыв ее при помощи XML Editor), сохраним ее и откроем в дизайнере. Теперь с этой моделью можно работать, как ни в чем не бывало.

Надеюсь, Вас не сильно утомил мой притянутый за уши пример кастомизации PluralizationService. К сожалению, более оригинального способа демонстрации этой возможности я придумать не смог.

На этом разговор о P/S в EF4 beta 1 закончен - пришло время поговорить о EF v. 1. Стандартной реализации подобного сервиса в EF v. 1 нет, однако есть замечательный плагин Huagati DBML/EDMX Tools (как можно догадаться по названию, он поддерживает не только EF, но и LINQ To SQL). Плагин не бесплатный, но с достаточно демократичными ограничениями на trial-версию. Так, например, согласно FAQ, можно снять со своей триал-лицензии ограничение в 50 сущностей на модель, просто написав письмо с соответствующей просьбой по адресу licensing@huagati.com. Таким же путем можно и продлить свою trial-лицензию. Кстати, этот продукт выпускается не только как плагин, но и как библиотека, позволяющая выполнять преобразования в runtime (правда, библиотека поставляется только с Professional-лицензией).

После установки плагина и выделения элемента в окне дизайнера (или щелчка по пустому месту) в меню Visual Studio появится новый элемент "DBML/EDMX Tools":
Сейчас нас интересует пункт "Standardize ADO.NET Entity Data Model class and member names". После щелчка по нему появится форма:
, которая позволяет редактировать имена сущностей, скалярных и навигационных свойств (но не во время создания или обновления модели, как в EF4, а постфактум). Как видим, функциональность плагина в плане P/S гораздо шире возможностей EF4 beta 1.

Генерация DDL по модели
В EF4 beta 1 появилась возможность генерации базы (точнее ее DDL) по модели.

Щелкаем правой кнопкой в свободном пространстве дизайнера -> выбираем Generate Database Script from Model...
В результате будет сгенерирован DDL, который условно можно разбить на две части: в первой удаляются текущие элементы базы данных, а во второй создаются новые.
Кликаем Finish -> дизайнер заботливо предупреждает нас о том, что Storage Scheme и Mappings будут перезаписаны (они будут сформированы на базе текущей концептуальной модели). Так, например, если Вы удалили из сущности Address свойство HomeNumber, в сгенерированном DDL его не будет, а, следовательно, его не будет ни в Storage Scheme, ни в Mapping.

В созданной дизайнером вкладке Visual Studio будет содержаться тот самый DDL:
Кликаем F5 -> выбираем базу -> OK -> база данных обновилась.

О кастомизации процесса генерации DDL подробно написано в блоге EF Design; а с тем, как применять описанные там возможности, можно ознакомиться в MSDN.

Плагин "Huagati DBML/EDMX Tools" тоже умеет генерировать DDL (опция "Generate Database (New Database)"). Правда, плагин генерирует лишь DDL для создание сущностей, поэтому при накате DDL на непустую базу, ее сущности придется предварительно удалить вручную.

В целом, целесообразность использования генерации DDL по EDM представляется мне весьма сомнительной. Ведь в EDM не хранится информация о тригерах, индексах и т. д., а значит при каждом выполнении сгенерированного DDL (который, как я уже отмечал, сначала удаляет все текущие элементы базы данных, а потом создает их заново) эти элементы будут потеряны. Вот если бы EF генерировал DDL, который вносил бы изменения в базу при помощи Alter, а не при помощи Drop+Create, думаю, толку от этой возможности было бы гораздо больше.

Удаление сущностей в дизайнере
Раз уж мы сегодня так много говорим о дизайнере (хотя и P/S, и генерация DDL - возможности обновленного edmgen; дизайнер - лишь обертка), грех не упомянуть изменения, коснувшиеся процесса удаления сущности из дизайнера (вот эта возможность - всецело заслуга дизайнера). В EF v. 1 при удалении сущности при помощи дизайнера она удалялась из CSDL и MSL, но оставалась в SSDL. Если бы Вы хотели удалить сущность, но при этом в дальнейшем использовать маппинг на таблицу, лежащую в ее "основе", поведение EF v. 1 - именно то, что Вам нужно. Однако при необходимости удалить сущность полностью приходилось открывать модель при помощи Xml Editor и вручную удалять все касающиеся данной сущности элементы из SSDL.

В EF4 разрешили данный дуализм и сделали это, на мой взгляд, очень красиво. При попытке удалить сущность Вы увидите следующее диалоговое окно:
При выборе No сущность будет удалена, как в EF v. 1, а при выборе Yes произойдет полное удаление. Очень удобно.

При использовании EF v. 1 нас опять выручит плагин "Huagati DBML/EDMX Tools". На этот раз выберем опцию "Cleanup SSDL/MSL/CSDL":
В левой колонке отображаются сущности, которые присутствуют в SSDL, но отсутствуют в CSDL и MSL. Можно либо удалить выбранные сущности полностью, нажав Remove, либо восстановить их описание в CSDL и MSL, нажав Create CSDL. В правой колонке, наоборот, отображаются сущности, которые присутствуют в CSDL, но отсутствуют в SSDL и MSL. В первую очередь, это касается сущностей, которые были созданы в дизайнере и еще не были замаплены. Опять же, плагин предлагает две опции: либо удалить описание сущности из концептуальной модели (Remove), либо создать для нее MSL и SSDL (маппинг будет произведен на таблицу базы данных с именем, совпадающим с именем сущности в концептуальной модели).

Таким образом, и в этот раз возможности плагина превосходят возможности EF4.

Заключение
Сегодня мы рассмотрели достаточно поверхностные изменения в EF: и P/S, и генерация DDL не являются критичным для ORM функционалом (приверженцы NHibernate долгое время вообще без дизайнера обходились). В следующий раз мы поговорим на более серьезную тему, на тему поддержки POCO. До скорых встреч!

Прикрепленный файл

четверг, 28 мая 2009 г.

Влюбляемся в Entity Framework: Шаг пятый: Выполнение запросов и маппинг

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

За выполнение запросов в EF отвечает метод Execute уже знакомого нам класса ObjectQuery. В качестве аргумента метод Execute принимает параметр перечисления MergeOption. MergeOption определяет поведение кэша:
  • AppendOnly: Добавлять в кэш только новые записи. Существующие (в кэше) сущности не обновлять;
  • OverwriteChanges: Заменять текущие (current) значения существующих сущностей полученными значениями;
  • PreserveChanges: Заменять исходные (original) значения существующих сущностей полученными значениями. При этом текущие значения не изменяются, следовательно все изменения, внесенные до сих пор, остаются в силе;
  • NoTracking: Полученные значения не записываются в кэш
Трактовка значений MergeOption, данная выше, - перевод соответствующего раздела MSDN. Не считаю это описание интуитивно понятным, поэтому предлагаю разобраться с этим вопросом детальнее.

Рассмотрим небольшое тестовое приложение, которое демонстрирует, как значение MergeOption влияет на результат запроса:

using (FirstModel ctx = new FirstModel())
{
Person person = ctx.Persons.Where(p => p.PersonId == 1).First();

//После выполнения предыдущей строчки измените значение какого-нибудь свойства данной записи в базе вручную

ObjectQuery<person> personQuery = (ObjectQuery<person>)ctx.Persons.Where(p => p.PersonId == 1);

//Раскоментируйте поочередно нижележащие строчки и обратите внимание на возвращаемый результат
//Person person1 = personQuery.Execute(MergeOption.AppendOnly).First();
//person1.Person person2 = personQuery.Execute(MergeOption.OverwriteChanges).First();
//Person person3 = personQuery.Execute(MergeOption.PreserveChanges).First();
//Person person4 = personQuery.Execute(MergeOption.NoTracking).First();
}
Итак, после выполнения первой строчки у нас есть запись в кэше. Затем мы вручную меняем значение в базе и выполняем последующие запросы в четыре захода (для чистоты эксперимента).
  1. Полученное из базы значение игнорируется - возвращается значение из кэша;
  2. Текущие значения кэша подменяются значениями, полученными из базы - person2 содержит актуальные значения.
  3. Самый интересный случай. Как было описано выше, PreserveChanges лишь подменяет исходные значения, и не оказывает никакого влияния на текущие значения. Однако после выполнения теста в person3 будет содержаться актуальная информация из базы. Это связано с тем, что так как мы не производили никаких манипуляций с person, его текущие свойства не заданы. После выполнения запроса с PreserveChanges текущие значения останутся не заданными, поэтому в результате запроса будут возвращены исходные значения (которые к этому моменту уже были подменены на актуальные данные из базы). Если дополнить считывание person модификацией его свойств (например, если Вы планирует модифицировать в базе фамилию, достаточно дописать person.Surname = "AnySurname") - в person3 будут помещены текущие значения из кэша (в данном случае Surname будет равен AnySurname), а PreserveChanges, как ему и полагается, подменит лишь исходные значения.
  4. В случае NoTracking данные в кэш не заносятся и из кэша не считываются: person4 всегда будет содержать актуальные данные из базы.
Помимо явного вызова метода Execute, в EF есть ряд методов, делающих соответствующий вызов неявно (при этом в качестве MergeOption используется значение AppendOnly). Так, выполнение запроса происходит при вызове метода ObjectQuery.GetEnumenator, который (опять же неявно) происходит при использовании конструкции foreach, а также при вызове некоторых методов-расширений (например, ToList, ToArray и др.). Выполнение запроса произойдет и при вызове ряда других (не связанных с GetEnumenator) методов-расширений, таких как First/FirstOrDefault, Last/LastOfDefault и др.

Маппинг
Вот мы и добрались до самой "вкусной" возможности любой ORM-системы - до маппинга. К счастью, на эту тему уже написано достаточно статей, и повторяться я не буду. Я в очередной раз сошлюсь на статьи Сергея Розовика (раз, два), а опишу лишь маппинг Table splitting, которого в указанных статьях нет.

Table splitting
Представьте, что у Вас есть таблица, которая помимо относительно легких атрибутов содержит и тяжелые, вроде varbinary(max) или filestream (на примере SQL Server), причем в большинстве случаев требуются лишь легкие атрибуты:
  • Если мы храним в varchar(max) фотографию товара в высоком разрешении, то она понадобится лишь в тех редких случаях, когда пользователь интернет-магазина кликнет "посмотреть в полном размере";
  • Если мы храним в filestream содержимое файла библиотеки, то значение этого атрибута понадобится лишь при скачивании файла; во всех остальных случаях необходимы лишь описывающие файл атрибуты: имя, размер, дата загрузки и т. д.;
  • и т. д.
К нам на помощь приходит маппинг Table Splitting, который позволяет разделить работу с одной таблицей на несколько сущностей.

Добавим в таблицу Persons нашей базы данных (бэкап которой можно скачать в прикрепленном ко второй части файле) поле Photo типа varbinary(max). Для того чтобы не скачивать с сервера полуторамегабайтный jpeg каждый раз, когда нам нужна информация о пользователе, реализуем Table Splitting:
  1. В дизайнере скопируем и вставим сущность Person
  2. Переименуем полученную сущность в PersonPhoto (а ее Entity Set - в PersonPhotos) и в Mapping Details зададим Maps To Persons.
  3. Удалим из сущности Person свойство Photo, а из сущности PersonPhoto - свойства Name и Surname
  4. Добавим связь 1:1 между Person и PersonPhoto (правой кнопкой в окне дизайнера -> Add -> Association); затем выделим связь -> Mapping Details -> выберем Maps to Persons)
Сверим модель:
, Mapping Details сущности PersonPhoto:
и маппинг созданной связи:
Если сейчас попробовать сбилдить проект, в результате получим ошибку "Each of the following columns in table Persons is mapped to multiple conceptual side properties: Persons.PersonId is mapped to".

Для того чтобы избавиться от ошибки, необходимо создать ReferentialConstraint, описывающий связь между PersonId сущностей Person и PersonPhoto. К сожалению, дизайнер в EF v. 1 этой возможности не поддерживает, поэтому закроем дизайнер -> щелкнем правой кнопкой по FirstModel.edmx в Solution Explorer -> Open With... -> XML Editor... -> в разделе CSDL найдем определение ассоциации PersonPersonPhoto:

<Association Name="PersonPersonPhoto">
<End Type="FirstModelModel.Person" Role="Person" Multiplicity="1" />
<End Type="FirstModelModel.PersonPhoto" Role="PersonPhoto" Multiplicity="1" />
</Association>
и добавим в него ReferentialConstraint:

<Association Name="PersonPersonPhoto">
<End Type="FirstModelModel.Person" Role="Person" Multiplicity="1" />
<End Type="FirstModelModel.PersonPhoto" Role="PersonPhoto" Multiplicity="1" />
<ReferentialConstraint>
<Principal Role="Person">
<PropertyRef Name="PersonId" />
</Principal>
<Dependent Role="PersonPhoto">
<PropertyRef Name="PersonId" />
</Dependent>
</ReferentialConstraint>
</Association>
Вот теперь нас поджидает successful build.

Воспользуемся созданным маппингом и добавим запись в таблицу:

using (FirstModel ctx = new FirstModel())
{
Person person = new Person();
person.Name = "X";
person.Surname = "Y";

PersonPhoto personPhoto = new PersonPhoto();
personPhoto.Photo = new byte[] { 1, 2, 3, 4, 5 };

person.PersonPhoto = personPhoto;

ctx.AddToPersons(person);

ctx.SaveChanges();
}
При этом будет сгенерирован один INSERT-запрос, в котором задаются все поля (в том числе и Photo), а это значит, что маппинг работает. Правда, есть один нюанс. Так, если при создании сущности необходимо оставить свойству Photo значение NULL, нельзя просто не задавать его, как это обычно делается со скалярынми свойствами. Загвоздка в том, что таблицы Person и PersonPhoto связаны 1:1, а связать их 1:0..1 невозможно, ибо связывается первичный ключ одной и той же таблицы. Поэтому необходимо явно присвоить свойству Photo экземпляр класса PersonPhoto:

using (FirstModel ctx = new FirstModel())
{
Person person = new Person();
person.Name = "X";
person.Surname = "Y";

person.PersonPhoto = new PersonPhoto();

ctx.AddToPersons(person);

ctx.SaveChanges();
}
Считывать значения PersonPhoto можно следующими способами:
  • можно считать непосредственно PersonPhoto (для него создан свой EntitySet), отобрав записи по PersonId;
  • можно считать Person, а затем при помощи defered-loading подгрузить навигационное свойство PersonPhoto;
  • а можно, считывая Person, загрузить навигационное свойство PersonPhoto при помощи eager-loading:

using (FirstModel ctx = new FirstModel())
{
List<Person> persons = ctx.Persons.Include("PersonPhoto").ToList();

...
}
Сгенерированный в этом случае запрос радует глаз: вся загрузка сводится к одному простенькому SELECT'у (точно такому, какой бы Вы написали вручную).

Заключение
В мои планы входило описание еще одного редко встречающегося типа маппинга - Table per concrete type (TPC). Однако столкнувшись с рядом ограничений, а также с несовместимостью Table splitting и TPC, отказался от этой идеи. Надеюсь, в следующей версии реализация TPC будет улучшена.

На этом посте данный цикл статей будет временно заморожен. Хотя некоторое время спустя я, наверное, к нему вернусь. В ближайшее же время Вас ждет новый цикл статей, который (уж не знаю, к счастью или к сожалению) опять-таки будет посвящен Entity Framework. До скорых встреч!

вторник, 17 марта 2009 г.

Влюбляемся в Entity Framework: Шаг четвертый: Создание запросов

Cоздание запросов
Entity Framework предоставляет два основных типа запросов: LINQ To Entities и Entity SQL (eSQL). При этом если Object Services поддерживает оба типа запросов, то Entity Client поддерживает лишь eSQL. Да и вообще, Object Services является основным средством создания запросов, а к Entity Client, как правило, прибегают лишь в крайних случаях.

LINQ To Entities представляет собой реализацию LINQ (в статье подразумевается, что читатель знаком с основами LINQ) для работы с Entity Framework, а следовательно поддерживает два типа синтаксиса: query-syntax и method-base syntax (эти понятия буду приводить на английском языке: уж больно нелаконично они звучат на русском).

Entity SQL создавался как основное средство создания запросов. Однако c появлением LINQ стало очевидно, что при решении большинства задач его использование оказывается гораздо более удобным (к слову, разработчики NHibernate после релиза LINQ создали свою реализацию LINQ - LINQ To NHibernate). Синтаксис eSQL схож с синтаксисом SQL (хотя еще больше он похож на синтаксис hSQL ;) ), однако между ними существуют принципиальные различия (помимо того, что eSQL -язык запросов к модели, а SQL - язык запросов к реляционным БД): так, eSQL, в отличие от SQL, поддерживает наследование и навигационные свойства.

Object Services
Начнем с Lint To Entities.
1. Query-syntax.

using (FirstModel ctx = new FirstModel())
{
var persons = from p in ctx.Persons
where p.Address.City == "Tomsk"
select p;

Console.WriteLine("В Томске проживают:");
foreach (Person person in persons)
Console.WriteLine(person.Name + " " + person.Surname);

Console.ReadLine();
}
Результат выполнения запроса:
В Томске проживают:
Evgenii Salomatov
Andrei Lupanov
Vitalii Pogonin
Evgenii Hudoba
Мы написали запрос, который возвращает всех Person, проживающих в городе Томске (этот запрос мы будем использовать для всех примеров).

2. Method-based syntax.
Перепишем запрос:

using (FirstModel ctx = new FirstModel())
{
var persons = ctx.Persons.Where(p => p.Address.City == "Tomsk");

Console.WriteLine("В Томске проживают:");
foreach (Person person in persons)
Console.WriteLine(person.Name + " " + person.Surname);

Console.ReadLine();
}
Перейдем к eSQL-запросам.

1. Чистый eSQL.
В ObjectContext есть метод CreateQuery, который позволяет выполнять eSQL-запросы:

using (FirstModel ctx = new FirstModel())
{
string queryString = "SELECT VALUE p FROM FirstModel.Persons AS p WHERE p.Address.City ='Tomsk'";
var persons = ctx.CreateQuery<Person>(queryString);

Console.WriteLine("В Томске проживают:");
foreach (Person person in persons)
Console.WriteLine(person.Name + " " + person.Surname);

Console.ReadLine();
}
2. Query-builder методы.
Некоторые методы (Where, Select и др.), используемые в method-based LINQ To Entities
(лишь метод Include можно использовать и с query syntax), принимают в качестве аргумента не делегат, а eSQL.

Перепишем наш method-based запрос с применением query-builder метода:

using (FirstModel ctx = new FirstModel())
{
var persons = ctx.Persons.Where("it.Address.City == 'Tomsk'");

Console.WriteLine("В Томске проживают:");
foreach (Person person in persons)
Console.WriteLine(person.Name + " " + person.Surname);

Console.ReadLine();
}
Данный способ приходится очень кстати, когда нужно добавить в запрос немного динамики (например, сортировку по полям, выбранным пользователем), но при этом не хочется терять строгую типизацию, переходя на чистый eSQL-запрос.

В запросе используется алиас по умолчанию - "it". Его можно изменить, воспользовавшись свойством Name класс ObjectQuery:

var persons = ctx.Persons;
persons.Name = "p";
persons = persons.Where("p.Address.City == 'Tomsk'");
Подведем промежуточные итоги.
1. Когда использовать чистые eSQL-запросы? Тогда, когда запрос формируется динамически, и query-builder методов не достаточно.

2. Что выбрать: query или method-based LINQ To Entities? Здесь многое зависит от личных предпочтений (для кого-то выразительность query-синтаксиса может оказаться дороже всех преимущества method-based синтаксиса)... но я рекомендую использовать method-based синтаксис: во-первых, при помощи query-синтаксиса можно выразить далеко не весь функционал LINQ (и LINQ To Entities в частности), а, во-вторых, из 13 query-builder методов query-синтаксис поддерживает лишь один - Include.

EntityClient
Если, читая статью, Вы успели соскучиться по старому-доброму ADO.NET, у меня для Вас приятный сюрприз. EntityClient, являясь более низкоуровневым компонентом, чем Object Services (как мы выяснили во второй статье, Object Service считывает данных при помощи EntityClient, а затем материализует их), оперирует умными наследниками таких базовых классов ADO.NET, как DbConnection, DbCommand, DbDataReader и т. д. Почему умными? Потому что в них учитывается специфика Entity Framewok: так, например, EntityDataReader поддерживает не только скалярные значения, но и DbDataReader, DbDataRecord и EntityKey.

Отмечу, что EntityClient не является самым низкоуровневым средством создания запросов, так как EDM поддерживает запросы на нативном SQL (при помощи Defining Query).

Перепишем наш запрос с использованием EntityClient:

using (EntityConnection connection = new EntityConnection("name=FirstModel"))
{
string queryString = "SELECT VALUE p FROM FirstModel.Persons AS p WHERE p.Address.City ='Tomsk'";
EntityCommand command = connection.CreateCommand();
command.CommandText = queryString;

connection.Open();

Console.WriteLine("В Томске проживают:");

using (EntityDataReader dataReader = command.ExecuteReader(CommandBehavior.SequentialAccess))
{
while (dataReader.Read())
{
string firstName = dataReader.GetString(1);
string secondName = dataReader.GetString(2);

Console.WriteLine(firstName + " " + secondName);
}
}

Console.ReadLine();
}
Вот мы и рассмотрели все типы запросов в EF (кроме DefiningQuery... но это тема для отдельного разговора).

Напоследок я раскрою третий аргумент в противостоянии method-based vs. query syntax.

ObjectQuery vs. IQueryable
ObjectQuery - это класс, который содержит все необходимую информацию о EF-запросе. Он реализует ряд интерфейсов, в том числе и IQueryable. Некоторые Linq To Entities методы возвращают ObjectQuery, а некоторые - IQueryable:
Самая серьезная потеря в IQueryable по сравнению с ObjectQuery - метод Execute (который мы рассмотрим в начале следующей статьи). Однако потерю можно восполнить: на самом деле эти методы возвращают не IQueryable, а ObjectQuery, поэтому нам нужно лишь привести тип:

using (FirstModel ctx = new FirstModel())
{
var iQueryablePersons = from p in ctx.Persons
where p.Address.City == "Tomsk"
select p;
var objectQueryPersons = (ObjectQuery)iQueryablePersons;
objectQueryPersons.Execute(MergeOption.AppendOnly);

Console.WriteLine("В Томске проживают:");
foreach (Person person in objectQueryPersons)
Console.WriteLine(person.Name + " " + person.Surname);

Console.ReadLine();
}
Заключение
К этому моменту мы изучили подноготную запросов (шаг 3) и типы запросов (эта статья). В следующий раз (боюсь, это случится не очень скоро) мы подробнее остановимся на процессе выполнения запросов и маппинге.

понедельник, 16 марта 2009 г.

Влюбляемся в Entity Framework: Шаг третий: Введение в запросы

Непринужденный флирт (первые две статьи) позади - пора познать внутренний мир Entity Framework. Первым шагом на этом пути станет изучение запросов в EF. Однако сначала разберемся, откуда у сгенерированных дизайнером классов ноги растут.

Изучаем EDM и сгенерированные дизайнером классы
Откроем нашу модель дизайнером и произведем небольшие изменения:
  1. Выделим сущность Persons и в окне Properties выставим: Name в Person, Entity Set Name в Persons
  2. В сущности Person в разделе Navigation Properties выделим Addresses и в окне Properties зададим свойству Name значение Address
  3. Выделим сущность Addresses и в окне Properties выставим: Name в Address, Entity Set Name в Addresses
  4. Сохраним модель и закроем ее.
Откроем FirstModel.edmx при помощи XML-редактора и рассмотрим концептуальную модель:

<edmx:ConceptualModels>
<Schema Namespace="FirstModelModel" Alias="Self" xmlns="http://schemas.microsoft.com/ado/2006/04/edm">
<EntityContainer Name="FistModel">
<EntitySet Name="Addresses" EntityType="FirstModelModel.Address" />
<EntitySet Name="Persons" EntityType="FirstModelModel.Person" />
<AssociationSet Name="FK_Persons_Addresses" Association="FirstModelModel.FK_Persons_Addresses">
<End Role="Addresses" EntitySet="Addresses" />
<End Role="Persons" EntitySet="Persons" />
</AssociationSet>
</EntityContainer>
<EntityType Name="Address">
<Key>
<PropertyRef Name="AddressId" />
</Key>
<Property Name="AddressId" Type="Int32" Nullable="false" />
<Property Name="City" Type="String" Nullable="false" MaxLength="50" Unicode="true" FixedLength="false" />
<Property Name="Street" Type="String" Nullable="false" MaxLength="50" Unicode="true" FixedLength="false" />
<Property Name="PostalCode" Type="String" Nullable="false" MaxLength="6" Unicode="true" FixedLength="false" />
<Property Name="Apartment" Type="String" MaxLength="6" Unicode="true" FixedLength="false" />
<NavigationProperty Name="Persons" Relationship="FirstModelModel.FK_Persons_Addresses" FromRole="Addresses" ToRole="Persons" />
</EntityType>
<EntityType Name="Person">
<Key>
<PropertyRef Name="PersonId" />
</Key>
<Property Name="PersonId" Type="Int32" Nullable="false" />
<Property Name="Name" Type="String" Nullable="false" MaxLength="50" Unicode="true" FixedLength="false" />
<Property Name="Surname" Type="String" Nullable="false" MaxLength="50" Unicode="true" FixedLength="false" />
<NavigationProperty Name="Address" Relationship="FirstModelModel.FK_Persons_Addresses" FromRole="Persons" ToRole="Addresses" />
</EntityType>
<Association Name="FK_Persons_Addresses">
<End Role="Addresses" Type="FirstModelModel.Address" Multiplicity="0..1" />
<End Role="Persons" Type="FirstModelModel.Person" Multiplicity="*" />
</Association>
</Schema>
</edmx:ConceptualModels>
Полный разбор концептуальной модели в мои планы на эту статью не входит: сделаю акцент на том, что имеет отношение к сегодняшней теме.

Концептуальная модель состоит из EntityContainer, множества определений EntityType и определений Association.

EntityContainer хранит опиcания EntitySet'ов и AssociationSet'ов.

Каждый элемент EntityType определяет сущность концептуальной модели. Элемент Key определяет совокупность свойств, являющихся первичным ключом. Элементы Property задают скалярные свойства сущности. А элементы NavigationProperty служат для связи сущностей.

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

Классы-сущности
Каждый EntityType преобразуется в класс-сущность (приведен упрощенный вариант кода):

public partial class Person : EntityObject
{
private int _PersonId;
partial void OnPersonIdChanging(int value);
partial void OnPersonIdChanged();
public int PersonId
{
get
{
return this._PersonId;
}
set
{
this.OnPersonIdChanging(value);
this.ReportPropertyChanging("PersonId");
this._PersonId = StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("PersonId");
this.OnPersonIdChanged();
}
}

//...
//Остальные скалярные свойства

public Address Address
{
get
{
return ((IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference("FirstModelModel.FK_Persons_Addresses", "Addresses").Value;
}
set
{
((IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference("FirstModelModel.FK_Persons_Addresses", "Addresses").Value = value;
}
}

public EntityReference AddressReference
{
get
{
return ((IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference("FirstModelModel.FK_Persons_Addresses", "Addresses");
}
set
{
if ((value != null))
{
((IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedReference("FirstModelModel.FK_Persons_Addresses", "Addresses", value);
}
}
}
}

public partial class Address : EntityObject
{
private int _AddressId;
partial void OnAddressIdChanging(int value);
partial void OnAddressIdChanged();
public int AddressId
{
get
{
return this._AddressId;
}
set
{
this.OnAddressIdChanging(value);
this.ReportPropertyChanging("AddressId");
this._AddressId = StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("AddressId");
this.OnAddressIdChanged();
}
}

//...
//Остальные скалярные свойства

public EntityCollection Persons
{
get
{
return ((IEntityWithRelationships)(this)).RelationshipManager.GetRelatedCollection("FirstModelModel.FK_Persons_Addresses", "Persons");
}
set
{
if ((value != null))
{
((IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedCollection("FirstModelModel.FK_Persons_Addresses", "Persons", value);
}
}
}
}
Если элементы Property просто конвертируются в C#-свойства (с добавлением partial-методов для валидации), то с NavigationProperty все несколько сложнее. Возможны два варианта:
  1. Если навигационному свойству соответствует 0..1 объект (как, например, в случае Person.Address), для него дизайнер сгенерирует два свойства: первое - с типом данных связанной сущности (Address) и с именем соответствующего NavigationProperty (Address), а второе - с типом EntityReference и с добавлением к имени постфикса "Reference" (AddressReference). Первое свойство - это непосредственно связанный с сущностью объект (адрес данной личности), второе - вспомогательное свойство, содержащее функционал, который пригодится при более изощренном использовании EF.
  2. Если навигационному свойству соответствует множество объектов (как, например, в случае Address.Persons), для него будет сгенерировано одно свойство типа EntityCollection с именем соответствующего NavigationProperty (Persons). В этом случае вспомогательного свойства не требуется: весь необходимый функционал встроен в EntityCollection.
Возникает вопрос, как определить сколько объектов (0..1 или множество) соответствует навигационному свойству? Конечно, данная информация есть в EDM, но нагляднее это изображено в дизайнере:
Подписи к ассоциации говорят, что "одному Address соответствует множество Person, а одному Person соответствует 0 либо 1 Address".

При создании модели лучше сразу переименовать навигационные свойства таким образом, чтобы они отображали "множественность".

Наследник ObjectContext
Заканчивая разговор о классах, сгенерированных дизайнером, рассмотрим класс FirstModel, который является наследником ObjectContext.

Как было отмечено в прошлой статье, ObjectContext является центральным классом Object Services. Однако на практике используется не сам ObjectContext, а его наследник, генерируемый дизайнером, т. к. в нем есть ряд свойств и методов, упрощающих работу:

public partial class FistModel : ObjectContext
{
private ObjectQuery _Addresses;
public ObjectQuery Addresses
{
get
{
if ((this._Addresses == null))
{
this._Addresses = base.CreateQuery("[Addresses]");
}
return this._Addresses;
}
}

private ObjectQuery _Persons;
public ObjectQuery Persons
{
get
{
if ((this._Persons == null))
{
this._Persons = base.CreateQuery("[Persons]");
}
return this._Persons;
}
}

public void AddToAddresses(Address address)
{
base.AddObject("Addresses", address);
}

public void AddToPersons(Person person)
{
base.AddObject("Persons", person);
}
}
Как мы выяснили в начале статьи, в EntityContainer хранятся определения EntitySet (в том числе). Так вот для каждого EntitySet в FirstModel генерируется:
  1. Свойство типа ObjectQuery для обращения к этому набору (Addresses, Persons)
  2. Метод для добавления элементов в этот набор (AddToAddresses, AddToPersons)
Может показаться, что для каждого EntityType есть свой EntitySet (как у нас в примере), однако это всего лишь частный случай (например, при реализации наследования EntitySet определяется только для базовой сущности).

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

вторник, 10 марта 2009 г.

Влюбляемся в Entity Framework: Шаг второй: Архитектура EF

Создание первой модели
Дабы дальнейший разговор был более предметным, создадим нашу первую модель. Предварительно необходимо в Sql Server 2005 заресторить базу данных из бэкапа, расположенного в прикрепленном к этому посту файле. Далее:
  1. Создадим в Visual Studio 2008 новый проект Console Application и назовем его FirstModel.
  2. В Solution Explorer правой кнопкой кликаем по имени проекта -> Add -> New Item -> выбираем ADO.NET Entity Data Model и вводим имя FirstModel -> Add
  3. Выбираем Generate From Database (по умолчанию) -> Next
  4. Заходим в New Connection, где создаем соединение к базе данных First Model. Если Вы выбрали Sql Server Authentication, то после создания строки нужно будет подтвердить хранение логина и пароля в строке соединения, выбрав "Yes, include sensitive data in the connection string". В поле ввода имени строки соединения вводим FirstModel (фактически это не имя строки соединения, а Entity Container Name, но об этом мы подробнее поговорим как-нибудь в следующий раз) -> Next
  5. В появившемся окне необходимо выбрать элементы, которые будут добавлены в модель. Выбираем таблицы Addresses и Persons -> Finish.
Модель сформирована - можем приступать к изучению ее "внутренностей".

Изучаем модель изнутри
После создания модели в проект было добавлено 2 файла: FirstModel.edmx и FirstModel.Designer.cs. Кроме того, Visual Studio открыла ForstModel.edmx EF-дизайнером.

FirstModel.edmx - XML файл, описывающий Entity Data Model (EDM), а также содержащий некоторую вспомогательную информацию для EF-дизайнера.

Я не буду подробно останавливаться на рассмотрении EDM: об этом подробно писал Сергей Розовик. Вкратце, EDM является одной из основных концепций EF и представляет собой совокупность трех элементов: схемы хранилища (Store Schema Definition Language (SSDL) = Storage Schema), концептуальной схемы (Conceptual Schema Definition Language (CSDL) = Conceptual Schema) и спецификации отображения (Mapping Specification Language (MSL) = С-S mapping).

Чтобы ознакомиться с содержимым EDM, закроем вкладку с FirstModel.edmx, щелкнем правой кнопкой по FirstModel.edmx в Solution Explorer -> Open With... -> XML Editor -> OK.
На этапе компиляции EDM поэлементно разбивается на три файла, которые встраиваются в ресурсы сборки. Это поведение можно изменить: например, если Вам необходимо во время выполнения динамически вносить изменения в EDM, файлы можно хранить рядом со сборкой.

Перейдем к рассмотрению файла FirstModel.Designer.cs. Как несложно догадаться из названия, этот файл был сгенерирован EF-дизайнером. Заглянем во внутрь.
Дизайнер любезно сгенерировал для нас три класса: класс FirstModel, являющийся наследником ObjectContext, и две сущности Addresses и Persons, являющиеся наследниками EntityObject.

Кстати, namespace, который мы вводили при создании модели, - это внутренний namespace модели, который используется для полного именования сущностей. Namespace же сгенерированных классов определяется их местонахождением в проекте.

Теперь настало время абстрагироваться от нашего примера и посмотреть, как же он вписывается в общую архитектуру Entity Framework.

Архитектура Entity Framework
Рассмотрим схему, изображающую основные компоненты (и способы взаимодействия с ними) Entity Framework:
Работа EF выглядит примерно следующим образом:
  1. Формируется запрос к EntityClient (при помощи Object Services либо напрямую)
  2. EntityClient при помощи метаданных преобразует Entity SQL или Linq To Entities-запрос в SQL-запрос
  3. SQL-запрос отдается на выполнение ADO.NET провайдеру, указанному в строке соединения (в нашем случае - SqlClient)
  4. После обращения провайдера к СУБД данные в обратном порядке возвращаются.
Детали выполнения запросов мы рассмотрим в следующей статье, сейчас же главная задача - изучить три основных (на мой взгляд) компонента архитектуры EF: Metadata Files (EDM), Entity Client и Object Services.
EDM мы уже рассмотрели в предыдущей главе, так что смело переходим к изучению Entity Client.

EntityClient
EntityClient - самый низкоуровневый способ создания запросов к EDM. EntityClient отличается от Object Services тем, что он не материализует данные в объекты, а просто возвращает данные в виде строк и столбцов посредством EntityDataReader.

При работе с EntityClient невольно вспоминаешь старые-добрые(?) времена, когда основными действующими лицами были SqlClient, OracleClient и т. д. : приходится оперировать аналогичными классами: EntityConnection, EntityCommand, EntityParameter и т. д.

Зачем в Entity Framework нужен столь низкоуровневый подход? На практике самым популярным применением EntityClient является реализация того, что не умеет (или умеет, но плохо) Object Services (например, работа с некоторыми типа хранимых процедур). Об этом мы обязательно подробно поговорим в будущих статьях.

Object Services
Object Services является верхушкой API Entity Framework и располагается в пространстве имен System.Data.Objects. Object Services реализует весь необходимый функционал для удобного создания и взаимодействия с сущностями концептуальной модели. Основным компонентом Object Services является класс ObjectContext, наследником которого в нашей модели является сгенерированный дизайнером класс FirstModel.

Функционал ObjectServices можно разбить на четыре основные группы:
  1. Обработка запросов (Query Processing). Как я уже отмечал, обработку запросов мы отложим до следующей статьи. Однако по рисунку несложно догадаться, что Object Services позволяет делать запросы как при помощи LINQ To Entities, так и при помощи Entity SQL.
  2. Материализация объектов (Object materialization). После получения результатов от ADO.NET-провайдера EntityClient возвращает в Object Services EntityDataReader, после чего Object Services материализует результаты в экземпляры сущностей.
  3. Управление состоянием объектов (Object state management). ObjectContext хранит экземпляр ObjectStateEntry для каждого экземпляра сущности (entity) и взаимосвязи (relationship). В частности, в ObjectStateEntry хранятся оригинальные и текущие значения свойств сущности, что позволяет реализовать отслеживание изменений (change tracking).
  4. Управлением взаимосвязями объектов (Object relationship management). Хотя экземпляры сущностей и "знают", как обратиться к связанным объектам, именно ObjectContext обеспечивает эти взаимосвязи.
Заключение
Надеюсь к этому моменту мне удалось передать в общих чертах суть архитектуры Entity Framework. Понимание того, как и какие компоненты взаимодействуют друг с другом очень важно при решении более сложных задач, до которых мы со временем обязательно доберемся.

Прикрепленный файл

понедельник, 9 марта 2009 г.

Влюбляемся в Entity Framework: Шаг первый: Введение

Пролог
Этот пост является началом цикла статей по Entity Framework, идея написания которого преследует меня уже достаточно продолжительное время.

Что побудило меня на это начинание?

Во-первых, мне нравятся ORM-системы (со всеми своими достоиствами и недостатками).
Во-вторых, несмотря на то, что мне приходится иметь дело с разными ORM, c Entity Framework меня связывают особенное теплые отношения :)
В-третьих, уже прошло более полугода с момента релиза EF v. 1 (а с моменты появления стабильных бета-версий - и того больше), а материалов в русскоговорящем сообществе до сих пор довольно мало. Здесь я хотел бы отметить статьи Сергея Розовика в блоге stump-workshop.blogspot.com (справа в блоке "Темы" выберите "Entity Framework"). Вероятно, я еще не раз буду ссылаться на эти статьи, дабы не повторяться.

Я сомневался, как писать этот цикл статей: последовательно от простого к сложному или как набор постов, описывающих определенные подходы и решающих конкретные задачи. С одной стороны, хочется, как можно скорее, поделиться интересными мыслями и наработками (это аргументы в пользу второго варианта), с другой, - ввиду отсутствия обширной информации по EF на русском языке, гораздо более уместным мне видится первый вариант. На нем и остановился.

Прежде чем приступить к введению, хотел бы отметить, что стиль именования статей я позаимствовал у Дмитрия Сошникова (с его разрешения). Со статьями Дмитрия Вы можете ознакомиться в его блоге на Хабре.

Введение
Итак, что же такое Entity Framework? EF - это реализация фирмы Microsoft технологии Object-relational Mapping (ORM) под .NET.

Так сложилось, что в области обработки данных (data management) мэйнстримом являются объектно-ориентированные языки программирования, а в области хранения данных (data storing) - реляционные базы данных (РБД).

ORM - это подход, позволяющий конвертировать данные между реляционными базами данных и объектно-ориентированными языками программирования, тем самым нивелируя так называемый object-relational impedance mismatch (по-русски это звучит не очень лаконично, вроде "несоответствие принципов, лежащих в основе объектного и реляционного подходов", поэтому позволю себе использовать английский вариант).

Рассмотрим ряд различий между объектной и реляционной концепциями:
  1. Различия в базовых принципах. ООП базируется на принципах создания программного обеспечения (инкапсуляция, наследование, полиморфизм), в то время как реляционная парадигма имеет в основе математические правила (операции с множествами). Если инкапсулацию и полиморфизм на уровне РБД представить невозможно (да, наверное, и не нужно), то с наследованием ORM справляются на отлично.
  2. Различия в оперируемых сущностях. В ООП мы оперируем объектами и ссылками на объекты, а в РБД - таблицами, атрибутами и записями. ORM с блеском нивелирует это различие, позволяя программисту обращаться к связанным объектам традиционными для ООП способами, лишая программиста "удовольствия" создания разнообразных join-запросов.
  3. Различия в оперируемых типах (эта проблема не является уникальной для взаимодействия объектной и реляционной систем: с ней приходится сталкиваться даже при взаимодействии двух объектно-ориентированных платформ, например, через Web-сервисы). В качестве примера обычно приводят тип данных "строка". Реляционные базы данных позволяют ограничивать размер строк, тогда как в большинстве объектно-ориентированных языков размер строки ограничивается лишь объемом доступной виртуальной памяти. В любом случае конвертация типов - рутинная работа, которую лучше автоматизировать за счет использования ORM.
Безусловно, на этом отличия не заканчиваются. Но, думаю, этого достаточно для того, чтобы понять, что ORM-системы появились не на ровном месте, и имеют право на существование.

Кроме того, ORM-системы направлены не только на борьбу с object-relational impedance mismatch, они предлагают и ряд других полезных возможностей (набор таких возможностей индивидуален для каждой конкретной реализации). Так, например, ORM позволяют генерировать SQL-запросы налету: Вы выполняете определенные операции с сущностями, отдаете команду ORM-системе, а она генерирует SQL-запрос и отдает его на выполнение. Это ли не чудо?! Больше не нужно писать рутинный код для работы с базой данных! Больше не нужно судорожно делать бесконечные Find&Replace при малейших изменениях схемы базы! Больше не нужно...

Стоит отметить, что до появления ORM (Hibernate 1.0 датирован лишь 2002 годом) программисты зачастую создавали свои собственные утилиты, которые по возможностям очень напоминали сегодняшние ORM. Таким образом, появление серьезных ORM стало не революцией, а, скорее, эволюцией, качественным улучшением средств реализации слоя доступа к данным.

Противники скрещивания ужа (объектной парадигмы) с ежом (реляционной парадигмой) совершенно справедливо указывают на то, что более логичным и лежащим на поверхности решением object-relational impedance mismatch был бы переход на объектно-ориентированные базы данных (ООБД). Однако, несмотря на то, что идея ООБД не нова (начало 70-х годов 20-го века), позиции объектно-ориентированных СУБД очень шатки. Причин на то много, но самая существенная, на мой взгляд, - как раз прочность позиций РБД.

Дабы обратить внимание на другие парадигмы, решил привести ссылку на пост двухгодичной давности с Enfranchised Mind, посвященный functional-relational impedance mismatch. Если я все правильно понял, описываемое там решение уже реализовано в .NET, и имя ему - Linq (в случае EF - Linq To Entities). Однако этот пост подтолкнул меня к дальнейшим размышлениям, в ходе которых я пришел к тому, что ORM - верный путь, ибо не напасешься СУБД, заточенных под каждую конкретную парадигму (все-таки ORM-система и СУБД - не сопоставимые по сложности задачи). А затем я вспомнил то, как ужасно ведут себя объемные RDF-хранилища, реализуемые на базе реляционных СУБД (лирическое отступление на тему Semantic Web). А все потому, что реляционая парадигма совершенно не отражает принципы, заложенные в онтологии (так, например, такая мощная возможность СУБД, как индексы, вынуждена простаивать без дела).

Так что мнение "лучше использовать ORM, чем внедрять объектно-ориентированную СУБД" актуально лишь для мэйнстримовых (на данный момент) задач. При решении же более узкоспециализированных задач возможны и диаметрально-противоположные мнения.

P. S.
Категорически приветствую критику. Если Вы заметили, что я ошибся, допустил неточность, или Вам кажется, что какой-то момент нужно рассмотреть более детально, - оставляйте комментарии или пишите на e-mail (указан в профиле).