понедельник, 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 определяется только для базовой сущности).

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

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

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

Ждем продолжение!

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

Четвертая статья уже давно написана. А если Вы о пятой, то она появится в течение двух недель.

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

Я после использование всем ORM систем понял, что они значительно разгуржают сервер и он практически не трудиться. Хотесь бы видеть какое-то универсальное стредство от MS, которое позволяло бы использовать преимущетсво РБД, но получать данные в объектном виде. Я вот все равно не пойму, у меня есть скажем уже рабочае база данных - так часто в реальной жизни случается, и многое не разрабатываеться с нуля, и я бы могу результаты запроса привезти в объектной модели и вот здесь начинается ручная свистопляска... и никак, вызывает DataReader приобразовываешь результаты в объекты и потом с ними работаешь, не совсем удобно...

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

>>Я после использование всем ORM систем понял

Всех-всех-всех? :)

>>они значительно разгуржают сервер и он практически не трудиться

Сервер СУБД трудится ровно столько же. И вообще не вижу связи между ORM и нагруженностью сервера.

>>и я бы могу результаты запроса привезти в объектной модели и вот здесь начинается ручная свистопляска... и никак, вызывает DataReader

Зачем DataReader? Сущности замапили - и вперед. DataReader, если и нужен, то для очень специфичных вещей.

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

>>Зачем DataReader? Сущности замапили - и вперед. DataReader, если и нужен, то для очень специфичных вещей.

Ну почему - давай-те представим ситуаций, к которой структура БД уже пределеная и использование динамических запросов запрещенно - типичная систуация для финансовых приложение.

Т.е. у нас есть хранимая процедура, скажем для вашего примера, которая возвращает Имя пользователя и его адресс один result-set. Как в данном случае можно воспользоваться EF или объектами ORM? Создать отдельный элемент для каждого result-set вроде нет смысла.

Самый оптимальный вариант, это делать мапинг - при помощи того же самого DataReader - на типичный объекты ORM? В таком случае, где выгодность ORM.

Второй вопрос, как перенести сложную логику выборки данных в SQL-сервер? Это тоже не всегда возможно.

Для задач типа Persons --> Address это легко :))) А возьмите корпоративный БД, в которой 700 таблиц и милионы запросов в секунду. С динамически генеруемым SQL он просто загнется.

Еще вариант, как указать на определенной использование индекса - HINT. Никак..

и т.д.

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

Ааа, вот вы о чем.

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

А высокая производительность хранимых процедур по сравнению с динамическими запросами - это миф. В современных РСУБД план выпонения динамических запросов кэшируется практически так же хорошо, как и хранимок.

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

Хорошо написано, спасибо.

Для пущего счастья стоит добавить ссылки на все другие части во все эти части (надеюць, что понятно выразился :).

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

Очень хочется 5-ю статью, которая должна была выйти в течение 2-х недель в 2009 году :) Она есть?

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

Я ждал этого комментария больше года :) Да, цикл заглох, несмотря на мое обещание написать продолжение.

Хотя заглох он на 7-й статье, а не на пятой. И почему вы пишете комментарий к 3-й статье? :)

Кроме того, я еще немного писал о EF уже вне рамок цикла. Все статьи, касающиеся EF, можно просмотреть по соответствующему тегу: http://alexidsa.blogspot.com/search/label/Entity%20Framework