В прошлых частях была рассмотрена разработка простейшего веб-приложения, реализующего функции каталога книг. В её процессе не раз упоминалась библиотека Entity Framework, краткому знакомству с которой и будут посвящены несколько следующих частей.
Данная глава не ставит целью полное освещение Entity Framework. В её рамках будут рассмотрены только базовые принципы данной библиотеки и пример применения в ASP.NET MVC 3 приложениях.
Первую часть посвятим теории, поэтому знакомые с Entity Framework могут пропустить её.
Проблемы использования баз данных для хранения информации
Для хранения информации наиболее часто используются реляционные системы управления базами данными. Информация в них представлена в виде таблиц. Они содержат простые типы данных и, при необходимости, могут быть взаимосвязаны между собой.
Приложения, как правило, оперируют экземплярами классов, которые являются абстракциями объектов реального мира. Такой подход более удобен с точки зрения бизнес-логики. Кроме того, это дает такие преимущества при разработке как проверки типов, скорость работы кода, Intellisense в редакторе и т. д.
Таким образом, для использования реляционной базы данных необходимо создать конвертер, который будет преобразовывать объекты в табличный вид и наоборот. При этом он должен учитывать различия в организации информации в обоих форматах. Например, в таблицах все строки отличаются друг от друга. В противовес, в приложении могут существовать разные объекты, содержащие одинаковые данные.
Для простоты понимания давайте рассмотрим дальнейшее на небольшом примере. Максимально упростим уже используемую Модель. Будем считать, что в некой базе данных содержится две таблицы со следующими названиями и полями:
- таблица Publishers – перечень издательств:
- Id – уникальный код издательства;
- Title – наименование.
- таблица Books – данные о книгах:
- Id – уникальный код книги;
- Title – название;
- Authors – список авторов;
- PublisherId – уникальный код издательства, выпустившего книгу.
Давайте рассмотрим, каким образом .NET приложение может получить указанную информацию.
Объекты-контейнеры для таблиц
В .NET первым вариантом решения рассматриваемой проблемы является использование класса DataTable. По сути, он является представлением таблицы в виде объекта .NET и упрощает работу с ней в приложениях. Например, экземпляры данного класса можно передавать в элементы управления для заполнения их значениями.
using (SqlConnection sqlConn = new SqlConnection(connectionString)) {
using (SqlDataAdapter sqlAdapter = new SqlDataAdapter("Select * from Books", sqlConn)) {
DataTable dataTable = new DataTable();
sqlAdapter.Fill(dataTable);
listView.DataSource = dataTable;
listView.DataBind();
}
}
Посмотрим на еще один вариант, но уже с использованием класса SqlDataReader:
using (SqlConnection sqlConn = new SqlConnection(connectionString)) {
using (SqlCommand cmd = new SqlCommand("Select * from Books where Id = 1", sqlConn)) {
sqlConnection.Open();
using (SqlDataReader rd = cmd.ExecuteReader()) {
rd.Read();
bookTitle.Text = rd["Title"].ToString();
bookAuthors.Text = rd["Authors"].ToString();
}
}
}
Необходимо отметить, что данный подход имеет несколько серьезных недостатков:
- Большая степень связанности, т.к. код напрямую зависит от структуры базы данных и её таблиц. В частности, названия полей таблиц задаются в виде строковых констант. Это приводит к тому, что ошибки в их написании можно было определить только во время выполнения приложения. Легко заметить, что изменения структуры или имен полей базы данных приведёт к достаточно большим изменениям в исходном коде программы.
- Отсутствие контроля типов данных, т.к. все значения в DataTable представлены как экземпляры Object, базового класса .NET. Таким образом, перед их использованием потребуется явное преобразования к заданным типам.
Использование классов
Для решения отмеченных выше проблем, можно преобразовать данные, полученные из таблиц, в объекты, используемые в приложении. При этом, если сосредоточить такую обработку в одном месте, то можно сильно уменьшить связанность или, другими словами, зависимость от структуры базы данных. Там же будет происходить приведение типов, что позволит использовать их контроль в остальной части кода.
Для рассматриваемого примера можно написать следующий код получения данных:
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Authors { get; set; }
public int PublisherId { get; set; }
}
public class DatabaseTables
{
public Book LoadBook(int id)
{
Book book = null;
using (SqlConnection sqlConn = new SqlConnection(connectionString)) {
string query = string.Format("Select * from Books where Id = %1", id);
using (SqlCommand cmd = new SqlCommand(query, sqlConn)) {
sqlConnection.Open();
using (SqlDataReader rd = cmd.ExecuteReader()) {
rd.Read();
book = new Book();
book.Id = (int)rd["Id"];
book.Title = rd["Title"].ToString();
book.Authors = rd["Authors"].ToString();
book.PublisherId = (int)rd["PublisherId"];
}
}
}
return book;
}
}
Объектная модель
Давайте обратим внимание на поле PublisherId класса Book. Само по себе его значение особой роли в бизнес-логике приложения не играет. Ведь для неё цель – получить данные издательства, а не его некий абстрактный и непонятно от чего зависящий код. Таким образом, вполне логично изменить определение класса Book следующим образом:
public class Publisher
{
public int Id { get; set; }
public int Title { get; set; }
}
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Authors { get; set; }
public Publisher Publisher { get; set; }
}
Как легко заметить, в данном случае между собой связаны два класса, данные для которых содержатся в двух таблицах. В дальнейшем, при увеличении числа типов связи могут становиться сложнее. Например, одной книге могут соответствовать несколько других ключевых слов. В свою очередь, каждому такому слову соответствует несколько книг.
Группа классов для хранения данных, связанных между собой, называется объектная модель. А рассмотренное ранее соответствие вида "таблица – класс" перерастет в "база данных – объектная модель". Реализация данного подхода и приведет к созданию ORM библиотеки.
ORM библиотеки и решаемые ими задачи
Аббревиатура ORM расшифровывается как Object–Relational Mapping, что в переводе на русский язык значит Объектно-Реляционное Отображение.
Как следует из названия, основной задачей ORM является установка соответствия между объектами, используемыми в приложении, и таблицами, хранящимися в реляционных базах данных. При этом решается также ряд задач, указанных ниже:
Создание SQL запросов
При запросе приложением объектов, ORM библиотека самостоятельно создает SQL-код запросов и передает его в систему управления базами данных. При необходимости разработчик может вмешаться в данный процесс с целью тонкой оптимизации производительности.
Несоответствие типов
Не всегда для типов данных, используемых в реляционных базах данных, есть точные аналоги в .NET. Например, к таким можно отнести nvarchar(n). Это строка из unicode-символов ограниченной длины. Ближе всего в .NET к нему будет тип string. Однако у него отсутствует контроль длины строки. В качестве решения ORM может реализовывать его при записи значения в соответствующее свойство объекта.
Различные типы связей между таблицами
Различают несколько типов связей записей в таблицах, которые необходимо учитывать при преобразовании их в объектный вид и обратно:
- One-to-one (одна к одной) – записи в одной таблице соответствует одна запись в другой.
- One-to-many (одна ко многим) – записи в одной таблице соответствует несколько в другой. К этому типу относится вариант с таблицами Publisher и Book (один издатель – много книгам).
- Many-to-many (многие ко многим) – в этом случае для каждой записи из обоих таблиц соответствует несколько записей в другой. Проще понять данное взаимодействие можно на следующем примере: каждая книга может быть отмечена несколькими ключевыми словами, но и само ключевое слово может быть присвоено любому числу книг.
Отсутствие классов
Предположим, что необходимо сохранить адреса отправителя и получателя заказа. В .NET подобная задача может быть решена добавлением двух свойств типа Address. В реляционных СУБД для этого придется создавать копии полей для каждого адреса или специальной таблицы. Но независимо от используемого варианта, с помощью ORM эти адреса могут быть отображены как свойства одного объекта.
Отсутствие наследования
В системах управления базами данных нет классов, а значит нет и аналога наследования. В противовес этому в приложении часто встречаются ситуации когда удобно использовать объекты, унаследованные от общего базового класса. ORM может решить эту проблему, связав таблицы с классами-наследниками.
Сокрытие идентификатора id
ORM позволяет использовать в качестве уникального идентификатора значение, которое соответствует бизнес-логике приложения. Например, для печатных книг это может быть их ISBN код. При этом внутри базы данных будет использоваться привычный для многих целочисленный уникальный идентификатор id.
Может возникнуть вопрос: почему не использовать ISBN сразу вместо id. Дело в том, что вместо упрощения это приведет к усложнению приложения. Ведь поскольку ISBN указывается пользователем, то вполне возможны ошибки при его вводе. Для их исправления необходимо будет не только изменить его в основной таблице, но также и во всех связанных с ней. При использовании id такая проблема отсутствует.
Абстракция используемой базы данных
В процессе работы с ORM библиотекой приложение оперирует привычными ему объектами. При этом для хранения информации могут быть использованы различные реляционные системы управления базами данных: SQL Server, SQL Server Express, SQL Server Compact, mySQL и т.д. Это вносит дополнительный уровень гибкости в архитектуру приложения.
Что такое Entity Framework?
Ответ на вопрос, поставленный в заголовке, будет очень короткий: это ORM библиотека от Microsoft.
Архитектура Entity Framework
Посмотрим на схему с изображением архитектуры библиотеки:
Entity Data Model
Модель данных Entity (EDM или Entity Data Model) это модель, описывающая отношение клиентских объектов и таблиц, расположенных в базе данных. Можно выделить следующие её составляющие:
- Концептуальная модель (Conceptual model) – содержит описание классов клиентской Модели и взаимоотношения между ними.
- Модель хранилища (Storage model) – аналогична Концептуальной модели, но описывает таблицы, расположенные в реляционной базе данных.
- Отображение (Mapping) – содержит схему соответствия между указанными выше моделями.
Слой Службы объектов (Object Services)
Для получения необходимой информации клиент может воспользоваться любым из двух поддерживаемых языков: Entity SQL и LINQ to Entities.
Запросы передаются в слой Службы объектов, который отвечает за взаимодействие с объектами клиентской части. Здесь они преобразуются в деревья команд (command tree). Кроме того, осуществляется контроль текущего состояния объектов. Это необходимо для сохранения сделанных в них изменений.
Кроме того, данный слой отвечает за преобразование данных, передаваемых клиенту от базы данных. При этом объекты с табличной структурой преобразуются в экземпляры классов концептуальной Модели.
Слой Клиентского провайдера данных (Entity Client data provider)
Слой Клиентского провайдера данных используется для взаимодействия с базой данных. Для упрощения архитектуры, он не обращается к ней напрямую, а использует провайдера данных ADO.NET.
При получении от Службы объектов дерева команд, данный слой создает SQL запрос. Для этого используется все составляющие, входящие в Модель данных Entity. После чего результат передается на выполнение в базу данных, используя ADO.NET.
При получении результата, Слой Клиентского провайдера данных преобразует его из простой табличной формы в специальные объекты и передает далее в Службы объектов для окончательной обработки.
Слой провайдера данных ADO.NET (ADO.NET data provider)
Последний слой, Провайдер данных ADO.NET, используется для непосредственного обращения к реляционной системе управления базами данных.
В следующей части будет рассмотрены основные принципы использования библиотеки Entity Framework. После чего перейдем к примеру её использования в ASP.NET MVC 3.