среда, января 30, 2008

Entity Framework - mapping (часть 2)

Продолжение. Начало здесь

Один класс - несколько EntitySet.



Еще один интересный вариант маппинга один класс – несколько таблиц, когда все таблицы имеют одинаковую структуру. В терминах EF это называется multiple entity sets per type (MEST). Рассмотрим пример БД:



БД содержит две таблицы одинаковой структуры для хранения каких-то запросов. Поскольку запросов много, для хранения текущих запросов используется таблица Request, а устаревшие и обработанные запросы переносятся в таблицу RequestHistory.

Если создать EDM визардом, мы получим две идентичные сущности Request и RequestHistory, и два соответствующих им EntitySet. Согласитесь это не очень удобно. Вот если бы EntitySet Request и RequestHistory остались, но оба использовали один и тот-же тип сущности. И это можно сделать. Однако нам придется опять расстаться с дизайнером, поскольку он не поддерживает такой вариант маппинга.

Итак, закрываем дизайнер и открываем edmx файл в XML редакторе. Изменяем описание EntitySet RequestHistory, чтобы он использовал класс Entity вместо EntityHistory:


<EntityContainer Name="MultiSet">
<EntitySet Name="Request" EntityType="MultiSetModel.Request" />
<EntitySet Name="RequestHistory" EntityType="MultiSetModel.Request" />
</EntityContainer>


Затем удаляем описание сущности RequestHistory (<EntityType Name="RequestHistory">).

Наконец правим маппинг EntitySet RequestHistory, заменяем тип RequestHistory на Request (измененное выделено):


<EntitySetMapping Name="RequestHistory">
<EntityTypeMapping TypeName="IsTypeOf(MultiSetModel.Request)">
<MappingFragment StoreEntitySet="RequestHistory">
<ScalarProperty Name="Id" ColumnName="Id" />
<ScalarProperty Name="IncomDate" ColumnName="IncomDate" />
<ScalarProperty Name="Text" ColumnName="Text" />
<ScalarProperty Name="AcceptDate" ColumnName="AcceptDate" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>



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

Маппинг на хранимые процедуры



Многие спрашивают, поддерживает ли EF при маппинге классов хранимые процедуры? Для любителей маппинга на хранимые процедуры у меня две новости, хорошая и плохая. Хорошая состоит в том, что маппинг сущностей на хранимые процедуры в EF есть. Плохая – полностью отказаться от использования запросов в маппинге не возможно. Использовать хранимые процедуры в маппинге сущностей можно для операций вставки, обновления и удаления. В EF beta3 эти возможности уже поддерживаются дизайнером EDM. Однако для выборки все равно нужен маппинг на таблицы или view.

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

Создадим таблицу Category с двумя полями Id (первичный ключ) и Title:



CREATE TABLE Category(
Id int IDENTITY(1,1) NOT NULL,
Title nvarchar(50) NOT NULL,
CONSTRAINT PK_Category PRIMARY KEY CLUSTERED (Id ASC)



Обратите внимание, что ключевое поле Id объявлено как IDENTITY, это повлияет на некоторые детали маппинга.

Далее нам необходимо создать три хранимые процедуры для вставки, обновления и удаления записей в таблице Category:



CREATE PROCEDURE [dbo].[CreateCategory]
@Title nvarchar(50)AS
BEGIN
INSERT INTO [Category] ([Title])
VALUES(@Title)

SELECT [Id] from [Category] where @@ROWCOUNT > 0 and [Id] = scope_identity()
END

CREATE PROCEDURE [dbo].[UpdateCategory]
@Id int,
@Title nvarchar(50)AS
BEGIN
UPDATE [Category]
SET [Title] = @Title
WHERE [Id] = @Id
END

CREATE PROCEDURE [dbo].[DeleteCategory]
@Id int as
BEGIN
DELETE [Category]
WHERE [Id] = @Id
END



Из-за того, что столбец Id у нас объявлен как Identity, в процедуру CreateCategory передается только один параметр @Title, иначе надо было бы передавать еще и @Id. По этой же причине в процедуре присутствует select, который возвращает значение Id только что добавленной строки. И наконец, по этой же причине, в процедуре UpdateCategory параметр @Id используется исключительно для поиска нужной записи.

Теперь можно создать EDM по нашей базе, при этом в визарде надо поставить галочки для импорта хранимых процедур. Я не буду останавливаться на том, как хранимые процедуры описываются в EDM, там все довольно очевидно просто загляните в edmx файл и в документацию по EF.

Теперь займемся маппингом. В левом углу панели «Mapping details» есть две кнопки, которые переключают режим работы панели: «Map Entity to Tables / Views» и «Map Entity to Functions». До сих пор мы пользовались только первым режимом. Теперь нам надо переключиться на второй:



В пустой панели предлагается выбрать три функции для Insert, Update и Delete. Выбираем функции в выпадающих списках. Затем мапим параметры. Для функции CreateCategory необходимо выполнить Result Column Bindings. Для этого на месте надписи <Add Result Binding> вводим «Id» и мапим его на свойство Id. Таким образом, EF сможет получать значение Id вставленной записи.

Для параметров функции Update мы видим флажки «Use Original Value». При помощи этого флажка мы говорим, что EF должен взять оригинальное, а не измененное значение поля Id и передать его в функцию UpdateCategory. Почему так, я думаю, вы сами догадались.

Остается добавить, что «Result Column Bindings» есть и для функции Update. В нашем случае он не используется. Однако можно представить себе ситуацию, когда хранимая процедура не только сохраняет переданные в параметрах значения в БД, но руководствуясь какой-то своей логикой, еще и изменяет их. В этом случае новые значения можно вернуть через «Result Column Binding». Это, конечно, кошмарный сценарий, но в жизни бывает всякое.

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



Избавиться от него не получится. EF для выборок будет использовать только его. Единственное, что можно сделать, это заменить таблицу на view.

Ассоциация «многие ко многим»



Все мы знаем, что объектная ассоциация многие ко многим в реляционных структурах реализуется посредством промежуточной таблицы и двух ассоциаций «один ко многим». Вот я и решил посмотреть, как с этой задачей справится EF. Для этого я сделал вот такую БД с клиническим классическим примером «студенты – курсы»:



После работы визарда получилась вот такая EDM модель:



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

Заключение



Не все, конечно, но основные сценарии маппинга я рассмотрел. Из увиденного можно сделать вывод, что EF позволяет создавать развитые объектные модели и эффективно маппировать их на реляционные структуры данных. Поддерживаются различные режимы организации наследования классов и представления ассоциаций.

Среди обнаруженных недостатков можно выделить следующие:

  • при маппинге столбцов необходимо устанавливать соответствие свойству и нельзя вместо свойства использовать константу

  • нет способа запретить создание навигационных свойств на одном из концов ассоциации.

  • нет возможности маппировать класс только на хранимые процедуры без использования таблиц и view.

  • не все возможности маппинга поддерживаются дизайнером EDM.

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

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

many to many многие могут, а вот мне интереснее двухсторонняя one-to-many/many-to-one с числовым индекстом там, где оно список.

Просто я с hibernate реально сейчас работаю, и там с реализацией этого вылезло множество проблем мировоззренческого плана.

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

to Ilyak
> мне интереснее двухсторонняя one-to-many/many-to-one с числовым индекстом там, где оно список.

Там где оно список, у класса будет свойство типа EntityCollection<T>. У самой коллекции индексера нет, но она реализует интерфейс IListSource у которого есть метод GetList(), который пакует всех членов коллекции в IList. Вот тут уже есть индексер.

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

Индексер этого списка - поле в таблице?
А изменения порядка элементов в этом списке синхронизируются с таблицей?

Да, еще такой вопрос: как реализовать в EF древовидную систему:

в терминах Java:
class TreeEntity
{
int id;
// Родитель
TreeEntity parent;
// С работающим индексом, соблюдая заданный порядок!
List<TreeEntity> children;
// Список всех предков, отсортированный, то есть первый элемент = parent, последний - корень.
List<TreeEntity> ascendants;
// Набор всех-всех потомков.
Set<TreeEntity> descendants;
}

В hibernate я это реализовал с некоторым количеством зубовного скрежета и одной хранимой процедурой :)

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

to Ilyak
> Индексер этого списка - поле в таблице?
В C# индексер это специальное свойство, позволяющее импользовать синтаксис массива при обращении к элементам коллекции. Например:
Person p = department.persons[0];
То что в квадратных скобках и есть индексер.
По поводу сортированных коллекций.
В C# 3.0 появились новые возможности, и поэтому идеалогия решения подобных проблем отличается от Java.
Допустим у нас есть класс Department и в нем свойство-коллекция ссылок на людей Persons. Получение отсортированнго списка будет выглядеть примерно так:
IEnumerable<Person> ordered = department.Persons.OrderBy<Person>(p => p.LastName);
В заисимости от реализации метода OrderBy этот код выльется либо в запрос к БД либо в сотритовку в памяти.

Естественно ни о какой "синхронизации с таблицей" порядка сортировка речи не идет :)

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

Отсортировать коллекцию по произвольному признаку хоть на сервере, хоть на клиенте - невелика проблема. И в возможности получения списка меня интересовали вовсе не рюшечки типа persons[0].

Мне вот что интересно: можно ли и как в EF хранить *упорядоченный* список записей? Не по имени или дате отсортированный, а по порядку.

Грубо говоря:

class Parent
{
int id;
/* Активная сторона ассоциации */
List children;
}

class Child
{
int id;
Parent parent;
/* Инверсная сторона ассоциации */
int pos;
}

То есть, pos - колонка в таблице детей, но она является индексом списка.
И ORM должно гарантировать, что я смогу безгеморройно делать children.add(3, new Child()), и при этом новый ребенок во всех сессиях будет в списке именно четвертым, и pos-индексы всех затронутых детей будут соответственно сдвинуты.

Ну и про дерево тоже интересно.

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

to Ilyak
> И ORM должно гарантировать, что я смогу безгеморройно делать children.add(3, new Child())

Это частная прикладная задача. Я не вижу никаких проблем, в том чтобы реализовать такое поведение.

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

ORM само по себе частная прикладная задача, я не вижу никаких проблем написать его при необходимости с нуля своё. Благо, невиндовая культура поощряет развитие собственного окружения, а не переодическую покупку нового пакета.

Вопрос же в том, что уже реализовано и предоставляется, как сервис. Мне интересно - как с помощью рекламируемой EF сделать такой List, с индексом, хранящимся в БД же? Можешь привести требуемые пассы? Или придется-таки писать это руками следить за индексами самостоятельно, обновлять их при вставке, сдвигать при удалении?

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

to Ilyak
1.Плохо, что вы не отличаете частных прикладных задач от типовых инфраструктурных для которых создаются различные Framework-и.
2. ORM можно сделать самому. Я лично, занимался этим не менее четырех раз. Но при наличии готовой в составе платформы готовой библиотеки, я конечно воспользуюсь ей. Мне не известны "культуры" которые поощряли бы изобретение велосипедов при наличии готовых решений. Open Source разработка также ориентирована на использование готовых блоков и библиотек при создании решений, причем даже в большей степени, чем разработка проприетарных решений.
3. Частные прикладные задачи, решаются путем написания прикладного кода, а не при помощи "пассов". Ваша задача относится именно к этой категории, и я не вижу препядствий для ее решения в EF. Она решается на раз, созданием наследника от EntityCollection, который умеет обновлять индексное поле своих элементов при втавке и удалении (или оповещать их об этих событиях). Можно реализовать эту логику и в хранимой процедуре и замапить эту ХП на update класса элемента списка. EF умеет принимать из такой ХП изменившиеся (вычисленные) значения полей. Вот уже два варианта решения. И без "зубовного скрежета".

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

1. Нормально я их отличаю.
3. Только вот проблема в том, что если для EF задача создания list-indexа является "частной прикладной", то в Hibernate, например, индексы поддерживаются исходно, и для их реализации не надо писать ни сточки кода.
См. http://www.hibernate.org/hib_docs/v3/reference/en/html/collections.html#collections-indexed

Получается, что самые базовые возможности Hibernate - это "частная прикладная задача" для EF. К третьей версии нагоните, к пятой - перегоните? :)

Hibernate есть ведь и под .Net, кстати. Хотелось бы почитать сравнение между этими двумя ORM.

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

to Ilyak
> Получается, что самые базовые возможности Hibernate - это "частная прикладная задача" для EF. К третьей версии нагоните, к пятой - перегоните? :)

Ну, ну. "Я Пастернака не читал, но осуждаю..."

Кстати, по языковым возможностям C# уже давно обогнал Java.

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

А какое это имеет отношение к обсуждаемому вопросу? Hibernate есть и под Java, и под .Net. EF есть только под .Net и не умеет list-index. Зато маппинги можно рисовать мышкой, правда, при этом половина возможностей пока недоступна. Правда, это не отменяет того факта, что Hibernate кривое говно, однако EF пока вообще vaporware.

А вот сравнение двух этих ORM и правда хотелось бы почитать.

Yury Skaletskiy комментирует...

Сергей -- комментарий по поводу маппинга на хранимые процедуры. Не все так плохо :)

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

1) Создать function import, и из entity container станет доступна функция-фраппер, возвращающая коллекцию нужных нам сущностей. Главное, чтобы хранимая процедура возвращала все нужные нам колонки.

2) Подправить в .EDMS значение EntitySet/DefiningQuery так, чтобы данные брались из User Defined Function. Например, так:

SELECT
[Extent1].[TimeStamp] AS[TimeStamp],
[Extent1].[Message] AS [Message]
FROM FGetEventLog() AS [Extent1]

Таким образом, реализовать паттерн, когда приложения не имеют вообще доступа на таблицы, в принципе, возможно.

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

to Yury Skaletskiy
Да, можно. Но подгрузка по навигационным свойствам (Include()) работать в этом случае не будет. Поэтому ИМХО игра не стоит свеч.