Entity Framework в многоуровневых архитектурах
ОГЛАВЛЕНИЕ
В статье за этот месяц я продемонстрирую добавление платформы Entity Framework в n-уровневую архитектуру, в которой используются технологии Windows® Communication Foundation (WCF) и Windows Presentation Foundation (WPF), а также шаблон «модель-представление-презентатор» (Model View Presenter, MVP). Я представлю пример архитектуры со слоями для базы данных логического хранилища, доступа к данным, модели области, диспетчера, уровня службы, уровня представления и пассивного уровня пользовательского интерфейса и продемонстрирую интеграцию этих слоев с помощью платформы Entity Framework. Все используемые примеры кода доступны для загрузки на веб-узле журнала MSDN Magazine.
Определение слоев
Представленное приложение позволяет выполнять поиск клиентов в образце базы данных NorthwindEF, а также просматривать, добавлять, изменять и удалять их. Но прежде чем углубляться в код и примеры, давайте рассмотрим общую архитектуру примера. Поскольку основное внимание уделяется не архитектуре, а интеграции платформы Entity Framework с архитектурным проектом, я выбрал относительно распространенную архитектуру, которая может быть достаточно просто изменена и интегрирована с другими стратегиями.
На рис. 1 показано общее представление типичной многослойной архитектуры. Два верхних слоя предназначены для представления пользовательского интерфейса и перемещения по нему с помощью слоя пользовательского интерфейса и слоя презентации. Слой пользовательского интерфейса может быть реализован с помощью любой из многочисленных доступных технологий; однако в этой статье и примерах используется платформа WPF. Слой пользовательского интерфейса соответствует шаблону MVP с пассивным представлением, это означает, что представления (верхний слой пользовательского интерфейса) управляются и подготавливаются слоем презентации. Презентаторы ответственны за предоставление представлениям данных, получение данных из представлений для сохранения в нижних слоях и вообще за реагирование на события, созданные представлениями.
Рис. 1. Обзор архитектуры
В моем примере презентаторы взаимодействуют с нижними слоями через платформу WCF. Презентатор вызывает службу через платформу WCF, используя контракт службы в качестве руководства. Слой службы предоставляет службы через интерфейсы контрактов служб. Эти контракты позволяют презентаторам получать сведения о вызове служб.
Слой службы ответственен за получение данных от презентаторов и вызов соответствующих методов бизнес-слоя, выполняющих соответствующую бизнес-логику, а также сбор и модификацию данных. Бизнес-слой содержит бизнес-логику и код LINQ к Entities. Код LINQ к Entities ссылается на сущностную модель, автоматически созданную на основе платформы Entity Framework. При выполнении запросов LINQ платформа Entity Framework преобразует запрос LINQ в концептуальную сущностную модель (Entity Data Model – EDM ), сопоставляет аспекты сущности уровню хранилища и создает запрос SQL для выполнения для базы данных.
Создание модели
Теперь, после представления общего обзора работы слоев в архитектуре, рассмотрим ключевые аспекты всех слоев, относящихся к Entity Framework. Поскольку для приложения уже существует база данных, я создал сущностную модель, используя базу данных NorthwindEF в качестве отправной точки.
Кроме того, я мог сначала создать сущностную модель, а потом сопоставить сущности с базой данных. Мастер EDM помогает в создании модели базовой сущности, которая затем может быть нужным образом изменена для включения наследования, разделения сущностей и других концепций моделирования домена. На рис. 2 показан мастер EDM со всеми таблицами и хранимыми процедурами, выбранным для импорта в EDM.
Рис. 2. Создание модели на основе базы данных
Один вопрос относительно EDM, который часто вызывает путаницу, это соглашение об именовании по умолчанию для наборов EntitySet и объектов EntityTypes. Я использую единичные имена для всех сущностей в моделях областей. Я создаю экземпляр Customer или возвращаю список экземпляров Order с помощью List<Order>. Все сущности – это единственные экземпляры плана, свойства которого определяют сущность.
С другой стороны, для наборов EntitySet я использую соглашение о множественных наименованиях. Наборы EntitySet часто используются в запросах LINQ при запросе контекста ObjectContext на ссылку на набор объектов Customer или Order.
В качестве примера рассмотрим следующий запрос LINQ к Entities:
var q = from c in context.Customers
select c;
List<Customer> customerList = q.ToList();
Этот запрос указывает LINQ к Entities на доступ к набору EntitySet с именем Customers и возврат всех экземпляров сущности Customer по выполнении. Вторая строка выполняет запрос и возвращает List<Customer> локальной переменной customerList. В данном примере EntitySet имеет множественное число, из чего понятно, что выполняется запрос наборов EntitySet и возвращаются экземпляры сущности Customer (обратите внимание на единственное число).
Необходимо ли придерживаться такого соглашения об именовании? Конечно, нет. Однако это делает код более удобным для чтения. В противном случае мастер EDM по умолчанию возвращает наборы EntitySet с именами Customer и EntityType с именем Customers, в результате чего запрос LINQ к Entities выглядит следующим образом:
var q = from c in context.Customers
select c;
List<Customers> customerList = q.ToList();
При создании модели мастером EDM имена EntitySet и EntityType могут быть просто изменены. Для этого необходимо выбрать сущность в схеме, просмотреть ее свойства в окне «Properties» (Свойства) и изменить нужный параметр (см. рис. 3). Для этого приложения я установил для всех объектов EntityType единственное число с помощью свойства Name. Я не изменял свойство Name набора EntitySet, поскольку для него уже установлено множественное число.
Рис. 3. Изменение имен EntityType
Описание полученных результатов
Теперь я продемонстрирую приложение и рассмотрю его работу, начиная с верхнего слоя, представлений (находятся в проекте NWUI) и презентаторов (находятся в проекте NWPresentation). Оба проекта доступны в материалах для загрузки, прилагаемых к статье. Приложение загружает представление поиска клиентов, позволяющее выполнять поиск клиентов по названию компании (см. рис. 4). Представление реализуется с помощью платформы WPF, и при взаимодействии пользователя с ним создаются события, прослушиваемые презентатором, который может выполнить соответствующее действие.
Рис. 4. Поиск клиентов
При поиске пользователем всех клиентов, начиная с буквы D, как показано на рис. 4, представление создает событие при нажатии пользователем кнопки «Search» (Поиск). Презентатор прослушивает это событие и отвечает на него, вызывая слой служб через платформу WCF для получения списка сущностей клиентов, которые будут отображены в представлении CustomerSearchView. Ниже приведен код в представлении при нажатии пользователем кнопки «Search» (Поиск):
private void btnSearch_Click(object sender,
RoutedEventArgs e) {
if (FindCustomerSearchResults != null)
FindCustomerSearchResults();
}
Этот код не взаимодействует со списком возвращенных сущностей, оставляя эту работу презентатору. Представление использует привязку данных платформы WPF для ссылки на свойства сущности, поэтому ему известно, как осуществляется привязка списка сущностей к элементам элемента управления представления списком. Взаимодействие представлений с сущностями осуществляется только через привязку данных.
Представление CustomerSearchView создает событие FindCustomerSearchResults, а презентатор CustomerSearchPresenter прослушивает событие и отвечает на него, принимая маркер и выполняя поиск. В следующем коде показано создание классом CustomerSearchPresenter экземпляра класса NWServiceClient, являющегося прокси для службы WCF, предоставляемой на нижнем слое:
public void view_FindCustomerSearchResults()
{
if (this.view.CompanyNameCriteria.Length > 0)
using (var svc = new NWServiceClient())
{
IList<Customer> customerList = svc.FindCustomerList(
view.CompanyNameCriteria);
view.CustomerSearchResultsList = customerList;
}
}
Обращение к NWServiceClient осуществляется с помощью ServiceReference из проекта NWPresentation, поэтому презентаторам известно о способе вызова служб и о типах возвращаемых данных. Слой презентаций не должен и не ссылается на EDM напрямую. Вместо этого он сообщает ожидаемые типы сущностей с помощью атрибутов DataContract, представленных через платформу WCF. Это позволяет передачу сущностей платформы Entity Framework презентаторам через физические границы сети с помощью платформы WCF.
Обратите внимание, что после возврата списка сущностей Customer для него устанавливается общее свойство представления. После этого данное свойство представления принимает List<Customer> и выполняет его привязку к контексту DataContext представления. Презентатор предоставляет данные, передает их, а представление все специфичные для него привязки (поскольку этот код очень специфичен для конкретной технологии, будь то WPF, Silverlight, Windows Forms или ASP.NET).
Этот прием позволяет использовать тот же самый презентатор для взаимодействия с любыми представлениями, реализующими интерфейс ICustomerSearchView. В данном приложении привязка обрабатывается методами привязки WPF с помощью DataContext.
Контракты предоставляют метод, который затем может быть вызван на уровне службы, так же как и возвращенные сущности. В данном приложении используется только метод для возврата типов сущностей Customer и Order. Это означает, что в контракт будут включены только эти типы сущностей.
Платформа WCF выполняет сериализацию сущностей, применяя к ним при необходимости атрибуты DataContract WCF. Благодаря предоставлению сущностей через DataContracts сущности могут использоваться на уровнях пользовательского интерфейса, не ссылаясь непосредственно на EDM.
Обратите внимание, что для бета-версии 1 пакета обновления 1 (SP1) для .NET Framework 3.5 платформа Entity Framework поддерживает автоматическую сериализацию графа. Например, если у родительской сущности имеются соответствующие дочерние сущности, будут сериализованы как родительские, так и дочерние сущности. Поскольку в примере приложения метод FindOrderList OrderManager использует запрос LINQ к Entities, который выполняет упреждающую загрузку объекта Order Details для всех сущностей Order, каждая сущность Order, возвращенная из промежуточного уровня, будет содержать List<OrderDetail>, доступный через свое свойство переходов.
Хотя сериализованные сущности могут быть переданы между презентаторами и слоем служб через платформу WCF, ObjectContext не сериализуется и не передается презентатору. Это означает, что сущности могут использоваться в слоях пользовательского интерфейса, а ObjectContext остается в нижних слоях, где для него будет доступна модель EDM и все ресурсы платформы Entity Framework.
Оставление ObjectContext внизу означает, что он может использоваться для получения или изменения сущностей непосредственно в слое пользовательского интерфейса, но не может использоваться для управления отслеживанием изменений в слое пользовательского интерфейса. Лучше всего все равно оставить эти роли для нижних слоев. Но при обратной передаче сущностей нижним слоям приложение должно выполнить синхронизацию с ObjectContext, чтобы оно могло сохранить изменения в сущностях.
При нажатии пользователем кнопки «Search» (Поиск), показанной на рис. 4, презентатор вызывает слой служб, который затем вызывает бизнес-слой (находится в проекте NWBusinessManagers) для получения List<Customer>. Этот слой имеет две основные функции. Первая заключается в получении данных из модели EDM или размещения данных в ней. Вторая состоит в обработке любой существующей бизнес-логики.
Диспетчер CustomerManager использует контекст ObjectContext для взаимодействия с EDM, поэтому он определяет локальное поле с именем «context» и создает его экземпляр в конструкторе. Контекст ObjectContext может быть создан и удален во всех методах. Однако он оптимизирован для открытия и закрытия ресурсов подключения к базе данных в случае такой необходимости. Кроме того, обеспечивая доступность контекста ObjectContext через его класс, он может поддерживать отслеживание изменений без необходимости передачи ряда частных методов внутри класса:
public CustomerManager()
{
context = new NWEntities();
}
Однако имейте в виду, что для этого типа приложения контекст ObjectContext не должен удерживаться, а должен при необходимости создаваться/удаляться. Из-за разрешения идентификаторов удерживание одного и того же контекста объекта в конечном итоге приведет к несогласованности и неактуальности данных и (при увеличении объема отслеживаемых данных) снижению производительности при разрешении идентификаторов, кроме того, это может привести к проблемам обновления в многопоточных средах.
В следующем коде показан метод FindCustomerList класса CustomerManager в бизнес-уровне. Этот метод объявляет запрос LINQ к Entities, получающий доступ к контексту для запроса списка начинающихся с критерия сущностей Customer. При выполнении этого запроса он вычисляет сопоставления с концептуального слоя до слоя хранилища и создает соответствующую команду SELECT:
public List<Customer> FindCustomerList(string companyName)
{
var q = from c in context.Customers
where c.CompanyName.StartsWith(companyName)
select c;
return q.ToList();
}
При необходимости можно просмотреть выполнение запросов с помощью профилировщика SQL Server®.
Сохранение изменений
Теперь, после рассмотрения приложения, использующего простое извлечение, можно рассмотреть сохранение изменений данных. При изменении пользователем клиента представление CustomerView связано с экземпляром соответствующей сущности Customer (см. рис. 5). Представление CustomerView создает событие для презентатора, который в свою очередь запрашивает экземпляр сущности Customer из нижних слоев.
Рис. 5. Изменение клиентов
При изменении пользователем клиента и сохранении изменений сущность передается от презентатора в нижние слои с помощью кода, показанного на рис. 6. Этот код вычисляет, выполнил ли пользователь добавление или изменение клиента, и затем вызывает соответствующий метод слоя служб, передавая сущность.
Рис. 6. SaveCustomer в презентаторе
public virtual void view_SaveCustomer()
{
Customer customer = view.CurrentCustomer;
var svc = new NWServiceClient();
switch (view.Mode)
{
case ViewMode.EditMode:
svc.UpdateCustomer(customer);
break;
case ViewMode.AddMode:
svc.AddCustomer(customer);
break;
default:
break;
}
view.CurrentCustomer = FindCustomer();
}
Затем слой служб передает управление бизнес-слою, который сохраняет сущность клиента в базе данных. Поскольку сущность клиента больше не является частью контекста ObjectContext, она должна быть снова объединена с классом ObjectContext с помощью его метода Attach, как показано в приведенном ниже коде. После объединения сущности с контекстом свойства сущности должны быть помечены как измененные. Это можно выполнить с помощью диспетчера ObjectStateManager контекста и вызова метода SetModified для каждого свойства. Теперь, когда контексту известно об изменении сущности, может быть вызван метод SaveChanges, который создает команду SQL UPDATE и выполняет ее для базы данных:
public void UpdateCustomer(Customer customer)
{
context.Attach(customer);
customer.SetAllModified(context); // custom extension method
context.SaveChanges();
}
Обратите внимание, что код в методе UpdateCustomer использует метод расширения, который я назвал SetAllModified<T>, что упрощает установку состояния всех свойств для изменяемой сущности. SetAllModified<T> получает экземпляр ObjectStateEntry для определенной сущности T. Затем он получает список имен всех свойств для сущности и итеративно вызывает SetModifiedProperty для каждого свойства:
public static void SetAllModified<T>(this T entity, ObjectContext context)
where T : IEntityWithKey
{
var stateEntry = context.ObjectStateManager. GetObjectStateEntry(entity.EntityKey);
var propertyNameList = stateEntry.CurrentValues.DataRecordInfo. FieldMetadata.Select
(pn => pn.FieldType.Name);
foreach (var propName in propertyNameList)
stateEntry.SetModifiedProperty(propName);
}
Другим способом сохранения сущности является вызов метода Refresh контекста. Это указывает контексту на необходимость получения данных для экземпляра сущности и обновление значений свойств по значениям базы данных. Перечислитель RefreshMode для ClientWins заменяет исходные значения на последние значения в базе данных, таким образом обеспечивая осуществление стратегии «побеждает последний».
Перечислитель RefreshMode для StoreWins переписывает исходные и текущие значения в кэше сущности, используя значения базы данных. ClientWins – подходящая стратегия для принципа «побеждает последний», а стратегия StoreWins подходит для случаев необходимости отмены изменений и обновления представления пользовательского интерфейса с последними значениями в базе данных:
context.Refresh(RefreshMode.ClientWins, customer); // Last in wins
Платформа Entity Framework обеспечивает выполнение оптимистичного параллелизма при создании команд обновления и удаления. Это достигается путем включения исходных значений в предложение WHERE для всех свойств, для атрибута ConcurrencyMode которых установлено значение Fixed.
По умолчанию модели создаются без полей, указанных как поля параллелизма. Это означает, что сохранение пользователем изменений может привести к непреднамеренной перезаписи изменений другого пользователя. При изменении другим пользователем значения при открытом представлении CustomerView и необходимости использования оптимистичного параллелизма это можно осуществить путем установки атрибута ConcurrencyMode для типа EntityType в концептуальной модели.
Изменение файла EDM и установка для ConcurrencyMode значения Fixed указывает платформе Entity Framework на добавление этого столбца в предложение WHERE всех команд Update или Delete Таким образом, если соответствующая строка не будет найдена, возникает исключение OptimisticConcurrencyException. На рис. 7 показано данное создание исключения при изменении региона клиента в базе данных непосредственно перед попыткой изменения пользователем этого же региона.
Рис. 7. Исключение OptimisticConcurrencyException
Можно перехватить это исключение и выполнить любое соответствующее действие. Например, можно перехватить исключение, зарегистрировать его, а затем переписать изменения пользователя, как показано ниже:
catch (OptimisticConcurrencyException e){
context.Refresh(RefreshMode.ClientWins, customer); // Last in wins
logger.Write(e);
context.SaveChanges();
}
Удаление и добавление
При удалении пользователем клиента метод DeleteCustomer CustomerManager получает сущность клиента и выполняет операцию удаления:
context.Attach(customer);
context.DeleteObject(customer);
context.SaveChanges();
Прежде всего, экземпляр сущности Customer должен быть повторно объединен с ObjectContext с помощью метода Attach. Затем клиент должен быть удален из контекста ObjectContext. При использовании такого способа механизму отслеживания изменений контекста ObjectContext известно об удалении экземпляра сущности Customer. Наконец, при вызове метода SaveChanges теперь контексту ObjectContext известно об удалении сущности, поэтому он должен создать команду SQL DELETE и выполнить ее.
При добавлении пользователем клиента метод AddCustomer CustomerManager получает сущность клиента и выполняет операцию вставки, например следующую:
context.AddToCustomers(customer);
context.SaveChanges();
Этот экземпляр сущности является новым, поэтому он должен быть добавлен в контекст и отмечен как новый экземпляр сущности Customer путем сопоставления экземпляра сущности Customer с контекстом ObjectContext с помощью метода AddToCustomer. Наконец, при вызове метода SaveChanges теперь контексту ObjectContext известно о добавлении удалении сущности, поэтому он должен создать команду SQL INSERT и выполнить ее.
Заключение
В данной статье я продемонстрировал интеграцию платформы Entity Framework с архитектурой, использование современных шаблонов, таких как шаблон MVP, и разрешение распространенных архитектурных проблем. Ключевыми компонентами платформы Entity Framework в многослойных архитектурах являются механизмы отслеживания изменений, интеграция с LINQ к Entities, возможность отключения и повторного подключения к контексту ObjectContext, а также предоставления разработчику средств разрешения проблем параллелизма.
Автор: Джон Папа