Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

The splendour and misery of Entity Framework designer, architecture and runtime

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 состоит из таких основных компонентов:

  1. Дизайнер Visual Studio 2008 SP1
  2. Кодогенератор, входит в .NET Framework 3.5 SP1
  3. Библиотеки поддержки, входит в .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-х частей:

  1. Conceptual Schema - CSDL-файл, описывающий сущности и отношения между ними.
  2. Storage Metadata Schema - SSDL-файл, описывающий схему базы данных.
  3. Mapping Specification - MSL-файл, связывающий сущности и способ их хранения в реляционной БД.

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

Информация, заложенная в этих файлах, называется Entity Data Model, или сокращенно - EDM.

Я сейчас добавлю в проект пустую модель из шаблона ADO.NET Entity Data Model и назову ее TestModel:
Add new ADO.NET Entity Data Model

Мастер Entity Data Model предлагает нам создать пустую модель или сгенерировать ее по существующей базе данных:
Entity Data Model Wizard
Мы выберем Empty model и я уточню, что никакой генерации, фактически, не происходит. Дизайнер просто сразу связывает модель со схемой конкретной базы данных. Это действие можно выполнить в любой момент после. В результате работы мастера в проекте появляется файл TestModel.edmx. Это обычный XML, в котором хранятся CSDL, SSDL, MSL и вспомогательный контент самого дизайнера Entity Framework.

Элементы дизайнера Entity Framework

  1. Model Browser - показывает нам списки сущностей, ассоциаций между ними, наборы сущностей и схему БД в упрощенном виде. Окошко не позволяет производить никаких изменяющих схему манипуляций с объектами.

    Entity Framework Model Browser

  2. Mapping Details - позволяет связать сущность с физической схемой БД.

    Entity Framework Mapping Details

    Окошко имеет две закладки (названия, полагаю, говорят сами за себя):

    1. Map Entity to Tables / Views

      Связывание сущности с одной или несколькими таблицами БД.

    2. Map Entity to Functions

      Связывание операций создания, изменения и удаления сущности с процедурным кодом БД.

    Mapping Details используется не только для сущностей, но и для ассоциаций между ними.

  3. Полотно концептуального дизайнера.

    Entity Framework edmx designer surface

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

  4. Окна 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:

  • Multiple entity sets per type.
  • Creating entity sets for non-root types.
  • Table-per-concrete class mapping.

    Не поддерживает, но и не ругается - покажу, как подкрутить.

  • Using EntityType properties in mapping conditions.
  • Editing storage model elements.
  • Unmapped abstract types. When you create an abstract entity type with the Entity Designer, the type must be mapped to a table or view.
  • Creating conditions on association mappings.
  • Mapping associations directly to stored procedures. Mapping many-to-many associations is not supported. You can indirectly map other associations to stored procedures along with entity types by mapping the appropriate navigation properties to stored procedure parameters.
  • Creating conditions on Function Import mappings.
  • Complex types.
  • Annotations.
  • QueryViews.
  • Specifying a parameter on an update function to return the number of rows affected. The Entity Designer does not expose a user interface for specifying this output parameter. However, you can manually edit the .edmx file so that the update function will handle this output parameter.
  • Models that contain references to other models.

Спускаемся с небес на землю - физическая модель (SSDL)

Первое, что необходимо сделать - подключить базу данных к модели Entity Framework - пункт меню Update Model from Database... вызывает Update Model Wizard:
Update Model from Database wizard
Далее нам предлагают внести изменения в нашу модель:
Choose your Database Objects
Обращаю внимание, что основная задача этого мастера - синхронизация физической модели БД со схемой SSDL. При добавлении таблицы мастер создаст одноименную сущность, наполнив ее нужными атрибутами, и автоматически выполнит связывание с таблицей (mapping), но этот эффект единичный - после удаления сущности из концептуальной модели вернуть ее таким образом будет невозможно, придется создавать вручную! Я сначала думал, что это дефект дизайнера, но потом понял, что синхронизируется лишь SSDL схема. Кстати, удалить из SSDL ненужные объекты БД можно только тогда, когда они перестали существовать в самой БД.

Связывание концептуальной и физической моделей (MSL) - отображение (mapping)

Завершающая стадия проектирования модели Entity Framework - отображение концептуальной модели на физическую. Здесь нужно рассмотреть различные сценарии, которые поддерживаются текущей версией платформы и/или дизайнером.

Отображение сущности на таблицу

Эта процедура выполняется с помощью окна Mapping Details. Следующая картинка демонстрирует сущность ComplexObject и ее отображение на одноименную таблицу БД:
Simple Entity mapping to one database table
Обратите внимание, что дизайнер позволяет наложить условия на поля таблицы, а также добавить еще одну или несколько таблиц. Это потребуется для следующих двух сценариев.

Vertical Entity splitting

Entity Framework позволяет вертикально отобразить несколько таблиц на одну сущность. Таблицы для такого сценария должны быть физически связаны отношением [1 - 1], т.е. первичный ключ дочерней таблицы должен быть одновременно внешним ключом к основной. Entity Framework требует, чтобы соединение таблиц происходило только через первичные ключи. Следующая картинка демонстрирует сущность ComplexObject и ее отображение на две таблицы БД - ComplexObject и ComplexObjectEx:
Vertical Splitting Entity mapping to two database tables

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-диаграмма и концептуальная схема, которые я буду использовать:
Associations database ER model
Associations conceptual model

Чтобы связать две сущности, необходимо выполнить следующие действия:

  1. Создать в концептуальной модели ассоциацию между сущностями.
  2. В свойствах этой ассоциации выставить кардинальность сторон.
  3. Убедиться, что никакие внешние ключи (foreign key), используемые для соединения таблиц, не отображены на атрибуты сущностей.
  4. Выбрать ассоциацию в дизайнере и перейти в окно Mapping Details.

    Association Mapping Details

  5. Нажать Add a Table or View и выбрать из списка таблицу, которая содержит внешний ключ, используемый для соединения таблиц.

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

  6. Сопоставить ключи (первичные и вторичные) с атрибутами сущностей.

    С этим внимательно - дизайнер сам сопоставляет одинаковые имена. В результате отображение сконфигурировано некорректно:
    Wrong association mapping
    А правильно вот так:
    Correct association mapping

Association One To One [1 - 1]

Association One To One

Association One To Many [1 - *]

Association One To Many

Association Many To Many [* - *]

Здесь немного интереснее. Я писал, что для отображения ассоциации нужно выбрать из списка таблицу, которая содержит внешний ключ, используемый для соединения таблиц на концах ассоциации. Но ни одна из этих таблиц не содержит такого ключа! Что же делать? Да выбрать таблицу, которая связывает одно с другим - промежуточную таблицу!
Association Many To Many

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

Наследование, абстрактные классы, иерархии

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

  1. Table per Hierarchy (TPH)
  2. Table per Type (TPT)
  3. Table per Concrete Type (TPC)

Отображение абстрактных классов

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

В случае, когда абстрактный класс не имеет неабстрактных наследников, - библиотека времени выполнения выбросит исключение.

Table per Hierarchy (TPH)

В TPH все классы иерархии отображены на одну таблицу БД, которая должна содержать все атрибуты всех классов иерархии. Экземпляр класса сопоставляется с одной строчкой таблицы, которая содержит значение NULL в колонках, для которых нет соответствующих атрибутов в классе. Также необходимо иметь колонку-идентификатор, значения которой будут идентифицировать конечный тип иерархии. При отображении каждой сущности иерархии необходимо накладывать на эту колонку условие. Условия должны удовлетворять таким требованиям:

  1. Они должно быть взаимоисключающим.
  2. Условия должны покрывать все строки таблицы.
  3. Колонка-идентификатор не должна быть отображена на свойство сущности.
Пример концептуальной модели для схемы Table per Hierarchy (TPH)

Table per Hierarchy (TPH) conceptual model

Модель нуждается в некоторых пояснениях, поскольку кое-что визуально определить нельзя. Классы 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 всем неабстрактным классам:
Table per Hierarchy (TPH) mapping non-abstract entity
Абстрактные классы по логике вещей в условии не нуждаются.

Итак, все условия соблюдены, но валидация модели проходит с ошибками:

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) mapping abstract entity
Table per Hierarchy (TPH) mapping non-abstract entity

Преимущества и недостатки схемы 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)

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

Здесь я буду использовать немного измененную концептуальную модель:
Table per Concrete Type (TPC) conceptual model

Для простоты эксперимента (напоминаю, что дизайнер не поддерживает 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. Этому вопросу я, пожалуй, посвящу отдельную статью.

Описывать результат генерации кода я не стану, замечу лишь, что

  1. все классы - парциальные (partial classes);
  2. присутствуют парциальные методы (partial methods), которые позволяют внедрять свой код в сгенерированный;
  3. код аннотирован атрибутами, в т.ч. отвечающими за сериализацию - SerializableAttribute, DataContractAttribute, DataMemberAttribute.

Библиотеки поддержки Entity Framework

Сгенерированный код сам по себе не содержит никакого функционала по взаимодействию с реляционной базой данных. Фактически, это та же концептуальная модель, просто написанная на языке программирования. На данный момент эта модель зависит от runtime-библиотеки Entity Framework (все сгенерированные классы имеют предка в этой библиотеке), но в будущем, во 2-й версии, нам обещают поддержку POCO (Plain Old C# Objects).

Библиотека поддержки Entity Framework состоит из единственной сборки - System.Data.Entity.dll. Архитектуру Entity Framework можно представить в виде следующей диаграммы:
Entity Framework runtime
Скажу сразу, что часть 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"]));
        }
    }
}
  1. Подключение к поставщику данных.

    В конструктор EntityConnection необходимо передать строку подключения. Я, как опытный программист, использую для этого класс EntityConnectionStringBuilder, экземпляру которого нужно установить следующие свойства:

    1. расположение EDM метаданных (файлов csdl, ssdl и msl);
    2. имя поставщика данных ADO.NET, специфического для используемой базы данных;
    3. строку подключения, специфическую для этого провайдера.
  2. Создание EntityCommand.

    Язык запросов eSQL позволяет мне выбрать в первой колонке сущность Course, а во второй колонке - список сущностей CourseSchedule, которые ассоциированы с курсом. Обратите внимание, как для выборки CourseSchedule используется оператор NAVIGATE вместе с именем ассоциации [1 - *] между Course и CourseSchedule.

  3. Выполнение запроса.

    При выполнении моего 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-запрос, который во многих случаях не блещет ни производительностью, ни количеством транспортируемых данных.

  4. Чтение результата.

    Здесь все несколько сложнее, чем с обычными 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();
}
  1. Создание 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=&quot;Data Source=veastduo;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />
        </connectionStrings>
    </configuration>
    
  2. Для удобства я устанавливаю свойство DefaultContainerName чтобы не указывать имя контейнера в именах сущностей или других объектов концептуальной модели.
  3. Далее с помощью метода CreateQuery<Course> я создаю объект типа ObjectQuery<Course>.

    В качестве eSQL-запроса я передаю просто [Course], а ObjectContext сам расширяет его до вида SELECT VALUE c FROM [Course] AS c. Запрос будет возвращать сущности Course, а ObjectQuery<Course> будет их материализировать.

  4. Далее в цикле происходит изменение полученных объектов.
  5. Созданный объект Course добавляется в контекст вызовом метода AddObject.
  6. Сохранение контекста осуществляется вызовом меода SaveChanges.

    При этом все изменения сохраняются в хранилище данных.

  7. Следующая часть кода демонстрирует, что 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). Получение всех ассоциированных объектов выполняется двумя способами:

  1. Упреждающая загрузка (включение ассоциации в запрос).

    ObjectQuery<T> содержит метод Include, которому можно передать список имен ассоциаций, разделенных точкой.

  2. Явная загрузка коллекций вызовом метода 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...

13 коммент.:

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

Молодец. Неплохое начало статьи.
Продолжение нужно.
Ну и в общем в начале неплохо бы указать, а зачем собственно Entity Framework нужен, почему и как без него раньше жили (да и живут).

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

+1 (про "зачем нужен"). Да и вообще, если у автора есть ясная картина, набросать примерные сценарии, где эта EF "рвёт как грелку" старые технологии. А то неохота из пушки по воробьям, да ещё за свои деньги. :)

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

>>> Нет поддержки свойств-перечислений.
А так пробовали указывать: global::MyProject.MyNamespace.MyEnum?

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

>>> все классы - парциальные (partial classes)
Зашибись, стоит ли это пониать так, что если у меня есть класс из другой сборки, к которой я не имею кода, то замапить его будет невозможно?

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

Спасибо за интересную статью!

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

Спасибо. Хорошая статья. Думаю много времени самостоятельного изучения многим людям экономить будет.
Хорошо бы ещё тему Entity Framework в связке в WCF затронуть в продолжении статьи.

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

отличная статья!! Молодца))))

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

Kruto!

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

>Чтобы связать две сущности, необходимо выполнить следующие действия:
Убедиться, что никакие внешние ключи (foreign key), используемые для соединения таблиц, не отображены на атрибуты сущностей.
------------------
Убираю отображение от внешнего ключа, потом делаю все как описано далее и получаю ошибку

"Property 'xxx' is not mapped."

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

ӏ dοn't know if it's just me or if everyboԁy else expeгіencing prοblemѕ with
youг sіtе. Іt looks like some of
the teхt within уour posts аre running off
the sсreen. Cаn sοmеbodу elsе рlease comment and lеt
mе knoω if this іs happеning tо them too?
Thiѕ may be а іssue with my broωѕer because I've had this happen before. Thanks

my homepage :: how to build a duck boat

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

Ηі outstаnding blog! Does running a blog liκe thіs take a lot of work?
I have viгtuallу nο еxpertіse in programming but I was hoping to ѕtаrt mу own blog in the near futuгe.

Anywaу, shoulԁ yοu have any idеаs
or tіρѕ for new blog owners please shаre.

I understаnd this is off subject neverthеlеss I simplу neеdeԁ to ask.
Mаnу thankѕ!

Check оut my web-ѕite :: birmingham is great
My web page - http://birminghamfun.tumblr.com/

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

Hеllo! Quick quеstion that's totally off topic. Do you know how to make your site mobile friendly? My blog looks weird when viewing from my iphone 4. I'm trying to
finԁ a temρlate οr plugin that might be able to fix thіs problem.
Ιf you havе anу rеcommendations,
ρleаse share. Manу thanks!

My blog :: penuis enlargement

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

Hello, i read youг blog oсcasiοnally and i οwn a similar
one and i wаs јust curiοus if you get а lot of spam remarks?
If so how do you гeduce іt, any plugin or anything уou сan suggeѕt?
I get so much latelу іt's driving me crazy so any assistance is very much appreciated.

my web-site ... guitar lessons for beginners

Отправить комментарий

Copyright 2007-2011 Chabster