Этот пример наглядно демонстрирует разнонаправленность двух механизмов LINQ для SQL и Entity Framework, которая позволяет им существовать параллельно: если вам нужно отображение баз данных SQL Server на классы вашей бизнес-логики, чтобы все "просто работало", необходима скоростная разработка, и к гибкости ORM не предъявляются повышенные требования, то LINQ для SQL вполне может стать тем инструментом, который полностью удовлетворит ваши потребности. В случае же, когда к структуре модели данных и возможностям ORM предъявляются серьезные требования, вроде потенциальной поддержки разнообразных источников данных, то среди двух описанных ранее технологий Entity Framework — это то, что вам нужно.
Принципы построения слоя доступа к данным
Грамотно построенное приложение состоит из множества слабосвязанных, легкозаменяемых частей. В этом смысле при построении слоя доступа к данным (рис. 3.4) можно определить следующие принципы:
□ для доступа к модели данных необходимо использовать механизм ORM, который предпочтительнее всего взять со стороны, но не реализовывать своими силами;
□ интеграция ORM в систему должна быть слабосвязанной, так, чтобы существовала безболезненная возможность перейти на другой вариант ORM;
□ вместе с моделью данных, которую предоставит ORM, необходимо реализовать хранилища и сервисы, которые будут предоставлять доступ к необходимым наборам данных и реализовывать добавление, видоизменение и удаление данных. Реализация слабосвязанных сервисов и хранилищ позволит скрыть подробности реализации доступа к данным, предоставляемых ORM, что даст большую гибкость слою доступа к данным и позволит исключить жесткую привязку, например, к LINQ-запросам для работы с ORM.
Что дает такая схема, и зачем нужна дополнительная прослойка между ORM и бизнес-логикой приложения:
□ внедрение хранилища для типовых запросов, например, получения записи по идентификатору или получения всех заявок заказчика, позволит, с одной стороны, унифицировать получение таких данных, а с другой, сделает связь с ORM менее жесткой;
□ внедрение сервисов похоже на внедрение хранилищ, только реализующих логику управления данными. Например, хорошей практикой является определение сервиса, который добавляет, изменяет либо удаляет данные. Обращаясь к такому сервису, ваш код не будет зависеть от реализации функций, существующей ORM и даже базы данных;
□ внедрение такого рода инъекций кода отделяет бизнес-логику от ORM и уменьшает зависимость между ними, что позволит в дальнейшем без особых трудов использовать другой ORM, переписав для этого только хранилище и сервисы. В случае же прямой связи бизнес-логики с ORM, безболезненной замены ORM провести не удастся, т. к. придется инспектировать и переписывать весь написанный код.
Возможность замены источника данных
Для более наглядного примера потребности в промежуточном коде и необходимости предусматривать возможные изменения в работе с ORM приведем случай из практики одного из авторов книги. Компания, в которой он работал, разрабатывала крупный проект, работающий с использованием SQL Server 2000. После нескольких лет разработки и поддержки, заказчик пожелал сменить SQL Server на другую СУБД. В связи с тем, что код проекта был жестко завязан на особенностях SQL Server, такой переход стоил больших усилий всего персонала компании.
В то время еще не существовало LINQ для SQL, но даже если бы компания использовала этот ORM, ей все равно пришлось бы переписывать подавляющую часть кода работы с данными в связи с тем, что LINQ для SQL не поддерживает ничего, кроме SQL Server.
В случае же, если бы существовала инъекция кода в виде хранилищ и сервисов, необходимо было бы реализовать только их новый вариант без затрагивания любого другого кода. Излишне говорить, что затраченное на это время было бы значительно меньшим по сравнению с тем, сколько было потрачено на самом деле.
Реализация слоя данных
Создадим простейший слой для работы с базой данных, который отвечал бы всем нашим требованиям. Для начала возьмем простую структуру базы данных (рис. 3.5).
Рис. 3.5. Структура базы данных
У нас есть три таблицы: заказчики, заказы и товары. Каждая из таблиц содержит набор свойственных ей полей, так в таблице Products (товары) есть поле isAvailible, которое показывает, доступен ли товар в настоящее время. У таблицы Orders (заказы) есть поля count — количество товара в штуках и orderDateTime — дата и время оформления заказа. Таблица Customers (заказчики) содержит информацию о заказчике.
Для реализации хранилищ и сервисов нам необходимы интерфейсы данных, определим их так, как показано в листинге 3.1.
Листинг 3.1. Интерфейсы данных
public interface ICustomer {
Guid CustomerId { set; get; }
string Name { set; get; }
string Phone { set; get; }
string Address { set; get; }
}
public interface IOrder {
Guid OrderId { set; get; }
Guid CustomerId { set; get; }
Guid ProductId { set; get; }
int Count { set; get; }
DateTime OrderDateTime { set; get; }
}
public interface IProduct {
Guid ProductId { set; get; }
string Name { set; get; }
bool IsAvailible { set; get; }
bool Cost { set; get; }
}
Воспользуемся мастером создания модели LINQ для SQL, чтобы сгенерировать классы для работы с базой данных. Посмотрим на сгенерированный код для таблицы Customers (приведен только фрагмент кода):
public partial class Customer : INotifyPropertyChanging,
INotifyPropertyChanged
{
private System.Guid _customerId;
private string _name;
private string _address;
private string _phone;
private EntitySet<Order> _Orders;
public Customer()
{
// код
}
[Column(Storage="_customerId",
DbType="UniqueIdentifier NOT NULL",
IsPrimaryKey=true)] public System.Guid customerId {
get { return this._customerId; }
set { // код }
}
[Column(Storage="_name",
DbType="NVarChar(250) NOT NULL", CanBeNull=false)]
public string name {
get { return this._name; }
set { // код }
}
[Column(Storage="_address",
DbType="NVarChar(1024) NOT NULL",
CanBeNull=false)]
public string address {
get { return this._address; }
set { // код }
}
[Column(Storage="_phone", DbType="NVarChar(250)")]
public string phone {
get { return this._phone; }
set { // код }
}
[Association(Name="Customer_Order",
Storage="_Orders", ThisKey="customerId",
OtherKey="customerId")]
public EntitySet<Order> Orders {
get { return this._Orders; }
set { this._Orders.Assign(value); }
}
}
Полученный код примечателен тем, что класс Customer является partial-классом, а это значит, что мы можем легко расширить его, и все прочие классы, для поддержки наших интерфейсов. Создадим частичные классы для реализации интерфейсов на базе LINQ для SQL так, как показано в листинге 3.2.