понедельник, 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. До скорых встреч!

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

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

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

Добрый день, а не подскажите (или киньте в меня ссылкой), что есть POCO?

Idsa комментирует...

Добрый день!
Ссылочкой кину - http://en.wikipedia.org/wiki/POCO, а подробности будут в следующей статье.

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

Ага, спасибо большое. Что-то в английской википедии не додумался посмотреть - а в русской нету

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

Тяжело читать примеры кода в статье: приходится постоянно скролить. Было бы здорово чуть пошире сделать шаблон страницы.

Рома комментирует...

Тяжело читать примеры кода в статье: приходится постоянно скролить. Было бы здорово чуть пошире сделать шаблон страницы.

_______________________
Солидарен с предыдущим оратором

Idsa комментирует...

Изменил шаблон. Поделитесь вашими впечатлениями