Andrey on .NET | Entity Framework. Часть 2 – Основные принципы работы с EF

Entity Framework. Часть 2 – Основные принципы работы с EF

Теперь самое время посмотреть, каким образом можно взаимодействовать с Entity Framework. И вполне логично будет начать с вариантов создания Модели данных Entity.

Создание Модели данных Entity (EDM)

Существует несколько подходов создания EDM. Рассмотрим их подробнее.

База данных вначале (Database first)

Данный подход подразумевает, что в первую очередь проектируется и разрабатывается база данных. Это может быть сделано при помощи любых доступных разработчику инструментов. После этого на её основе Entity Framework создаст описание EDM и классы Концептуальной модели.

При таком варианте проектирования архитектуры приложения главная роль отводится структуре базы данных. Классы бизнес логики вынуждены адаптироваться под неё. Однако это позволяет максимально раскрыть потенциал используемой системы управления базами данных.

Создание новой EDMЧтобы использовать рассматриваемый подход в своем проекте, необходимо выбрать пункт "Add Item" в контекстном меню проекта и добавить описание Модели данных Entity (ADO.NET Entity Data Model).

В появившемся диалоге "Entity Data Model Wizard" нужно выбрать вариант "Generate from a database". После этого потребуется указать базу данных и параметры соединения с ней (выбрать или создать строку соединения). В результате в проект будет добавлен EDMX-файл, который содержит описание EDM в формате XML.

Вид Модели данных Entity (EDM) в дизайнереДля редактирования созданного описания используется специальный дизайнер. В качестве примера возьмем базу данных, которая может быть разработана для создаваемого демонстрационного веб-приложения. Вид созданной на её основе EDM представлен на рисунке.

Каждый представленный здесь класс в терминологии Entity Framework называется сущностью. Все их свойства разделены на две группы:

  1. Связанные напрямую с полями базы данных: Id, Title, и т. д.
  2. Навигационные: Language, Publisher, Tags.

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

Кроме того, стоит обратить внимание, что между сущностями указаны не только связи, но и их тип. При этом символ "звездочка" обозначает неограниченное число элементов (many). Так в приведенном примере есть следующие варианты:

  • *–1 – тип One-to-many (одна ко многим). Т.е. любой записи в таблице BookDetails соответствует одна запись в Language. И наоборот, одной записи в таблице Language может соответствовать любое число записей в BookDetails.
  • *–0..1 – также тип One-to-many (одна ко многим), но этом связь не обязательно должна существовать для каждой записи в таблице BookDetails.
  • *–* – этот вариант указывает на связь типа Many-to-many (многие ко многим). В частности любой книге (BookDetails) соответствует любое число ключевых слов (Tag), как и наоборот.

Модель вначале (Model first)

Следующий подход к разработке Модели данных Entity называется Модель вначале. Он является противоположностью предыдущего варианта. При этом изначально в дизайнере создается описание EDM, руководствуясь требованиями бизнес-логики. После чего на его основе генерируется база данных.

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

Добавление в проект описания EDM практически аналогично. Отличие только в диалоге "Entity Data Model Wizard", где необходимо выбрать пункт "Empty Model". После завершения работ по созданию Модели остается только сгенерировать базу данных. Для этого нужно выбрать пункт "Generate Database from Model" в контекстном меню дизайнера.

Код вначале (Code First)

В рассмотренных выше вариантах классы Модели получаются путем их автоматической генерации. А что если классы Модели уже есть? Или, например, удобнее написать их С# код, чем рисовать в дизайнере?

Начиная с версии 4.1 в Entity Framework еще один подход к разработке описания EDM – Код вначале. С его помощью можно создать базу данных на основе классов C# или Visual Basic. Причем для этого достаточно даже их самого простого варианта – POCO (Plain Old CLR Object).

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

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

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

Соответствие наследования таблицам в базе данных

Независимо от используемого способа создания EDM, можно столкнуться с задачей, которая в языках высокого уровня решается с использованием наследования. Но каким образом можно отразить это в базе данных? Существует три подхода для решения этой проблемы.

Используем пример, чтобы сделать дальнейшее описание более понятным. Предположим, что в Модели автомобильного каталога существует базовый класс Car и два его наследника CivilCar и SportCar.

Таблица для каждого конкретного типа (Table per concrete type или TPC)

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

Для указанных выше классов их будет три: Cars, CivilCars и SportCars. Стоит учесть, что в случае необходимости получить полный список автомобилей, в запросе будут использоваться все эти таблицы.

Таблица для каждого типа (Table per type или TPT)

В данном случае будет создано несколько таблиц:

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

Таким образом, в предложенном примере будет основная таблица Cars и две вспомогательные – CivilCars и SportCars. Подобное решение может быть выгодно, если большая часть запросов требует только информацию, соответствующую свойствам класса Car. Например, наиболее часто выводится полный список автомобилей, а уже по выбранным моделям отображаются подробные данные.

Таблица для иерархий (Table per hierarchy или TPH)

Последний подход позволяет создать одну общую таблицу для всех классов. Тип, к которому относится конкретная запись, в этом случае указывается в специальным поле. Здесь есть принципиальный момент: необходимо чтобы все свойства, добавленные в классах-наследниках, могли принимать значение null. Дело в том, именно оно будет указываться в полях, у которых нет аналогов в сохраняемом экземпляре.

В рассматриваемом примере будет создана таблица Cars, содержащая перечень всех свойств всех наследников класса Car.

Указание строки соединения

После создания EDM может потребоваться указание строки соединения для Entity Framework. Обратите внимание, что при использовании дизайнера она будет автоматически внесена в конфигурацию приложения. А вот в случае применения подхода Код вначале, её необходимо добавить самостоятельно.

Использование файла конфигурации (app.config или web.config)

Строка соединения Entity Framework выглядит несколько отличающейся от привычного варианта, используемого в ADO.NET. Однако понятна с первого взгляда:

<?xml version="1.0"?>
<configuration>
.........

  <connectionStrings>
    <add name="BookCatalogEntities" 
         connectionString="
            metadata=res://*/DBFirstCatalogModel.csdl|res://*/DBFirstCatalogModel.ssdl|
                     res://*/DBFirstCatalogModel.msl;
            provider=System.Data.SqlClient;
            provider connection string=&quot;
               data source=(local);
               initial catalog=BookCatalog;
               integrated security=True;
               multipleactiveresultsets=True;
               App=EntityFramework&quot;"
         providerName="System.Data.EntityClient" />
  </connectionStrings>

.........
</configuration>

В данном примере в файл web.config добавляется новая строка соединения с следующими параметрами:

  • name – имя строки соединения. В данном примере это BookCatalogEntities;
  • connectionString – параметры соединения для Entity Framework:
    • metadata – где расположены метаданные EDM. Три указанных файла отвечают за Контестную модель (csdl), Модель хранилища (ssdl) и Модель соотношения (msl).
    • provider – имя провайдера данных для Entity Framework. В данном случае это System.Data.SqlClient для работы с SQL Server или SQL Server Express.
    • provider connection string – параметры соединения. Содержимое этого параметра зависит от выбранного провайдера данных.
  • providerName – имя провайдера данных для приложения. Оно всегда равно System.Data.EntityClient.

При использовании подхода Код вначале отпадает необходимости в указании EDM. Поэтому строка приобретает более привычный вид:

<add name="CatalogContext" 
connectionString="Data Source=(local);Initial Catalog=BookCatalog;Integrated Security=true;"
providerName="System.Data.SqlClient" />

Указание строки соединения в коде приложения

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

Есть два способа программного указания строки соединения:

  • создать её как обычную строку, используя string и StringBuilder.
  • воспользоваться специальным классом EntityConnectionStringBuilder, который предоставляет следующие свойства:
    • Metadata – соответствует секции metadata в варианте рассмотренном выше;
    • Provider – указывает используемого провайдера данных;
    • ProviderConnectionString – настраивает параметры соединения провайдера.
    • ConnectionString – строка соединения. Свойство доступно только для чтения.

Все что остается – создать новый контекст данных ObjectContext с новой строкой соединения. В этом с случае также придется создать самостоятельно все необходимые объекты типа ObjectSet<T>. Цель указанных типов будет пояснена чуть ниже, а пока посмотрим на пример такого кода:

var newConnection = new EntityConnectionStringBuilder() {
    Metadata = @"res://*/DBFirstCatalogModel.csdl|res://*/DBFirstCatalogModel.ssdl|res://*/DBFirstCatalogModel.msl",
    Provider = @"System.Data.SqlClient",
    ProviderConnectionString = @"data source=(local);initial catalog=BookCatalog;integrated security=True;multipleactiveresultsets=True;App=EntityFramework"
};


using (var context = new ObjectContext(newConnection.ConnectionString)) {
    var catalog = context.CreateObjectSet<BookDetails>();
    var languages = context.CreateObjectSet<Language>();
    var publishers = context.CreateObjectSet<Publisher>();
    var tags = context.CreateObjectSet<Tag>();

    // Запросить данные ...
}

Выбираем язык запросов

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

  • Entity SQL – использует текстовый для представления запросов. Внешне схож с стандартным SQL, но отличается от него функциями. Необходимо отметить, что только с использованием Entity SQL можно напрямую обращаться к Клиентскому провайдеру данных Entity, минуя слой Службы объектов. В некоторых сценариях это может дать большой прирост в производительности.
  • LINQ to Entities – является диалектом языка запросов LINQ для Entity Framework и использует его синтаксис. Стоит отметить, что поддерживаются не все доступные методы. При этом их использование не является ошибкой на этапе компиляции. Однако в процессе выполнения приложения будет выброшено исключение NotSupportedException.

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

Принципы получения и модификации данных

Как уже было отмечено в прошлой части, сделать запрос можно двумя способами:

  1. Обратиться с помощью Entity SQL или LINQ to Entities к слою Служб объектов.
  2. Использовать Entity SQL для запроса напрямую к слою Клиентского провайдера данных Entity.

Рассмотрим принцип использования только первого варианта.

Основным объектом, который обеспечивает взаимодействие клиентского кода со слоем Служб объектов является контекст данных. Он представлен классом, унаследованным от ObjectContext или DbContext. Последний используется в подходе Код вначале.

Стоит отметить, что ObjectContext и DbContext не являются абстрактными. Механизм наследования используется только для того, чтобы добавить в новый класс свойства, отвечающие за доступ к данным. Их тип ObjectSet<T> или DbSet<T> соответственно, а имена совпадают с именами сущностей Модели. Эти свойства позволяют работать с объектами базы данных используя к LINQ to Entities.

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

Давайте перейдем от теории к практике и посмотрим как реализована работа с Entity Framework в разрабатываемом демонстрационном веб-приложении.

Комментарии (18) -

Осваиваю EF и не могу сделать довольно простую вещь, уже сломал всю голову.
Если кратко, то не могу повторить поведение TPH из Model First в Code First
Вопрос собственно описал тут www.sql.ru/forum/actualthread.aspx?tid=882961
Если будет возможность - посмотрите пожалуйста.

@ Alexandr: А можете привести код классов, которые создаете при Code First?

@ Andrey:

CREATE TABLE [dbo].[BaseSet](

    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Col0] [nvarchar](max) NOT NULL,
    [SomeId] [int] NOT NULL,
    [Col2] [nvarchar](max) NULL,
    [Col1] [nvarchar](max) NULL,
    [__Disc__] [nvarchar](max) NOT NULL,
)

CREATE TABLE [dbo].[SomeSet](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [SomeName] [nvarchar](max) NOT NULL
)


Остальное тут
dl.dropbox.com/u/218829/ConsoleApplication2.zip

Поглядел исходный код.
1) Вижу что Database.SetInitializer<TestSN51Context>(null); - а кто тогда создаст саму базу?

2) Добавлять __Disc__ необходимости нет. EF сам добавит Descriminator.

3) Аналогично и объявления имен колонок, совпадающих с именами свойств, не нужны.

4) Свойство Id по умолчанию является первичным ключом.

Добавил код обращения к таблицам (записал пару значений разных классов). В итоге получаем БД с 2 таблицами: одну для иерархии от BaseSet и одну для SomeSet.

1) База уже есть её не надо создавать
2), 3) и 4) С этим понятно, вопрос был не в этом.
в БД и так две таблицы, я привел их стуктуру выше.
Вопрос был в том, что можно ли сделать вот такое

http://i.stack.imgur.com/ypFZi.jpg

на Fluent API

Тогда вы меня запутали. Я думал что не получается именно сделать TPH. Если по текущему вашему контексту создать модель (что есть в EF tools), то она будет соответствовать приведенной. Исключение - связи (* - 0..1) вместо (* - 1). Это исправляется указанием: this.HasRequired(t => t.SomeSetForEntity1).WithMany(t => t.Entity1).WillCascadeOnDelete(false);

Если не в этом проблема - уточните вопрос (что именно не так).

Не работает так

При попытке запроса пишет
Invalid column name 'SomeSetForEntity1_Id'.Invalid column name 'SomeSetForEntity2_Id'

И генерит вот такой SQL

SELECT 

[Extent1].[Id] AS [Id],
[Extent1].[__Disc__] AS [__Disc__],
[Extent1].[Col0] AS [Col0],
[Extent1].[SomeId] AS [SomeId],
[Extent1].[Col1] AS [Col1],
[Extent1].[Col2] AS [Col2],
[Extent1].[SomeSetForEntity1_Id] AS [SomeSetForEntity1_Id],
[Extent1].[SomeSetForEntity2_Id] AS [SomeSetForEntity2_Id]
FROM [dbo].[BaseSet] AS [Extent1]
WHERE [Extent1].[__Disc__] IN ('Entity1','Entity2')


т.е. добавляет 2 лишних поля

Странно. Попробовал сделать запрос (select), предварительно записав в таблицу 2 объекта. Все работает (ошибок нет, возвращает нужные объекты).

Андрей, по вашему примеру написал следующее:

var newConnection = new EntityConnectionStringBuilder()

            {
                Metadata = @"res://*/MyModel.csdl|res://*/MyModel.ssdl|res://*/MyModel.msl;",
                Provider = @"System.Data.SqlClient",
                ProviderConnectionString = @"data source=server;initial catalog=dbase;persist security info=True;user id=sa;password=123;multipleactiveresultsets=True;App=EntityFramework"
            };

            using (var context = new MyModelConteiner(newConnection.ConnectionString))
            {
                var users = context.CreateObjectSet<Users>();

                var usersList = from r in context.Users
                                orderby r.Descr
                                select r;

                foreach (var r in usersList)
                    comboBox1.Items.Add(r.Descr.ToString());
            }


Но не работает...
В чем может быть причина?

всмысле не происходит подключения...

@ sergey: Проверьте строку подключения и прочитайте что пишет в тексте выпадающего Exception.

Разобрался - лишняя ";" в поле Metadata =)
А эксепшена никакого не вылезало.

Роман 11.07.2014 19:30:14

Добрый день пишу приложение MVC 4 + Entity Framework 6 code first
Есть проблемма планирования архитектуры возможно вы сможете подсказать, в интернете на эту тему либо нечего не нашел либо плохо искал так как даже не предпологаю куда смотреть.
Что хочу получить но нет идей как это реализовать:

предстваьте есть сайт который педоставляет услуги-сервисы и есть люди которые их заказывают

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

ну например чтоб заказать такси нужны следующие поля:
Откуда, куда, когда(датавремя)

для эвакуатора
Модель, марка, куда, когда(датавремя)

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

моя проблемма в том что я не могу спроектировать модели (с остальным бы я разобрался)

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

это вообще технически возможно ? если да направьте в нужную сторону может чего по читать нужно заранее спасибо

@ Роман: Можно попробовать сделать так
1) Модель с общими данными для услуг (id, когда добавлена, кем, описание и .т.д).

2) Модель с описанием полей услуги:
* id,
* id услуги,
* тип поля - enum, который указывает что это string, date, int, float, phone, money итд
* порядок в таблице (номер колонки, если говорить как про таблицу)
* его конфигурация - byte[] содержащий какие-то доп. условия. Например для строки - макс. длина, для int - диапазон итд

3) Модель с данными для (2). Здесь что-то вроде:
id, id поля, id "виртуальной" строки, данные (byte[])

По сути модель 2 описывает поля (структуру) нашей виртуальной таблицы, а модель 3 хранит ее данные.

АНДРЕЙ огромное спасибо за исчерпывающий ответ

@ Роман Кстати, вот еще что подумал. Для хранения данных можно попробовать использовать string, а не byte[]. Скорее всего большая часть полей будут строками, тогда будет быстрее и удобнее работать поиск. Остальные же типы данных (даты) в этом поле можно будет хранить как ToString или Encode Base64. А файлы и картинки вообще хранить в отдельной таблице c filestream, а тут сохранять только Id (правда нужно подумать как в этом случае лучше следить за целостностью - id приводить к string или же сделать отдельное поле "int? attach" как ключ к таблице с файлом).

Роман 16.10.2014 17:48:44

Спасибо

Доброго) Тоже есть вопрос по EF. Делаю диплом на MVC 4, надо сделать регистрацию пользователей с указанием города, выпадающими списками: страна, регион, город. В конечном итоге с подгрузкой аяксом. Но не получается даже просто синхронно. Загнал в таблицы базу со странами, регионами и городами. Городов в базе больше 11к. Когда запускаю в отладке, показывает, что контекст создается, но модели данными не заполняются. Возвращает NULL. Два дня пытаюсь разобраться в чем проблема. Ну и сам вопрос, а вдруг в этом может быть проблема: есть ли ограничения на кол-во записей в таблице при заполнении модели данными? Если есть, то это как то настраивается?

Добавить комментарий