Entity Framework уже больше года блуждает призраком по просторам интернета. Несколько CTP-шек, несколько Beta-версий, абстрактные разговоры о его безграничных возможностях и куча примеров в стиле Hello World, которыми автора, наверное, хотят продемонстрировать нам всю мощь LINQ to Entities или удивить своими обширными познаниями. Я поигрался несколько дней и далее вы прочитаете мои открытия и впечатления.
Что такое Entity Framework
Маркетологи Microsoft уверяют нас, простых смертных программистов, что Entity Framework - начало нового века, что it's the first step in a much larger vision of an entity-aware data platform
. А люди, которые на этом собаку съели, не верят. И я не верю. Entity Framework в текущем виде представляет собой не что иное, как OR/Mapper, причем слабенький. С другой стороны, учитывая политику Microsoft, Entity Framework будет развиваться и обрастать. Уже заявлено начало работ над 2-й версией. Вероятнее всего, дальнейшая архитектура data-driven приложений и фреймворков будет строиться именно на Entity Framework.
Бороздя просторы интернета, я наткнулся на ADO.NET Entity Framework Vote of No Confidence - петиция к Microsoft по поводу проблем Entity Framework. Советую почитать, если есть время. Забавно просто очень - целая петиция.
Состав Entity Framework
Entity Framework состоит из таких основных компонентов:
- Дизайнер Visual Studio 2008 SP1
- Кодогенератор, входит в .NET Framework 3.5 SP1
- Библиотеки поддержки, входит в .NET Framework 3.5 SP1
Рекомендую сразу взлгянуть на раздел (я этого не делал и, кстати, жалею - там много полезного написано для начального изучения) MSDN --> MSDN Library --> Development Tools and Languages --> Visual Studio 2008 --> Visual Studio --> .NET Framework Programming in Visual Studio --> Accessing Data --> ADO.NET --> Entity Data Model Tools
.
Visual Studio 2008 Entity Framework Visual Designer
Visual Studio 2008 SP1 позволяет визуально редактировать модель Entity Framework, которая состоит из 3-х частей:
- Conceptual Schema - CSDL-файл, описывающий сущности и отношения между ними.
- Storage Metadata Schema - SSDL-файл, описывающий схему базы данных.
- Mapping Specification - MSL-файл, связывающий сущности и способ их хранения в реляционной БД.
Все эти файлы представляют собой обычный XML, что позволяет в будущем неограниченно расширять формат, добавляя новые функции и расширения.
Информация, заложенная в этих файлах, называется Entity Data Model, или сокращенно - EDM.
Я сейчас добавлю в проект пустую модель из шаблона ADO.NET Entity Data Model и назову ее TestModel
:
Мастер Entity Data Model предлагает нам создать пустую модель или сгенерировать ее по существующей базе данных:
Мы выберем Empty model
и я уточню, что никакой генерации, фактически, не происходит. Дизайнер просто сразу связывает модель со схемой конкретной базы данных. Это действие можно выполнить в любой момент после. В результате работы мастера в проекте появляется файл TestModel.edmx
. Это обычный XML, в котором хранятся CSDL, SSDL, MSL и вспомогательный контент самого дизайнера Entity Framework.
Элементы дизайнера Entity Framework
-
Model Browser - показывает нам списки сущностей, ассоциаций между ними, наборы сущностей и схему БД в упрощенном виде. Окошко не позволяет производить никаких изменяющих схему манипуляций с объектами.
-
Mapping Details - позволяет связать сущность с физической схемой БД.
Окошко имеет две закладки
(названия, полагаю, говорят сами за себя):
- Map Entity to Tables / Views
Связывание сущности с одной или несколькими таблицами БД.
- Map Entity to Functions
Связывание операций создания, изменения и удаления сущности с процедурным кодом БД.
Mapping Details используется не только для сущностей, но и для ассоциаций между ними.
-
Полотно
концептуального дизайнера.
Полотно отображает сущности вместе с их данными и навигационными свойствами, отношения между сущностями - наследование или ассоциации с кардинальным числом.
- Окна Toolbox и Properties.
Начинаем работать с дизайнером - концептуальная модель (CSDL)
Уверен, читатель сможет без труда создать подобие схемки на картинке выше и убедиться (или просто поверить), что
- Есть возможность указать сущность, как абстрактную.
- Есть поддержка композитных первичных ключей.
- Есть возможность документирования - Long Description, Summary для всех элементов концептуальной модели.
- Есть поддержка оптимистического параллелизма (optimistic concurrency) - Concurrency Mode = Fixed (единственный режим).
- Поддерживаются только простые типы данных -
Binary
, Boolean
, Byte
, DateTime
, DateTimeOffset
, Decimal
, Double
, Guid
, Int16
, Int32
, Int64
, SByte
, Single
, String
, Time
и их Nullable вариации.
CSDL и MSL, кстати, поддерживают ComplexType
. Можно, например, сгруппировать несколько атрибутов сущности в отдельный тип. Но текущая версия дизайнера это не поддерживает. Она, вообще, мало чего поддерживает.
- Есть поддержка ассоциаций с кардинаностями [0..1 - *], [1 - 0..1], [1 - 1], [1 - *], [* - *].
- Невозможно скрыть (не генерировать в коде) одну из сторон ассоциации (например, у справочника).
- Нет поддержки отложенной загрузки (Lazy Loading).
- Нет поддержки свойств-перечислений.
- Нельзя менять порядок свойств в сущностях.
- Алгоритм расположения сущностей на полотне единственный, случайный и в результате неудобный.
- Валидация модели - это проверка edmx-файла на корректность. Поэтому сообщения об ошибке не говорят почти ничего о ситуации на экране.
- Дизайнер не отображает абстрактность класса.
- Возможности Mapping Details по фильтрации очень ограниченные - поддерживаются только операторы
=
, IS NULL
, IS NOT NULL
, на колонку можно наложить лишь одно условие, все условия соединяются логическим оператором AND
.
- Visual Studio 2008 SP1 иногда банально перестает открывать edmx-файл дизайнером.
Далее выдержка из MSDN. The following are Entity Framework features that are not currently supported by the Entity Designer:
Спускаемся с небес на землю - физическая модель (SSDL)
Первое, что необходимо сделать - подключить базу данных к модели Entity Framework - пункт меню Update Model from Database... вызывает Update Model Wizard:
Далее нам предлагают внести изменения в нашу модель:
Обращаю внимание, что основная задача этого мастера - синхронизация физической модели БД со схемой SSDL. При добавлении таблицы мастер создаст одноименную сущность, наполнив ее нужными атрибутами, и автоматически выполнит связывание с таблицей (mapping), но этот эффект единичный - после удаления сущности из концептуальной модели вернуть ее таким образом будет невозможно, придется создавать вручную! Я сначала думал, что это дефект дизайнера, но потом понял, что синхронизируется лишь SSDL схема. Кстати, удалить из SSDL ненужные объекты БД можно только тогда, когда они перестали существовать в самой БД.
Связывание концептуальной и физической моделей (MSL) - отображение (mapping)
Завершающая стадия проектирования модели Entity Framework - отображение концептуальной модели на физическую. Здесь нужно рассмотреть различные сценарии, которые поддерживаются текущей версией платформы и/или дизайнером.
Отображение сущности на таблицу
Эта процедура выполняется с помощью окна Mapping Details. Следующая картинка демонстрирует сущность ComplexObject
и ее отображение на одноименную таблицу БД:
Обратите внимание, что дизайнер позволяет наложить условия на поля таблицы, а также добавить еще одну или несколько таблиц. Это потребуется для следующих двух сценариев.
Vertical Entity splitting
Entity Framework позволяет вертикально отобразить несколько таблиц на одну сущность. Таблицы для такого сценария должны быть физически связаны отношением [1 - 1], т.е. первичный ключ дочерней таблицы должен быть одновременно внешним ключом к основной. Entity Framework требует, чтобы соединение таблиц происходило только через первичные ключи. Следующая картинка демонстрирует сущность ComplexObject
и ее отображение на две таблицы БД - ComplexObject
и ComplexObjectEx
:
Horizontal Entity splitting
Entity Framework позволяет горизонтально отобразить несколько таблиц на одну сущность. Таблицы для такого сценария должны обладать одинаковым набором колонок, которые будут отображены на сущность. Насколько я понимаю, дизайнер такую схему не поддерживает, да и сам Entity Framework не в ажуре с ней - сущность должна обладать булевым свойством, по которому она будет отнесена к одной из двух физических таблиц. В будущем эта функциональность может быть расширена. А зачем, если современные БД поддерживают это на уровне физической модели?
Вариант настройки вручную подробно описан в статье Entity Framework Horizontal Entity splitting.
Отображение ассоциаций [0..1 - *], [1 - 0..1], [1 - 1], [1 - *], [* - *], self association
Рассмотрим три основных типа ассоциаций между сущностями - [1 - 1], [1 - *] и [* - *]. Варианты [0..1 - ?] концептуально ничем не отличаются от своих собратьев, просто внешние ключи допускают значения NULL, а свойства сущностей, соответственно, - null. Self association - тот же [0..1 - *].
Ниже представлены ER-диаграмма и концептуальная схема, которые я буду использовать:
Чтобы связать две сущности, необходимо выполнить следующие действия:
- Создать в концептуальной модели ассоциацию между сущностями.
- В свойствах этой ассоциации выставить кардинальность сторон.
- Убедиться, что никакие внешние ключи (foreign key), используемые для соединения таблиц, не отображены на атрибуты сущностей.
- Выбрать ассоциацию в дизайнере и перейти в окно Mapping Details.
- Нажать Add a Table or View и выбрать из списка таблицу, которая содержит внешний ключ, используемый для соединения таблиц.
Это, как правило, будет таблица с кардинальностью выше, чем у таблицы другой стороны ассоциации.
- Сопоставить ключи (первичные и вторичные) с атрибутами сущностей.
С этим внимательно - дизайнер сам сопоставляет одинаковые имена. В результате отображение сконфигурировано некорректно:
А правильно вот так:
Association One To One [1 - 1]
Association One To Many [1 - *]
Association Many To Many [* - *]
Здесь немного интереснее. Я писал, что для отображения ассоциации нужно выбрать из списка таблицу, которая содержит внешний ключ, используемый для соединения таблиц на концах ассоциации. Но ни одна из этих таблиц не содержит такого ключа! Что же делать? Да выбрать таблицу, которая связывает одно с другим - промежуточную таблицу!
Здесь есть один нюанс. Дизайнер генерирует ассоциацию [* - *] только в случае, когда связующая таблица содержит две колонки - два внешних ключа, одновременно образующих первичный ключ этой же таблицы.
Наследование, абстрактные классы, иерархии
Объектно ориентированные языки программирования опираются на 3 основные парадигмы - наследование, полиморфизм и инкапсуляцию. Во многих языках полиморфизм реализуется через наследование. Классы, их отношения, а также их иерархия образуют концептуальную модель с одной стороны, а таблицы базы данных, на которые отображаются эти классы, - с другой. Появляется проблема - реляционная модель не имеет никакого понятия о наследовании. Поэтому, для отображения иерархий классов на реляционную модель применяют 3 основных схемы:
- Table per Hierarchy (TPH)
- Table per Type (TPT)
- Table per Concrete Type (TPC)
Отображение абстрактных классов
Дизайнер Entity Framework позволяет пометить сущность, как абстрактную. Соответственно, в коде генерируется абстрактный класс. Схемы, перечисленные выше, влияют на отображение абстрактного класса на таблицы БД (об этом будет указано в описании каждой их схем). Далее я использую термин класс
вместо сущность
.
В случае, когда абстрактный класс не имеет неабстрактных наследников, - библиотека времени выполнения выбросит исключение.
Table per Hierarchy (TPH)
В TPH все классы иерархии отображены на одну таблицу БД, которая должна содержать все атрибуты всех классов иерархии. Экземпляр класса сопоставляется с одной строчкой таблицы, которая содержит значение NULL в колонках, для которых нет соответствующих атрибутов в классе. Также необходимо иметь колонку-идентификатор, значения которой будут идентифицировать конечный тип иерархии. При отображении каждой сущности иерархии необходимо накладывать на эту колонку условие. Условия должны удовлетворять таким требованиям:
- Они должно быть взаимоисключающим.
- Условия должны покрывать все строки таблицы.
- Колонка-идентификатор не должна быть отображена на свойство сущности.
Пример концептуальной модели для схемы Table per Hierarchy (TPH)
Модель нуждается в некоторых пояснениях, поскольку кое-что визуально определить нельзя. Классы Animal
, Mammal
и Reptile
- абстрактные.
Отображение концептуальной модели для схемы Table per Hierarchy (TPH)
Для создания таблицы я использую следующий SQL скрипт:
CREATE TABLE [TPHInheritance].[Animal]
(
-- Animal
[ID] INT PRIMARY KEY NOT NULL IDENTITY,
[ClassType] NVARCHAR(128) NOT NULL
CHECK ([ClassType] IN (N'Horse', N'Dog', N'Snake', N'Crocodile')),
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
-- Mammal
[BirthDate] DATETIME /*NOT*/ NULL,
-- Horse
[MaximumSpeed] DECIMAL(4,2) NULL,
-- Dog
[Breed] NVARCHAR(128) /*NOT*/ NULL,
-- Reptile
[Length] DECIMAL(5,2) /*NOT*/ NULL,
-- Snake
[IsAdder] BIT /*NOT*/ NULL,
-- Crocodile
[Family] NVARCHAR(128) NULL,
[Genus] NVARCHAR(128) NULL
)
Связав модель с БД, я отображаю все сущности иерархии на эту таблицу, попутно добавляя условия на колонку ClassType
всем неабстрактным классам:
Абстрактные классы по логике вещей в условии не нуждаются.
Итак, все условия соблюдены, но валидация модели проходит с ошибками:
Error 2078: The EntityType 'TPHInreritanceContainer.Mammal' is Abstract and can be mapped only using IsTypeOf.
Error 2078: The EntityType 'TPHInreritanceContainer.Reptile' is Abstract and can be mapped only using IsTypeOf.
Я сбит с толку - текущей информации вполне хватает для определения типа!
Выход есть - создать по дискриминирующей колонке для каждого разветвления наследования:
CREATE TABLE [TPHInheritance].[Animal]
(
-- Animal
[ID] INT PRIMARY KEY NOT NULL IDENTITY,
[ClassType] NVARCHAR(128) NULL
CHECK ([ClassType] IN (N'Mammal', N'Reptile')),
[MammalType] NVARCHAR(128) NULL
CHECK ([MammalType] IN (N'Horse', N'Dog')),
[ReptileType] NVARCHAR(128) NULL
CHECK ([ReptileType] IN (N'Snake', N'Crocodile')),
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
-- Mammal
[BirthDate] DATETIME /*NOT*/ NULL,
-- Horse
[MaximumSpeed] DECIMAL(4,2) NULL,
-- Dog
[Breed] NVARCHAR(128) /*NOT*/ NULL,
-- Reptile
[Length] DECIMAL(5,2) /*NOT*/ NULL,
-- Snake
[IsAdder] BIT /*NOT*/ NULL,
-- Crocodile
[Family] NVARCHAR(128) NULL,
[Genus] NVARCHAR(128) NULL
)
Условия на дискриминирующую колонку накладываются при отображении всех сущностей, кроме базовой:
Преимущества и недостатки схемы Table per Hierarchy (TPH)
Преимущества (или когда использовать TPH)
- Полиморфные запросы выполняются быстро.
Недостатки (или когда не использовать TPH)
- Много колонок.
- Сильная разреженность (преимущество значений NULL). Как следствие - чрезмерное использование памяти и дискового пространства.
- Все колонки небазовых классов должны быть объявлены, как позволяющие значение NULL. Тоесть страдает целостность данных.
Table per Type (TPT)
Здесь все просто - каждому типу сопоставляется одна таблица, а каждому неунаследованному свойству - одна колонка в этой таблице. Концептуальная модель - та же. Таблицы связаны отношением [1 - 0..1]:
CREATE TABLE [TPTInheritance].[Animal]
(
[ID] INT PRIMARY KEY NOT NULL IDENTITY,
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
)
CREATE TABLE [TPTInheritance].[Mammal]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Animal],
[BirthDate] DATETIME NOT NULL
)
CREATE TABLE [TPTInheritance].[Horse]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Mammal],
[MaximumSpeed] DECIMAL(4,2) NULL
)
CREATE TABLE [TPTInheritance].[Dog]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Mammal],
[Breed] NVARCHAR(128) NOT NULL
)
CREATE TABLE [TPTInheritance].[Reptile]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Animal],
[Length] DECIMAL(5,2) NOT NULL
)
CREATE TABLE [TPTInheritance].[Snake]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Reptile],
[IsAdder] BIT NOT NULL
)
CREATE TABLE [TPTInheritance].[Crocodile]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Reptile],
[Family] NVARCHAR(128) NULL,
[Genus] NVARCHAR(128) NULL
)
Преимущества и недостатки схемы Table per Type (TPT)
Преимущества (или когда использовать TPT)
- Когда недостатки TPH существенны.
- TPT является нормализированным путем хранения данных.
Недостатки (или когда не использовать TPT)
- Если иерархия сложная - производительность может страдать из-за большого количества соединений между таблицами.
Table per Concrete Type (TPC)
Думаю, из названия понятно, что каждый неабстрактный тип отображается на одну таблицу, которая содержит колонки для всех свойств этого типа - как собственных, так и унаследованных.
Здесь я буду использовать немного измененную концептуальную модель:
Для простоты эксперимента (напоминаю, что дизайнер не поддерживает TPC) лишь одна сущность будет соответствовать схеме TPC, остальные строятся по схеме TPT:
CREATE TABLE [TPCInheritance].[Animal]
(
[ID] INT PRIMARY KEY NOT NULL IDENTITY,
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
)
CREATE TABLE [TPCInheritance].[Mammal]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Animal],
[BirthDate] DATETIME NOT NULL
)
CREATE TABLE [TPCInheritance].[Horse]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Mammal],
[MaximumSpeed] DECIMAL(4,2) NULL
)
CREATE TABLE [TPCInheritance].[Dog]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Mammal],
[Breed] NVARCHAR(128) NOT NULL
)
CREATE TABLE [TPCInheritance].[BadDog]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Dog],
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
[BirthDate] DATETIME NOT NULL,
[Breed] NVARCHAR(128) NOT NULL,
[AngerLevel] TINYINT NOT NULL
)
CREATE TABLE [TPCInheritance].[Reptile]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Animal],
[Length] DECIMAL(5,2) NOT NULL
)
CREATE TABLE [TPCInheritance].[Snake]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Reptile],
[IsAdder] BIT NOT NULL
)
CREATE TABLE [TPCInheritance].[Crocodile]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Reptile],
[Family] NVARCHAR(128) NULL,
[Genus] NVARCHAR(128) NULL
)
Для начала выполняем шаги, указанные в разделе Table per Type (TPT). Далее необходимо открыть edmx-файл в XML-редакторе и внести добавки
:
<!-- C-S mapping content -->
<edmx:Mappings>
<Mapping xmlns="urn:schemas-microsoft-com:windows:storage:mapping:CS" Space="C-S">
<Alias Key="Model" Value="TPCInheritance" />
<Alias Key="Target" Value="TPCInheritanceContainer.Store" />
<EntityContainerMapping CdmEntityContainer="TPCInheritanceEntities" StorageEntityContainer="TPCInheritanceContainerStoreContainer">
<EntitySetMapping Name="Animal">
<EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.Animal)">
<MappingFragment StoreEntitySet="Animal">
<ScalarProperty Name="FoodClassification" ColumnName="FoodClassification" />
<ScalarProperty Name="Weight" ColumnName="Weight" />
<ScalarProperty Name="ID" ColumnName="ID" />
</MappingFragment>
</EntityTypeMapping>
<EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.Mammal)">
<MappingFragment StoreEntitySet="Mammal">
<ScalarProperty Name="ID" ColumnName="ID" />
<ScalarProperty Name="BirthDate" ColumnName="BirthDate" />
</MappingFragment>
</EntityTypeMapping>
<EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.Horse)">
<MappingFragment StoreEntitySet="Horse">
<ScalarProperty Name="ID" ColumnName="ID" />
<ScalarProperty Name="MaximumSpeed" ColumnName="MaximumSpeed" />
</MappingFragment>
</EntityTypeMapping>
<EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.Dog)">
<MappingFragment StoreEntitySet="Dog">
<ScalarProperty Name="ID" ColumnName="ID" />
<ScalarProperty Name="Breed" ColumnName="Breed" />
</MappingFragment>
</EntityTypeMapping>
<EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.BadDog)">
<MappingFragment StoreEntitySet="BadDog">
<ScalarProperty Name="ID" ColumnName="ID" />
<ScalarProperty Name="AngerLevel" ColumnName="AngerLevel" />
<!-- MAPPINGS BELOW ARE ADDED MANUALLY -->
<ScalarProperty Name="Breed" ColumnName="Breed" />
<ScalarProperty Name="BirthDate" ColumnName="BirthDate" />
<ScalarProperty Name="FoodClassification" ColumnName="FoodClassification" />
<ScalarProperty Name="Weight" ColumnName="Weight" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
</EntityContainerMapping>
</Mapping>
</edmx:Mappings>
Думаю, смысл изменений понятен.
Преимущества и недостатки схемы Table per Concrete Type (TPC)
Преимущества (или когда использовать TPC)
- Схема TPC отлично работает для неполиморфных запросов и ассоциаций.
Недостатки (или когда не использовать TPC)
- Простой полиморфный запрос для корневой сущности вовлечет множество запросов и соединений. Такой запрос будет ограничивать производительность очень сильно.
- Если корневая сущность ссылается на что-либо, то внешний ключ должен быть продублирован на все таблицы иерархии.
- Любое изменение в корневой сущности сказывается на все таблицы иерархии.
Кодогенератор Entity Framework
Как я уже писал, Entity Framework состоит из 3-х частей: дизайнер, кодогенератор и библиотеки поддержки. Результатом работы дизайнера является edmx-файл. При сборке проекта он расщепляется на 3 файла - csdl, msl и ssdl, которые в свою очередь зашиваются в сборку в виде ресурсов. Кроме того, csdl-файл используется для генерации кода, необходимого для работы с Object Services и LINQ to Entities (о них - далее).
Дизайнером Entity Framework пользоваться не обязательно. Никто не запрещает написать файлы csdl, msl и ssdl вручную, затем воспользовавшись утилитой EDM Generator (EdmGen.exe
) получить код. Естественно, с таким подходом build процедуру придется писать самому. С другой стороны, открываются все возможности Entity Framework, львиная часть которых не поддерживается версией 1.0 дизайнера.
У многих может возникнуть желание просто отредактировать edmx-файл вручную, полагая, что msbuild сделает все остальное сам. Это не так. Дело в том, что при сборке файл проверяется валидатором дизайнера и если в нем будет что-либо не соответствующее требованиям последнего - ошибка. Поэтому в текущей версии для использования Entity Framework без дизайнера придется немного пошевелить мозгами.
Если результат работы кодогенератора не удовлетворяет требованиям - не беда, можно написать свой! Не нужно пугаться, дело в том, что Entity Framework предоставляет разработчикам классы для работы с метаданными - пространство имен System.Data.Metadata.Edm
сборки System.Data.Entity.dll
. Этому вопросу я, пожалуй, посвящу отдельную статью.
Описывать результат генерации кода я не стану, замечу лишь, что
- все классы - парциальные (partial classes);
- присутствуют парциальные методы (partial methods), которые позволяют
внедрять
свой код в сгенерированный;
- код аннотирован атрибутами, в т.ч. отвечающими за сериализацию -
SerializableAttribute
, DataContractAttribute
, DataMemberAttribute
.
Библиотеки поддержки Entity Framework
Сгенерированный код сам по себе не содержит никакого функционала по взаимодействию с реляционной базой данных. Фактически, это та же концептуальная модель, просто написанная на языке программирования. На данный момент эта модель зависит от runtime-библиотеки Entity Framework (все сгенерированные классы имеют предка в этой библиотеке), но в будущем, во 2-й версии, нам обещают поддержку POCO (Plain Old C# Objects).
Библиотека поддержки Entity Framework состоит из единственной сборки - System.Data.Entity.dll
. Архитектуру Entity Framework можно представить в виде следующей диаграммы:
Скажу сразу, что часть Data Providers скрыта от программиста и не представляет никакой ценности в контексте использования Entity Framework. Фактически, Data Providers - это связующий элемент между Entity Client и базой данных.
Entity Client
Entity Framework породила нового поставщика данных ADO.NET - Entity Client. Он выглядит, как любой другой провайдер ADO.NET, с которыми разработчики сталкивались на протяжении всего существования .NET, но на самом деле стоит на ступеньку выше с точки зрения абстракции.
Entity Client позволяет выполнять запросы в контексте EDM с помощью стандартных объектов - соединение (EntityConnection), команда (EntityCommand) и DataReader (EntityDataReader). Почему в контексте EDM? Да потому, что EntityConnection привязан к конкретной EDM и выполняет запросы на языке Entity SQL (eSQL). Entity SQL позволяет эффективно и удобно запрашивать данные, описанные в EDM.
Когда Entity Client выполняет eSQL-запрос, производится его разбор и по метаданным EDM строится Canonical Command Tree (CCT) - структура данных, представляющая исходный запрос в объектной форме. Далее Client View Engine выполняет над CCT преобразование отображения концептуального уровня на уровень хранения, устраняя все нереляционные концепции. Полученный CCT передается для исполнения нижестоящему поставщику данных ADO.NET, который преобразует его в специфический для конкретной базы данных запрос. Насколько я понимаю, в версии 3.5 SP1 .NET Framework поставщих данных System.Data.SqlClient был расширен и поддерживает CCT.
При чтении данных Entity Client выполняет их преобразование, снова пользуясь метаданными EDM. Весь процесс лучше объяснить на примере:
const String csdl = "res://*/Associations.Associations.csdl";
const String ssdl = "res://*/Associations.Associations.ssdl";
const String msl = "res://*/Associations.Associations.msl";
const String providerConnectionString =
"Data Source=veastduo;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True";
var entityConnectionStringBuilder = new EntityConnectionStringBuilder
{
Metadata = csdl + "|" + ssdl + "|" + msl,
Provider = "System.Data.SqlClient",
ProviderConnectionString = providerConnectionString
};
using (var entityConnection = new EntityConnection(entityConnectionStringBuilder.ToString())) {
entityConnection.Open();
var entityCommand = entityConnection.CreateCommand();
const String eSQL =
"SELECT c, (SELECT DEREF(cs) FROM NAVIGATE(c, AssociationsModel.CourseCourseSchedule) AS cs)" +
" FROM [AssociationsEntities].[Course] AS c";
entityCommand.CommandText = eSQL;
var entityDataReader = entityCommand.ExecuteReader(CommandBehavior.SequentialAccess);
while (entityDataReader.Read()) {
var course = entityDataReader.GetDataRecord(0);
Trace.WriteLine(String.Format("Course: ID = {0}, Name = {1}", course["ID"], course["Name"]));
var courseSchedules = entityDataReader.GetDataReader(1);
while (courseSchedules.Read()) {
var schedule = ((IExtendedDataRecord) courseSchedules).GetDataRecord(0);
Trace.WriteLine(String.Format("\tSchedule: ID = {0}, BeginTime = {1}, EndTime = {2}, DateProvided = {3}",
schedule["ID"], schedule["DateProvided"], schedule["BeginTime"], schedule["EndTime"]));
}
}
}
-
Подключение
к поставщику данных.
В конструктор EntityConnection
необходимо передать строку подключения. Я, как опытный программист, использую для этого класс EntityConnectionStringBuilder
, экземпляру которого нужно установить следующие свойства:
- расположение EDM метаданных (файлов csdl, ssdl и msl);
- имя поставщика данных ADO.NET, специфического для используемой базы данных;
- строку подключения, специфическую для этого провайдера.
- Создание
EntityCommand
.
Язык запросов eSQL позволяет мне выбрать в первой колонке сущность Course
, а во второй колонке - список сущностей CourseSchedule
, которые ассоциированы с курсом. Обратите внимание, как для выборки CourseSchedule
используется оператор NAVIGATE
вместе с именем ассоциации [1 - *] между Course
и CourseSchedule
.
- Выполнение запроса.
При выполнении моего eSQL-запроса, Entity Client формирует Canonical Command Tree, а нижестоящий поставщих данных ADO.NET преобразует его в такой SQL:
SELECT
[Project2].[ID] AS [ID],
[Project2].[Name] AS [Name],
[Project2].[C1] AS [C1],
[Project2].[C2] AS [C2],
[Project2].[ID1] AS [ID1],
[Project2].[DateProvided] AS [DateProvided],
[Project2].[BeginTime] AS [BeginTime],
[Project2].[EndTime] AS [EndTime]
FROM ( SELECT
[Extent1].[ID] AS [ID],
[Extent1].[Name] AS [Name],
1 AS [C1],
[Project1].[ID] AS [ID1],
[Project1].[DateProvided] AS [DateProvided],
[Project1].[BeginTime] AS [BeginTime],
[Project1].[EndTime] AS [EndTime],
[Project1].[C1] AS [C2]
FROM [Associations].[Course] AS [Extent1]
LEFT OUTER JOIN (SELECT
[Extent2].[ID] AS [ID],
[Extent2].[CourseID] AS [CourseID],
[Extent2].[DateProvided] AS [DateProvided],
[Extent2].[BeginTime] AS [BeginTime],
[Extent2].[EndTime] AS [EndTime],
1 AS [C1]
FROM [Associations].[CourseSchedule] AS [Extent2] ) AS [Project1] ON [Extent1].[ID] = [Project1].[CourseID]
) AS [Project2]
ORDER BY [Project2].[ID] ASC, [Project2].[C2] ASC
Ни одна из существующих реляционных баз данных не позволяет выполнять запросы вида SELECT A, (SELECT B, C FROM DetailTable WHERE DetailTable.MasterTableID = MasterTable.ID) FROM MasterTable
, поэтому Entity Client вынужден создавать монстрообразные и не интуитивные SQL-запросы, которые позволяют получить всю нужную информацию в удобной форме для обратного преобразования, при выполнения которого используются дополнительные информационные колонки [C1]
и [C2]
.
PS: Oracle позволяет преобразовать вложенную таблицу в курсор и поместить его в ячейку выборки: SELECT A, CURSOR(SELECT B, C FROM DetailTable WHERE DetailTable.MasterTableID = MasterTable.ID) FROM MasterTable
. Но этот трюк нельзя использовать бесплатно - количество одновременно открытых курсоров ограничено.
Какая бы вложенность не требовалась - в результате будет один огромный SQL-запрос, который во многих случаях не блещет ни производительностью, ни количеством транспортируемых данных.
- Чтение результата.
Здесь все несколько сложнее, чем с обычными DataReader-ами. Дело в том, что в колонках могут храниться не только простые типы данных, но и другие объекты - структуры, списки и пр. В нашем случае в первой колонке находится IExtendedDataRecord
(представляющий данные сущности Course
), а во второй - IDataReader
(вложенный DataReader), в первой колонке которого хранится IExtendedDataRecord
(представляющий данные сущности CourseSchedule
).
Дополнительную информацию можно найти в статье How to Parse an EntityDataReader.
Entity SQL - достаточно сложный абстрактный язык. В то же время он не поддерживает SQL-аналоги операций INSERT, UPDATE, DELETE. Также невозможно использовать специфические для конкретной базы данных конструкции.
Entity Client - это низкоуровневое средство выполнения eSQL-запросов, которое позволяет читать данные в обобщенном виде. Entity Client не выполняет материализацию результата (преобразование его в объектную форму концептуальной модели). Этим занимается слой Object Services.
Object Services
Над поставщикой данных Entity Client существует следующий слой абстракций Entity Framework именуемый Object Services. Object Services позволяет работать не с обобщенными записями, которые возвращает Entity Client, а с объектами. Этот метод отходит от прямого взаимодействия с поставщиком Entity Client (хотя с этим поставщиком происходит скрытый обмен данными).
Основная задача Object Services - материализация результатов выполнения eSQL-запросов. Однако есть еще одна важная деталь - объект ObjectContext
, который представляет собой сессию взаимодействия приложения и хранилища данных.
Entity SQL не поддерживает операторы ЯМД (DML), поэтому ObjectContext
становится особенно важен - именно он выполняет операции по сохранению объектов.
Рассмотрим пример:
using (var associationsEntities = new ObjectContext("name=AssociationsEntities")) {
associationsEntities.DefaultContainerName = "AssociationsEntities";
var courses = associationsEntities.CreateQuery<Course>("[Course]");
foreach (var course in courses) {
Trace.WriteLine(String.Format("Course: ID = {0}, Name = {1}", course.ID, course.Name));
// Capitalize
course.Name = CapitalizedName(course);
}
const String newCourseName = "programming";
var beginTime = TimeSpan.FromHours(8.0d);
var duration = TimeSpan.FromHours(1.5d);
var newCourse = new Course {Name = newCourseName, CourseDescription = new CourseDescription {Data = newCourseName}};
newCourse.CourseSchedule.Add(new CourseSchedule
{BeginTime = beginTime, EndTime = (beginTime + duration), DateProvided = DateTime.Now});
associationsEntities.AddObject("Course", newCourse);
associationsEntities.SaveChanges();
var programmingCourse =
associationsEntities.CreateQuery<Course>("SELECT VALUE c FROM [Course] AS c WHERE c.[Name] = @courseName",
new ObjectParameter("courseName", "programming"));
foreach (var course in programmingCourse) {
Trace.WriteLine(String.Format("Course: ID = {0}, Name = {1}", course.ID, course.Name));
// Capitalize
course.Name = CapitalizedName(course);
}
associationsEntities.SaveChanges();
}
- Создание
ObjectContext
.
В конструктор ObjectContext
я передаю строку подключения, которая в текущем варианте просто указывает, что ее нужно взять из файла app.config
:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<add name="AssociationsEntities" connectionString="metadata=res://*/Associations.Associations.csdl|res://*/Associations.Associations.ssdl|res://*/Associations.Associations.msl;provider=System.Data.SqlClient;provider connection string="Data Source=veastduo;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />
</connectionStrings>
</configuration>
- Для удобства я устанавливаю свойство
DefaultContainerName
чтобы не указывать имя контейнера в именах сущностей или других объектов концептуальной модели.
- Далее с помощью метода
CreateQuery<Course>
я создаю объект типа ObjectQuery<Course>
.
В качестве eSQL-запроса я передаю просто [Course]
, а ObjectContext
сам расширяет его до вида SELECT VALUE c FROM [Course] AS c
. Запрос будет возвращать сущности Course
, а ObjectQuery<Course>
будет их материализировать.
- Далее в цикле происходит изменение полученных объектов.
- Созданный объект
Course
добавляется в контекст вызовом метода AddObject
.
- Сохранение контекста осуществляется вызовом меода
SaveChanges
.
При этом все изменения сохраняются в хранилище данных.
- Следующая часть кода демонстрирует, что
ObjectContext
работает с обычными eSQL-запросами, которые поддерживают параметры.
Важно понимать, что контекст отслеживает свои объекты. Т.е. если просто создать экземпляр класса Course
и сохранить контекст - ничего не произойдет. Нужно присоединить объект к контексту. В данном случае эту операцию выполняет метод AddObject
. Ничего не мешает отсоединить объект от контекста, метод Detach
.
Кодогенератор создает не только классы, отвечающие сущностям концептуальной модели, но и наследника ObjectContext
, который упрощает взаимодействие с моделью и LINQ to Entities:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.3053
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
[assembly: global::System.Data.Objects.DataClasses.EdmSchemaAttribute()]
[assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("AssociationsModel", "StudentCourse", "Course", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Associations.Course), "Student", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Associations.Student))]
[assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("AssociationsModel", "CourseCourseSchedule", "Course", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Associations.Course), "CourseSchedule", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Associations.CourseSchedule))]
[assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("AssociationsModel", "CourseDescriptionCourse", "CourseDescription", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Associations.CourseDescription), "Course", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Associations.Course))]
// Original file name:
// Generation date: 28.08.2008 22:26:49
namespace Associations
{
/// <summary>
/// There are no comments for AssociationsEntities in the schema.
/// </summary>
public partial class AssociationsEntities : global::System.Data.Objects.ObjectContext
{
/// <summary>
/// Initializes a new AssociationsEntities object using the connection string found in the 'AssociationsEntities' section of the application configuration file.
/// </summary>
public AssociationsEntities() :
base("name=AssociationsEntities", "AssociationsEntities")
{
this.OnContextCreated();
}
/// <summary>
/// Initialize a new AssociationsEntities object.
/// </summary>
public AssociationsEntities(string connectionString) :
base(connectionString, "AssociationsEntities")
{
this.OnContextCreated();
}
/// <summary>
/// Initialize a new AssociationsEntities object.
/// </summary>
public AssociationsEntities(global::System.Data.EntityClient.EntityConnection connection) :
base(connection, "AssociationsEntities")
{
this.OnContextCreated();
}
partial void OnContextCreated();
/// <summary>
/// There are no comments for Course in the schema.
/// </summary>
public global::System.Data.Objects.ObjectQuery<Course> Course
{
get
{
if ((this._Course == null))
{
this._Course = base.CreateQuery<Course>("[Course]");
}
return this._Course;
}
}
private global::System.Data.Objects.ObjectQuery<Course> _Course;
/// <summary>
/// There are no comments for Student in the schema.
/// </summary>
public global::System.Data.Objects.ObjectQuery<Student> Student
{
get
{
if ((this._Student == null))
{
this._Student = base.CreateQuery<Student>("[Student]");
}
return this._Student;
}
}
private global::System.Data.Objects.ObjectQuery<Student> _Student;
/// <summary>
/// There are no comments for CourseDescription in the schema.
/// </summary>
public global::System.Data.Objects.ObjectQuery<CourseDescription> CourseDescription
{
get
{
if ((this._CourseDescription == null))
{
this._CourseDescription = base.CreateQuery<CourseDescription>("[CourseDescription]");
}
return this._CourseDescription;
}
}
private global::System.Data.Objects.ObjectQuery<CourseDescription> _CourseDescription;
/// <summary>
/// There are no comments for CourseSchedule in the schema.
/// </summary>
public global::System.Data.Objects.ObjectQuery<CourseSchedule> CourseSchedule
{
get
{
if ((this._CourseSchedule == null))
{
this._CourseSchedule = base.CreateQuery<CourseSchedule>("[CourseSchedule]");
}
return this._CourseSchedule;
}
}
private global::System.Data.Objects.ObjectQuery<CourseSchedule> _CourseSchedule;
/// <summary>
/// There are no comments for Course in the schema.
/// </summary>
public void AddToCourse(Course course)
{
base.AddObject("Course", course);
}
/// <summary>
/// There are no comments for Student in the schema.
/// </summary>
public void AddToStudent(Student student)
{
base.AddObject("Student", student);
}
/// <summary>
/// There are no comments for CourseDescription in the schema.
/// </summary>
public void AddToCourseDescription(CourseDescription courseDescription)
{
base.AddObject("CourseDescription", courseDescription);
}
/// <summary>
/// There are no comments for CourseSchedule in the schema.
/// </summary>
public void AddToCourseSchedule(CourseSchedule courseSchedule)
{
base.AddObject("CourseSchedule", courseSchedule);
}
}
/// <summary>
/// There are no comments for AssociationsModel.Course in the schema.
/// </summary>
/// <KeyProperties>
/// ID
/// </KeyProperties>
[global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(NamespaceName="AssociationsModel", Name="Course")]
[global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
[global::System.Serializable()]
public partial class Course : global::System.Data.Objects.DataClasses.EntityObject
{
/// <summary>
/// Create a new Course object.
/// </summary>
/// <param name="id">Initial value of ID.</param>
/// <param name="name">Initial value of Name.</param>
public static Course CreateCourse(int id, string name)
{
Course course = new Course();
course.ID = id;
course.Name = name;
return course;
}
/// <summary>
/// There are no comments for Property ID in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public int ID
{
get
{
return this._ID;
}
set
{
this.OnIDChanging(value);
this.ReportPropertyChanging("ID");
this._ID = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("ID");
this.OnIDChanged();
}
}
private int _ID;
partial void OnIDChanging(int value);
partial void OnIDChanged();
/// <summary>
/// There are no comments for Property Name in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public string Name
{
get
{
return this._Name;
}
set
{
this.OnNameChanging(value);
this.ReportPropertyChanging("Name");
this._Name = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value, false);
this.ReportPropertyChanged("Name");
this.OnNameChanged();
}
}
private string _Name;
partial void OnNameChanging(string value);
partial void OnNameChanged();
/// <summary>
/// There are no comments for Students in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "StudentCourse", "Student")]
[global::System.Xml.Serialization.XmlIgnoreAttribute()]
[global::System.Xml.Serialization.SoapIgnoreAttribute()]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.Data.Objects.DataClasses.EntityCollection<Student> Students
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedCollection<Student>("AssociationsModel.StudentCourse", "Student");
}
set
{
if ((value != null))
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedCollection<Student>("AssociationsModel.StudentCourse", "Student", value);
}
}
}
/// <summary>
/// There are no comments for CourseSchedule in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "CourseCourseSchedule", "CourseSchedule")]
[global::System.Xml.Serialization.XmlIgnoreAttribute()]
[global::System.Xml.Serialization.SoapIgnoreAttribute()]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.Data.Objects.DataClasses.EntityCollection<CourseSchedule> CourseSchedule
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedCollection<CourseSchedule>("AssociationsModel.CourseCourseSchedule", "CourseSchedule");
}
set
{
if ((value != null))
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedCollection<CourseSchedule>("AssociationsModel.CourseCourseSchedule", "CourseSchedule", value);
}
}
}
/// <summary>
/// There are no comments for CourseDescription in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "CourseDescriptionCourse", "CourseDescription")]
[global::System.Xml.Serialization.XmlIgnoreAttribute()]
[global::System.Xml.Serialization.SoapIgnoreAttribute()]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public CourseDescription CourseDescription
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<CourseDescription>("AssociationsModel.CourseDescriptionCourse", "CourseDescription").Value;
}
set
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<CourseDescription>("AssociationsModel.CourseDescriptionCourse", "CourseDescription").Value = value;
}
}
/// <summary>
/// There are no comments for CourseDescription in the schema.
/// </summary>
[global::System.ComponentModel.BrowsableAttribute(false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.Data.Objects.DataClasses.EntityReference<CourseDescription> CourseDescriptionReference
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<CourseDescription>("AssociationsModel.CourseDescriptionCourse", "CourseDescription");
}
set
{
if ((value != null))
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedReference<CourseDescription>("AssociationsModel.CourseDescriptionCourse", "CourseDescription", value);
}
}
}
}
/// <summary>
/// There are no comments for AssociationsModel.Student in the schema.
/// </summary>
/// <KeyProperties>
/// ID
/// </KeyProperties>
[global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(NamespaceName="AssociationsModel", Name="Student")]
[global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
[global::System.Serializable()]
public partial class Student : global::System.Data.Objects.DataClasses.EntityObject
{
/// <summary>
/// Create a new Student object.
/// </summary>
/// <param name="id">Initial value of ID.</param>
/// <param name="name">Initial value of Name.</param>
public static Student CreateStudent(int id, string name)
{
Student student = new Student();
student.ID = id;
student.Name = name;
return student;
}
/// <summary>
/// There are no comments for Property ID in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public int ID
{
get
{
return this._ID;
}
set
{
this.OnIDChanging(value);
this.ReportPropertyChanging("ID");
this._ID = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("ID");
this.OnIDChanged();
}
}
private int _ID;
partial void OnIDChanging(int value);
partial void OnIDChanged();
/// <summary>
/// There are no comments for Property Name in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public string Name
{
get
{
return this._Name;
}
set
{
this.OnNameChanging(value);
this.ReportPropertyChanging("Name");
this._Name = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value, false);
this.ReportPropertyChanged("Name");
this.OnNameChanged();
}
}
private string _Name;
partial void OnNameChanging(string value);
partial void OnNameChanged();
/// <summary>
/// There are no comments for Course in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "StudentCourse", "Course")]
[global::System.Xml.Serialization.XmlIgnoreAttribute()]
[global::System.Xml.Serialization.SoapIgnoreAttribute()]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.Data.Objects.DataClasses.EntityCollection<Course> Course
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedCollection<Course>("AssociationsModel.StudentCourse", "Course");
}
set
{
if ((value != null))
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedCollection<Course>("AssociationsModel.StudentCourse", "Course", value);
}
}
}
}
/// <summary>
/// There are no comments for AssociationsModel.CourseDescription in the schema.
/// </summary>
/// <KeyProperties>
/// ID
/// </KeyProperties>
[global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(NamespaceName="AssociationsModel", Name="CourseDescription")]
[global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
[global::System.Serializable()]
public partial class CourseDescription : global::System.Data.Objects.DataClasses.EntityObject
{
/// <summary>
/// Create a new CourseDescription object.
/// </summary>
/// <param name="id">Initial value of ID.</param>
/// <param name="data">Initial value of Data.</param>
public static CourseDescription CreateCourseDescription(int id, string data)
{
CourseDescription courseDescription = new CourseDescription();
courseDescription.ID = id;
courseDescription.Data = data;
return courseDescription;
}
/// <summary>
/// There are no comments for Property ID in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public int ID
{
get
{
return this._ID;
}
set
{
this.OnIDChanging(value);
this.ReportPropertyChanging("ID");
this._ID = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("ID");
this.OnIDChanged();
}
}
private int _ID;
partial void OnIDChanging(int value);
partial void OnIDChanged();
/// <summary>
/// There are no comments for Property Data in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public string Data
{
get
{
return this._Data;
}
set
{
this.OnDataChanging(value);
this.ReportPropertyChanging("Data");
this._Data = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value, false);
this.ReportPropertyChanged("Data");
this.OnDataChanged();
}
}
private string _Data;
partial void OnDataChanging(string value);
partial void OnDataChanged();
/// <summary>
/// There are no comments for Course in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "CourseDescriptionCourse", "Course")]
[global::System.Xml.Serialization.XmlIgnoreAttribute()]
[global::System.Xml.Serialization.SoapIgnoreAttribute()]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public Course Course
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseDescriptionCourse", "Course").Value;
}
set
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseDescriptionCourse", "Course").Value = value;
}
}
/// <summary>
/// There are no comments for Course in the schema.
/// </summary>
[global::System.ComponentModel.BrowsableAttribute(false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.Data.Objects.DataClasses.EntityReference<Course> CourseReference
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseDescriptionCourse", "Course");
}
set
{
if ((value != null))
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedReference<Course>("AssociationsModel.CourseDescriptionCourse", "Course", value);
}
}
}
}
/// <summary>
/// There are no comments for AssociationsModel.CourseSchedule in the schema.
/// </summary>
/// <KeyProperties>
/// ID
/// </KeyProperties>
[global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(NamespaceName="AssociationsModel", Name="CourseSchedule")]
[global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
[global::System.Serializable()]
public partial class CourseSchedule : global::System.Data.Objects.DataClasses.EntityObject
{
/// <summary>
/// Create a new CourseSchedule object.
/// </summary>
/// <param name="id">Initial value of ID.</param>
/// <param name="dateProvided">Initial value of DateProvided.</param>
/// <param name="beginTime">Initial value of BeginTime.</param>
/// <param name="endTime">Initial value of EndTime.</param>
public static CourseSchedule CreateCourseSchedule(int id, global::System.DateTime dateProvided, global::System.TimeSpan beginTime, global::System.TimeSpan endTime)
{
CourseSchedule courseSchedule = new CourseSchedule();
courseSchedule.ID = id;
courseSchedule.DateProvided = dateProvided;
courseSchedule.BeginTime = beginTime;
courseSchedule.EndTime = endTime;
return courseSchedule;
}
/// <summary>
/// There are no comments for Property ID in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public int ID
{
get
{
return this._ID;
}
set
{
this.OnIDChanging(value);
this.ReportPropertyChanging("ID");
this._ID = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("ID");
this.OnIDChanged();
}
}
private int _ID;
partial void OnIDChanging(int value);
partial void OnIDChanged();
/// <summary>
/// There are no comments for Property DateProvided in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.DateTime DateProvided
{
get
{
return this._DateProvided;
}
set
{
this.OnDateProvidedChanging(value);
this.ReportPropertyChanging("DateProvided");
this._DateProvided = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("DateProvided");
this.OnDateProvidedChanged();
}
}
private global::System.DateTime _DateProvided;
partial void OnDateProvidedChanging(global::System.DateTime value);
partial void OnDateProvidedChanged();
/// <summary>
/// There are no comments for Property BeginTime in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.TimeSpan BeginTime
{
get
{
return this._BeginTime;
}
set
{
this.OnBeginTimeChanging(value);
this.ReportPropertyChanging("BeginTime");
this._BeginTime = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("BeginTime");
this.OnBeginTimeChanged();
}
}
private global::System.TimeSpan _BeginTime;
partial void OnBeginTimeChanging(global::System.TimeSpan value);
partial void OnBeginTimeChanged();
/// <summary>
/// There are no comments for Property EndTime in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.TimeSpan EndTime
{
get
{
return this._EndTime;
}
set
{
this.OnEndTimeChanging(value);
this.ReportPropertyChanging("EndTime");
this._EndTime = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("EndTime");
this.OnEndTimeChanged();
}
}
private global::System.TimeSpan _EndTime;
partial void OnEndTimeChanging(global::System.TimeSpan value);
partial void OnEndTimeChanged();
/// <summary>
/// There are no comments for Course in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "CourseCourseSchedule", "Course")]
[global::System.Xml.Serialization.XmlIgnoreAttribute()]
[global::System.Xml.Serialization.SoapIgnoreAttribute()]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public Course Course
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseCourseSchedule", "Course").Value;
}
set
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseCourseSchedule", "Course").Value = value;
}
}
/// <summary>
/// There are no comments for Course in the schema.
/// </summary>
[global::System.ComponentModel.BrowsableAttribute(false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public global::System.Data.Objects.DataClasses.EntityReference<Course> CourseReference
{
get
{
return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseCourseSchedule", "Course");
}
set
{
if ((value != null))
{
((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedReference<Course>("AssociationsModel.CourseCourseSchedule", "Course", value);
}
}
}
}
}
Явная и упреждающая загрузка
Entity Framework не поддерживает ленивую загрузку (Lazy Loading). Получение всех ассоциированных объектов выполняется двумя способами:
- Упреждающая загрузка (включение ассоциации в запрос).
ObjectQuery<T>
содержит метод Include
, которому можно передать список имен ассоциаций, разделенных точкой.
- Явная загрузка коллекций вызовом метода
Load
.
Пример:
using (var associationsEntities = new ObjectContext("name=AssociationsEntities")) {
associationsEntities.DefaultContainerName = "AssociationsEntities";
var courses = associationsEntities.CreateQuery<Course>("[Course]").Include("CourseDescription");
foreach (var course in courses) {
Trace.WriteLine(String.Format("Course: ID = {0}, Name = {1}", course.ID, course.Name));
Debug.Assert(course.CourseSchedule.Count == 0);
Debug.Assert(!course.CourseSchedule.IsLoaded);
Debug.Assert(course.CourseDescription != null);
course.CourseSchedule.Load();
Debug.Assert(course.CourseSchedule.IsLoaded);
foreach (var courseSchedule in course.CourseSchedule) {
Trace.WriteLine(String.Format("\tSchedule: ID = {0}, BeginTime = {1}, EndTime = {2}, DateProvided = {3}",
courseSchedule.ID, courseSchedule.DateProvided, courseSchedule.BeginTime,
courseSchedule.EndTime));
}
}
}
CourseDescription
загружается упреждающе, а коллекция CourseSchedule
- явно. Каждый вызов метода Load
провоцирует взаимодействие с базой данных, что может негативно сказаться на производительности. Как правило, лучше загрузить все за один раз.
LINQ to Entities
LINQ to Entities - последнее звено абстракций Entity Framework, которое работает поверх Object Services, а точнее - поверх ObjectQuery<T>
. LINQ-запрос трансформируется в Canonical Command Tree (CCT) и передается для исполнения в ObjectContext
.
Как и Entity SQL, LINQ не поддерживает операторы ЯМД (DML) напрямую. Модифицировать сущности в базе данных можно только с помощью служб Object Services (используя метод SaveChanges).
Запросы LINQ предполагают строгий контроль типов и возможность возвращать сущности и проекции. С другой стороны, Entity SQL позволяет выполнять более сложные запросы.
Если обобщить, то Entity SQL работает с любыми EDM и позволяет выполнять динамические запросы, а LINQ to Entities оперирует над статическим EDM (кодом) и выполняет статические запросы (которые представляют собой тоже код).
Entity Client VS Object Services VS LINQ to Entities
Ниже представлена сводная таблица, демонстрирующая важные характеристики Entity Client, Object Services и LINQ to Entities:
|
Entity Client и Entity SQL |
Службы Object Services |
LINQ to Entities |
Напрямую к поставщику Entity Client и Entity SQL |
Да |
Нет |
Нет |
Хорош для оперативных запросов |
Да |
Да |
Нет |
Может выдавать DML напрямую |
Нет |
Нет |
Нет |
Строго типизирован |
Нет |
Нет |
Да |
Может возвращать сущности как результат |
Нет |
Да |
Да |
Архитектура Entity Framework
Архитектура Entity Framework очень сильно напоминает EJB: eSQL = EJBQL, ObjectContext
= EntityManager
, ejb-jar.xml
= csdl.xml
+ssdl.xml
+msl.xml
. Где-то Entity Framework слабее EJB, где-то сильнее.
To be continued...