Теперь самое время посмотреть, каким образом можно взаимодействовать с Entity Framework. И вполне логично будет начать с вариантов создания Модели данных Entity.
Создание Модели данных Entity (EDM)
Существует несколько подходов создания EDM. Рассмотрим их подробнее.
База данных вначале (Database first)
Данный подход подразумевает, что в первую очередь проектируется и разрабатывается база данных. Это может быть сделано при помощи любых доступных разработчику инструментов. После этого на её основе Entity Framework создаст описание EDM и классы Концептуальной модели.
При таком варианте проектирования архитектуры приложения главная роль отводится структуре базы данных. Классы бизнес логики вынуждены адаптироваться под неё. Однако это позволяет максимально раскрыть потенциал используемой системы управления базами данных.
Чтобы использовать рассматриваемый подход в своем проекте, необходимо выбрать пункт "Add Item" в контекстном меню проекта и добавить описание Модели данных Entity (ADO.NET Entity Data Model).
В появившемся диалоге "Entity Data Model Wizard" нужно выбрать вариант "Generate from a database". После этого потребуется указать базу данных и параметры соединения с ней (выбрать или создать строку соединения). В результате в проект будет добавлен EDMX-файл, который содержит описание EDM в формате XML.
Для редактирования созданного описания используется специальный дизайнер. В качестве примера возьмем базу данных, которая может быть разработана для создаваемого демонстрационного веб-приложения. Вид созданной на её основе EDM представлен на рисунке.
Каждый представленный здесь класс в терминологии Entity Framework называется сущностью. Все их свойства разделены на две группы:
- Связанные напрямую с полями базы данных: Id, Title, и т. д.
- Навигационные: 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="
data source=(local);
initial catalog=BookCatalog;
integrated security=True;
multipleactiveresultsets=True;
App=EntityFramework""
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.
Какой язык предпочтительнее? Однозначного ответа быть не может. У каждого из них есть свои плюсы. Поэтому данные вопрос, с учетом текущей задачи, каждый должен решить для себя сам.
Принципы получения и модификации данных
Как уже было отмечено в прошлой части, сделать запрос можно двумя способами:
- Обратиться с помощью Entity SQL или LINQ to Entities к слою Служб объектов.
- Использовать Entity SQL для запроса напрямую к слою Клиентского провайдера данных Entity.
Рассмотрим принцип использования только первого варианта.
Основным объектом, который обеспечивает взаимодействие клиентского кода со слоем Служб объектов является контекст данных. Он представлен классом, унаследованным от ObjectContext или DbContext. Последний используется в подходе Код вначале.
Стоит отметить, что ObjectContext и DbContext не являются абстрактными. Механизм наследования используется только для того, чтобы добавить в новый класс свойства, отвечающие за доступ к данным. Их тип ObjectSet<T> или DbSet<T> соответственно, а имена совпадают с именами сущностей Модели. Эти свойства позволяют работать с объектами базы данных используя к LINQ to Entities.
Полученные объекты отслеживаются слоем Служб объектов. Поэтому при вызове метода сохранения изменений, база данных будут соответствующе модифицирована.
Давайте перейдем от теории к практике и посмотрим как реализована работа с Entity Framework в разрабатываемом демонстрационном веб-приложении.