ASP.NET MVC Framework

Магдануров Гайдар

Юнев Владимир

ГЛАВА 3

Модель и доступ к данным

 

 

Согласно паттерну проектирования MVC основное назначение модели — это определение объекта приложения, представлением которого является вид (View). Модель и вид отделены друг от друга и взаимодействуют только в виде оповещений: изменившись, модель оповещает вид, который, согласно новым данным, изменяет свое состояние. Со своей стороны, вид обращается к модели для получения актуальных данных для отображения.

Структура этой главы построена так, что мы начнем рассмотрение построения доступа к данным с обзора механизма Object Relation Mapping, его истории, развития и того, какие механизмы ORM присутствуют в .NET. Мы рассмотрим технологию LINQ, LINQ для SQL, Entity Framework. После этого на простом примере разберем принцип организации эффективного доступа к данным в проектах ASP.NET MVC. В конце главы вас ждет раздел, в котором описаны наиболее популярные механизмы доступа к данным.

Задача получения доступа к данным из кода программы достаточно неординарна по своей природе. С одной стороны, нам хотелось бы иметь возможность оперировать сущностями данных, например, при наличии сущности "Заказчики" (таблица Customers), мы бы хотели иметь в коде возможность оперировать списками заказчиков, иметь возможность добавлять, удалять и изменять данные заказчиков. С другой стороны, мы бы не хотели, чтобы происходило смешивание кода бизнес-логики с такими данными, как строки SQL-запросов, что может привести к самым плачевным последствиям: от нечитабельности кода до непонятных и трудно диагностируемых ошибок или дыр в безопасности в виде SQL-инъекций. Чтобы избежать этого, мы хотели бы отказаться от написания запросов к нашему источнику данных на свойственном базе данных языке (например, построение строк SQL-запросов или разбор XML-файлов), внедрив некую прослойку между данными и операциями над ними. Такой прослойкой, которая с одной стороны предоставит нам полный контроль над данными, а с другой, позволит оперировать над ними в нужном нам стиле, становится ORM.

ORM — это аббревиатура от Object Relation Mapping или, по-русски, объектно-реляционная проекция. Очень легко объяснить сущность ORM: это механизм, который отображает на объекты объектно-ориентированного языка программирования сущности источника данных. Таким образом, одна запись таблицы Customers реляционной СУБД отображается в класс Customer в вашем коде (рис. 3.1).

#i_016.jpg

Рис. 3.1. Отображение сущности базы данных в класс на языке C#

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

□ высокий порог вхождения для новичка, тогда как использование распространенного стороннего ORM предполагает большую вероятность того, что новичок знаком с ним;

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

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

Поэтому сторонние универсальные ORM получили такое широкое распространение.

ORM — не настолько старая технология, как можно было бы подумать. Трудно сказать достоверно, но, возможно, самым первым ORM, получившим широкое распространение, был продукт TopLink для языка программирования Smalltalk. Тогда этот проект принадлежал компании The Object People, название которой и легло в основу названия продукта. TopLink для SmallTalk вышел в 1994 году, а уже в 1996 году появилась версия для Java, которая называлась соответственно TopLink для Java. На сегодняшний момент, после серии покупок, TopLink принадлежит компании Oracle, которая продолжает выпускать его для Java-разработчиков.

Следующими за TopLink в очередности ORM-продуктов можно считать Java Database Objects и Entity Beans, первые версии которых появились в конце 90-х—начале 2000-х годов. Как нетрудно заметить, на ранних стадиях ORM как технология развивалась в основном в Java-среде.

Но самой значимой вехой в истории ORM можно считать появление продукта Hibernate, который был разработан сообществом Java-программистов под руководством Гэвина Кинга (Gavin King). Позднее компания JBoss наняла ведущих разработчиков Hibernate для развития и поддержки проекта. Еще позднее JBoss стала частью компании Red Hat, которая до сих пор поддерживает проект Hibernate.

С ростом популярности .NET-платформы самая известная ORM-библиотека была портирована и на .NET, получив название NHibernate. Последняя версия NHibernate 2.0.1 вышла в конце 2008 года, на момент написания книги известно, что версия 3.0 проекта находится в разработке и будет использовать .NET 3.5-функционал, в том числе и LINQ, о котором речь пойдет далее.

На данный момент ORM-библиотеки реализованы для многих популярных платформ и языков программирования: С++, Delphi, PHP, Python, Ruby, Perl, в чем можно убедиться, если посмотреть статью про ORM в Wikipedia. Для каждого из языков на текущий момент существует несколько, если не сказать множество, разнообразных ORM. Так, только для одной платформы .NET существует около тридцати разнообразных ORM-библиотек, что наглядно иллюстрирует современный тренд к повсеместному использованию ORM в разработке проектов любой сложности.

Компания Microsoft также представила свои решения в области ORM, ими стали LINQ для SQL и Entity Framework. Работа с обеими библиотеками производится с помощью интегрированного языка запросов LINQ, о котором и пойдет речь далее.

 

Технология LINQ

Технология LINQ или Language Integrated Query, что можно перевести как "интегрированные в язык запросы", появилась на свет вместе с выходом .NET Framework 3.5 в ноябре 2007 года. Как и многие другие наработки Microsoft, LINQ первоначально прошла путь экспериментального проекта под названием С(омега).

Суть LINQ проста — дать возможность разработчикам единообразно работать с коллекциями и наборами данных из разных источников, будь то: базы данных, XML-файлы, коллекции в языке программирования. Для этих целей реализуется определенный LINQ-провайдер, вроде встроенных в .NET Framework LINQ для XML, LINQ для SQL, LINQ для объектов, LINQ для сущностей. Решению этой задачи поспособствовали добавленные в .NET Framework 3.5 языковые расширения. Например, в C# появились анонимные методы и лямбда-выражения, которые интенсивно используются в LINQ.

Рассмотрим простейший пример LINQ-выражения:

List a = new List(3);

a.Add(3);

a.Add(12) ;

a.Add(-l);

List positive = a.Where(x => x >= 0).Select(x => x).ToList();

В данном примере создается коллекция типа List (список целочисленных значений) и заполняется тремя значениями. Последняя строчка — это и есть работа LINQ-механизма, который, используя набор методов LINQ для объектов, позволяет нам выбрать все положительные значения (согласно условию лямбда-выражения) в другой список. Для сравнения посмотрите на фрагмент кода, который выполняет те же действия, но без использования LINQ:

List a = new List(3);

a.Add(3) ;

a.Add(12) ;

a.Add(-1);

List positive = new List();

foreach (int item in a)

{

  if (item >= 0)

  positive.Add(item) ;

}

То, для чего обычно требовалось несколько строк кода, циклы и условия, теперь можно записать одним выражением в одной строке. Это и есть суть LINQ. В языке C# существует альтернативный вариант записи LINQ-выражений, который выглядит так:

List positive = (

                from x in a

                where x >= 0

                select x

                ).ToList ();

Как можно заметить, этот вариант во многом похож на синтаксис SQL-запросов. Что и не удивительно, поскольку этот вариант разрабатывался с учетом потребности в написании таких запросов архитекторами SQL-баз данных и специалистов, привыкших к подобному синтаксису. В своих проектах вы можете использовать любой из вариантов либо смешивать их друг с другом. Как показывает практика, каждый из них предпочтителен в разных конкретных случаях.

Рассмотренный пример задействует в себе механизм LINQ для объектов, который позволяет оперировать с коллекциями и другими стандартными наборами данных. Но кроме него огромное значение при работе с данными играют два других LINQ-провайдера: LINQ для SQL и LINQ для сущностей, которые призваны поддержать работу с двумя отдельными друг от друга ORM: LINQ для SQL и Entity Framework.

 

LINQ для SQL

LINQ для SQL является встроенным в .NET Framework механизмом, что отличает его от появившегося немного позднее Entity Framework. Благодаря этой встроенной поддержке и удобным инструментам мэппинга базы данных, LINQ для SQL получил очень широкое распространение. Способствовало этому также простота работы с ORM и низкий порог вхождения для любого разработчика. Кроме того, для создания ORM-базы данных с помощью LINQ для SQL не требуется писать ни строчки кода (хотя такая возможность и присутствует). В помощь разработчикам был предложен мастер создания модели данных, который формирует особый DBML-файл и файл с необходимыми для мэппинга классами. От разработчика требуется только указать подключение к БД и выбрать необходимые таблицы, все остальное мастер берет на себя (рис. 3.2 и 3.3).

Дальнейшее использование полученной модели данных столь же простое. Разработчику необходимо создать контекст базы данных, внести изменения и подтвердить их. Продемонстрируем это на примере:

protected void DoSomethingWithCustomer(Guid someCustomerld)

{

  MyDatabaseDataContext db = new MyDatabaseDataContext();

  var customer = db.Customers.SingleOrDefault(

              x => x.customerId == someCustomerId);

  if (customer != null)

  {

    customer.name = "Заказчик 1";

    db.SubmitChanges();

  }

}

Рис. 3.2. Окно создания модели LINQ для SQL

Рис. 3.3. Добавление в LINQ для SQL таблицы Customer

Здесь создается контекст базы данных MyDatabaseDataContext, ищется пользователь с определенным идентификатором someCustomerid, если он найден, у него меняется наименование на Заказчик 1 и изменения вносятся в базу данных вызовом метода Submitchanges. Такой же код, но с использованием механизма ADO.NET, приведен в следующем фрагменте. Вы сами можете сравнить оба способа по требуемому количеству кода и простоте:

protected void DoSomethingWithCustomer(Guid someCustomerId)

{

  using (SqlConnection conn = new SqlConnection(@"

         Data Source=localhost;

         Initial Catalog=BookMVC; Integrated Security=True"))

  {

    conn.Open();

    SqlCommand cmd = new SqlCommand(@"

      SELECT * FROM Customers

      WHERE customerId = @customerId", conn);

    cmd.Parameters.Add(

      new SqlParameter("customerId", someCustomerId));

    SqlDataReader reader = cmd.ExecuteReader();

    if (reader.Read())

    {

      SqlCommand updateCmd = new SqlCommand(@"

        UPDATE Customers SET name = @name

        WHERE customerId = @customerId", conn);

      updateCmd.Parameters.Add(

          new SqlParameter(''name", "Заказчик 1"));

      updateCmd.Parameters .Add(

          new SqlParameter("customerId", someCustomerId));

      updateCmd.ExecuteNonQuery();

    }

    reader.Close();

  }

}

Простота LINQ для SQL послужила его широкой распространенности. Но вместе с тем, в LINQ для SQL существуют и свои минусы. Во-первых, не так редки ситуации, когда неверно или некорректно построенное разработчиком LINQ-выражение приводит к потерям в производительности, к выборке лишних данных, к расходу памяти. Кроме того, в LINQ для SQL отсутствуют такие механизмы, как прозрачный мэппинг таблиц, связанных друг с другом через промежуточную таблицу отношением "многие-ко-многим". Многие разработчики на веб-сайтах, блогах и других сетевых ресурсах критиковали LINQ для SQL как ORM Framework за недостатки, как в производительности, так и в функциональном плане. Некоторые из них утверждают, что LINQ для SQL вовсе нельзя считать полноценным ORM Framework, справедливо замечая, что в нем нет возможности, например, создавать комплексные типы или производить наследование типов. По сути своей LINQ для SQL скорее представляет собой строго типизированное представление базы данных SQL Server, чем полноценный механизм ORM. Еще один недостаток LINQ для SQL заключается в его привязке к SQL Server, как к единственно возможному источнику данных. Безусловно, для ORM это ограничение является огромным недостатком, поскольку на рынке представлено свыше десятка разнообразных баз данных.

Трудно сказать, было ли это ответным шагом или так и планировалось, но Microsoft спустя некоторое время анонсировала новую разработку, "честную" ORM, Entity Framework.

 

Entity Framework

Выход финальной версии Entity Framework произошел одновременно с выходом Service Pack 1 для Visual Studio 2008 и Service Pack 1 для .NET Framework 3.5 в августе 2008 года. Ранее доступный для бета-тестирования как отдельный продукт, с выходом обновлений, Entity Framework стал частью .NET Framework и Visual Studio, которая предложила средства для визуального моделирования мэппинга базы данных.

Entity Framework — это "настоящая" ORM, в которой присутствуют все те вещи, которых не хватало в LINQ для SQL. Здесь и поддержка комплексных типов, наследования типов и возможность создания моделей для любой БД (через специальный провайдер). Entity Framework описывается специальным файлом в формате EDM, который включает в себя трехуровневую архитектуру: концептуальный слой, схему источника данных и слой мэппинга, что представляет собой значительно более гибкое решение, чем одноуровневый вариант DBML-файлов LINQ для SQL. Работа с Entity Framework так же базируется на механизме LINQ, называемом LINQ для сущностей, который во многом схож с LINQ для SQL. Вместе со значительно улучшенным функционалом Entity Framework стала более требовательной к разработчику. Так, в ней пропали некоторые механизмы LINQ для SQL, например, так называемый механизм ленивой загрузки (Lazy Loading) данных, когда данные автоматически подгружаются по мере надобности.

Далее приведен пример этого случая:

MyDatabaseDataContext db = new MyDatabaseDataContext();

var orders = db.Customers.First().Orders;

В случае с LINQ для SQL этот код вернет данные в виде набора заказов первого заказчика в наборе данных, тогда как Entity Framework данных не вернет. Связано это с тем, что в первом случае работает механизм Lazy Loading, который подгружает данные заказов при обращении к ним. Тогда как в Entity Framework разработчик должен сам определить код, который подгрузит данные. Для того чтобы получить данные в Entity Framework, необходимо прямо их затребовать:

MyDatabaseDataContext db = new MyDatabaseDataContext(); var customer = db.Customers.First(); customer.Orders.Load(); // загружаем данные var orders = customer.Orders;

Этот пример наглядно демонстрирует разнонаправленность двух механизмов 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 _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 Orders {

    get { return this._Orders; }

    set { this._Orders.Assign(value); }

  }

}

Полученный код примечателен тем, что класс Customer является partial-классом, а это значит, что мы можем легко расширить его, и все прочие классы, для поддержки наших интерфейсов. Создадим частичные классы для реализации интерфейсов на базе LINQ для SQL так, как показано в листинге 3.2.

Листинг 3.2. Частичные классы с реализацией интерфейсов

public partial class Customer : ICustomer {

  public Guid CustomerId {

    get { return customerId; }

    set { customerId = value; }

  }

  public string Name {

    get { return name; }

    set { name = value; }

  }

  public string Phone {

    get { return phone; }

    set { phone = value; }

  }

  public string Address {

    get { return address; }

    set { address = value; }

  }

}

public partial class Order : IOrder {

  public Guid OrderId {

    get { return orderId; }

    set { orderld = value; }

  }

  public Guid Customerld {

    get { return customerId; }

    set { customerId = value; }

  }

  public Guid ProductId {

    get { return productId; }

    set { productId = value; }

  }

  public int Count {

    get { return count; }

    set { count = value; }

  }

  public DateTime OrderDateTime {

    get { return orderDateTime; }

    set { orderDateTime = value; }

  }

}

public partial class Product : IProduct {

  public Guid ProductId {

    get { return productId; }

    set { productId = value; }

  }

  public string Name {

    get { return name; }

    set { name = value; }

  }

  public bool IsAvailable {

    get { return isAvailable; }

    set { isAvailable = value; }

  }

  public decimal Cost {

    get { return cost; }

    set { cost = value; }

  }

}

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

После реализации интерфейсов создадим простейшие хранилища и сервисы, для этого сначала объявим их интерфейсы:

public interface ICustomerRepository {

  ICustomer GetCustomerById(Guid customerId);

  IEnumerable GetCustomersByProduct(Guid productId);

}

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

public interface IOrderRepository {

  IOrder GetOrderById(Guid orderId);

  IEnumerable GetCustomerOrders(Guid customerId);

}

Хранилище для заказов позволит выбирать заказ по идентификатору и список заказов определенного заказчика.

public interface IProductRepository {

  IProduct GetProductById(Guid productId);

  IEnumerable GetAvailableProducts();

  IEnumerable GetProductListByName(string name);

}

Хранилище для товаров позволит найти товар по идентификатору, список товаров по наименованию и список товаров, которые доступны в данный момент.

Реализация данных хранилищ не составляет труда (листинг 3.3).

Листинг 3.3. Реализация хранилищ

public class CustomerRepository : ICustomerRepository {

  private readonly MyDatabaseDataContext _dataBase;

  public CustomerRepository(MyDatabaseDataContext db)

  {

    if (db == null)

      throw new ArgumentNullException("db");

    _dataBase = db;

  }

  public ICustomer GetCustomerById(Guid customerId)

  {

    if (customerId == Guid.Empty)

      throw new ArgumentException("customerId");

    return _dataBase.Customers

       .SingleOrDefault(x => x.customerId == customerId);

  }

  public IEnumerable GetCustomersByProduct(Guid productId) {

    if (productId == Guid.Empty)

      throw new ArgumentException("customerId");

    return _dataBase.Orders

      .Where(x => x.productId == productId)

      .Select(x => x.Customer).Distinct();

  }

}

public class OrderRepository : IOrderRepository {

  private readonly MyDatabaseDataContext _dataBase;

  public OrderRepository(MyDatabaseDataContext db)

  {

    if (db == null)

      throw new ArgumentNullException("db");

    _dataBase = db;

  }

  public IOrder GetOrderByld(Guid orderld)

  {

    if (orderId == Guid.Empty)

      throw new ArgumentException("orderId");

    return _dataBase.Orders

      .SingleOrDefault(x => x.orderId == orderId);

  }

  public IEnumerable GetCustomerOrders(Guid customerId)

  {

    if (customerId == Guid.Empty)

      throw new ArgumentException("customerId");

    return _dataBase.Orders

      .Where(x => x.customerId == customerId)

      .Select(x => x);

  }

}

public class ProductRepository : IProductRepository {

  private readonly MyDatabaseDataContext _dataBase;

  public ProductRepository(MyDatabaseDataContext db)

  {

    if (db == null)

      throw new ArgumentNullException("db");

    _dataBase = db;

  }

  public IProduct GetProductById(Guid productId)

  {

    if (productId == Guid.Empty)

      throw new ArgumentException("productId");

    return _dataBase.Products

      .SingleOrDefault(x => x.productId == productId);

  }

  public IEnumerable GetAvailableProducts()

  {

    return _dataBase.Products.Where(x => x.isAvailable)

      .Select(x => x);

  }

  public IEnumerable GetProductListByName(string name)

  {

    if (string.IsNullOrEmpty(name))

      throw new ArgumentException("name");

    return _dataBase.Products

      .Where(x => x.name.Contains(name))

      .Select(x => x);

  }

}

Далее создадим сервисы, которые будут помогать нам манипулировать данными. Сначала определим интерфейсы сервисов:

public interface ICustomerService {

  ICustomer Create(string name, string phone, string address);

  void Delete(ICustomer customer);

  void Update(ICustomer customer, string name,

             string phone, string address);

}

public interface IOrderService {

  IOrder Create(ICustomer customer, IProduct product, int count,

      DateTime orderDateTime);

  void Delete(IOrder order);

  void Update(IOrder order, ICustomer customer,

      IProduct product, int count, DateTime orderDateTime);

}

public interface IProductService {

  IProduct Create(string name, bool isAvailable, decimal cost);

  void Delete(IProduct product);

  void Update(IProduct product, string name,

          bool isAvailable, decimal cost);

Реализуем интерфейсы сервисов, как показано в листинге 3.4, для класса customerService. Чтобы уменьшить количество кода, приводить реализацию для других классов мы не будем, для остальных классов определяем методы Create, Delete и Update точно таким же образом.

Листинг 3.4. Реализация интерфейсов сервисов

public class CustomerService : ICustomerService {

  private readonly MyDatabaseDataContext _database;

  public CustomerService(MyDatabaseDataContext db)

  {

    if (db == null)

      throw new ArgumentNullException("db");

    _database = db;

  }

  public ICustomer Create(string name, string phone, string address)

  {

    if (String.IsNullOrEmpty(name))

      throw new ArgumentNullException("name");

    Customer customer = new Customer()

             {

               CustomerId = Guid.NewGuid(),

               Address = address,

               Phone = phone,

               Name = name

             };

    _database.Customers.InsertOnSubmit(customer);

    return customer;

  }

  public void Delete(ICustomer customer)

  {

    if (customer == null)

      throw new ArgumentNullException("customer");

    _database.Customers.DeleteOnSubmit((Customer)customer);

  }

  public void Update(ICustomer customer, string name,

                     string phone, string address)

  {

    if (customer == null)

      throw new ArgumentNullException("customer");

    if (String.IsNullOrEmpty(name))

      throw new ArgumentNullException("name");

    customer.Name = name;

    customer.Phone = phone;

    customer.Address = address;

  }

}

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

public class UnitOfWork : IDisposable {

  private readonly MyDatabaseDataContext _database;

  public MyDatabaseDataContext DataContext {

    get

    {

      return _database;

    }

  }

  private bool _disposed;

  public UnitOfWork()

  {

    _database = new MyDatabaseDataContext();

  }

  public void Commit()

  {

    _database.SubmitChanges();

  }

  public void Dispose()

  {

    Dispose(true); GC.SuppressFinalize(this);

  }

  private void Dispose(bool disposing) {

    if (!this._disposed)

    {

      if (disposing)

      {

        _database.Dispose();

      }

      _disposed = true;

    }

  }

}

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

 

Пример использования слоя данных

Наша инъекция кода полностью реализована, рассмотрим вариант использования:

using (UnitOfWork unitOfWork = new UnitOfWork())

{

  ICustomerService customerService = new

    CustomerService(unitOfWork.DataContext);

  IOrderService orderService = new

    OrderService(unitOfWork.DataContext);

  IProductService productService = new

    ProductService(unitOfWork.DataContext);

  ICustomer customer = customerService.Create("Hoвый заказчик",

        "111-22-33", "Адрес нового заказчика");

  IProduct product = productService.Create("Новый товар", true, 50000);

  orderService.Create(customer, product, 200, DateTime.Now);

  unitOfWork.Commit();

}

Обратите внимание, в приведенном коде нет ни одной зависимости, которая привязывала бы нас либо к конкретной базе данных, либо к конкретному механизму ORM. Для замены одного из этих элементов нам потребуется всего лишь реализовать служебные механизмы вроде класса unitofWork, хранилищ и сервисов, согласно требованиям, но не приведенный код. Любой код, написанный с использованием описанных ранее механизмов, не потребует модернизации.

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

 

Механизмы для работы с данными

 

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

 

XML-данные

.NET Framework предлагает широкий ассортимент объектов для доступа к XML-данным, которые расположены в стандартных сборках: System.Xml, System.Xml.XPath, System.Xml.Xsl, System.Xml.Schema и System.Xml.Linq. Рассмотрим назначение и наиболее полезные классы каждой из сборок:

□ System.Xml — содержит базовые классы для работы с XML, такие как

XmlDocument, XmlElement, XmlNode и множество других, которые позволяют реализовать загрузку из файлов, обработку, добавление, изменение, удаление XML-данных;

□ System.Xml.XPath — содержит классы для реализации работы механизма XPath, позволяющего писать выражения к XML-документу для поиска необходимых данных;

□ System.Xml.Xsl — содержит классы для поддержки реализации XSLT-преобразований XML-документа;

□ System.Xml.Schema — содержит классы для поддержки XSD-схем и валидации XML-данных на их основе. Содержит большое число классов, позволяющих создавать XSD-схемы и использовать их;

□ System.Xml.Lin —  последняя сборка, которая недавно появилась в .NET Framework. Содержит классы, реализующие механизм доступа к XML-данным на основе LINQ-выражений. Этот механизм носит собственное название LINQ для XML. Он позволяет использовать уже известные вам LINQ-выражения для обработки XML-данных.

Рассмотрим пример обработки XML-данных с помощью LINQ для XML. Предположим, что у нас есть следующий XML-файл:

 

   

      Владимир Иванов

      yn. CTpoMTanefr

      12

      23

      MocKBa

      100888

      Россия

   

 

 

   

      Сергей Петров

      ул. Бажова

      76

      123

      Eкатеринбург

      620000

      Россия

   

 

Для получения имени по номеру ордера, используя LINQ для XML, мы можем написать следующий код:

XDocument xdoc =

XDocument.Load("D:\\CPS\\#Projects\\MVCBook\\MVCBook\\Order.xml");

IEnumerable orders =

  xdoc.Element("Orders").Descendants("Order");

int orderId = 100;

IEnumerable order =

  orders.Where(x => x.Attribute("OrderId").Value == orderId.ToString());

XElement address = order.Select(x => x.Element("Address"))

  .FirstOrDefault();

string name = address.Element("Name").Value;

В данном примере загружается файл orders.xml, в переменной orders присваиваются все заявки (элементы Order в XML-файле). Затем с помощью LINQ-запроса находится заявка с идентификатором 100. После этого выбирает элемент адреса заявки и, в завершение, из адреса извлекается имя.

Стоит отметить, что в данном примере не производились проверки возвращаемых значений на null, что на практике делать обязательно.

 

Работа с данными через ADO.NET

Понятие ADO.NET на платформе .NET достаточно широкое. На самом деле LINQ для SQL и LINQ для XML, а также все остальные стандартные LINQ-реализации входят в механизм ADO.NET как составные части. Но исторически ADO.NET развивалось от версии к версии платформы через другие механизмы, такие как объекты Dataset.

Dataset — это объект, содержащий кэш данных, загруженных из источника данных. По сути Dataset — это набор объектов типа DataTable, представляющих собой таблицы данных, и DataRelation, которые описывают связи между объектами DataTable.

Практически весь функционал по работе с данными через Dataset расположен в .NET Framework в пространствах имен System.Data и System.Data.OleDb (кроме этого, существует еще более двух десятков пространств имен, название которых начинается с System.Data). Перечислим основные самые важные классы, которые используются чаще всего при работе с Dataset из System.Data:

□ DataSet, DataTable, DataColumn, DataRow — различные варианты представления данных (набор, таблица, схема колонки, строка данных);

□ ConstraintCollection, Constraint, UniqueConstraint — представляют ограничения, которые могут быть наложены на объекты DataColumn.ConstraintCollection содержит набор таких ограничений для объекта DataTable;

□ DataView — объект, позволяющий привязывать данные из DataTable к формам WinForms или WebForms;

Список наиболее часто используемых классов пространства имен System.Data.OleDb:

□ OleDbConnection — обеспечивает подключение к базе данных через механизм OLE DB;

□ oleDbCommand — содержит запрос к базе данных на языке SQL либо представляет хранимую процедуру;

□ oleDataAdapter — обеспечивает заполнение объекта DataSet нужными данными с помощью элементов OieDbCommand;

□ oleDbDataReader — позволяет читать данные, полученные от oieDataAdapter, в виде строк и в прямом порядке;

□ oleDbTransaction — представляет собой транзакцию в источнике данных.

Существует еще одно часто используемое пространство имен, которое может быть полезно при разработке баз данных на SQL Server — System.Data.SqlClient. Это пространство имен содержит весь функционал System.Data.OleDb, только в реализации для использования исключительно с SQL Server. Применение этого пространства имен позволяет получить доступ к особенностям SQL Server, таким как новые типы данных SQL Server 2008.

Рассмотрим пример доступа к данным посредством базовых механизмов ADO.NET DataSet:

SqlConnection conn = new

  SqlConnection("Data Source=localhost,

      Initial Catalog=BookMVC, Integrated Security=True");

conn.Open();

SqlCommand cmd = new SqlCommand("SELECT * FROM Customers", conn);

SqlDataReader reader = cmd.ExecuteReader();

string phone;

while (reader.Read())

{

  string name = reader["name"].ToString();

  if (name == "Сергей Петров")

  {

    phone = reader["phone"].ToString();

    break;

  }

}

reader.Close();

conn.Close();

Данный пример инициализирует строку соединения с базой данных и открывает соединение. Затем создает команду в виде SQL-запроса на чтение всех записей из таблицы Customers (заказчики). С помощью ExecuteReader команда выполняется, и для работы с данными строится объект SqlDataReader. После этого формируется цикл, который проходит по всем записям, ищет первую запись с именем Сергей Петров и прерывает цикл, сохраняя данные о телефоне в локальной переменной.

Для сравнения перепишем этот простой пример, используя LINQ для Dataset:

SqlConnection conn = new SqlConnection(@"

Data Source=localhost;

Initial Catalog=BookMVC;

Integrated Security=True");

conn.Open();

SqlCommand cmd = new SqlCommand("SELECT * FROM Customers", conn);

DataSet ds = new DataSet();

SqlDataAdapter adapter = new SqlDataAdapter(cmd);

adapter.Fill(ds);

DataTable customers = ds.Tables[0];

string phone = customers. AsEnumerable()

  .Where(x => x.Field("name") == "Сергей Петров")

  .Select(x => x.Field("phone")).SingleOrDefault();

conn.Close();

Как нетрудно заметить, наш цикл, нацеленный на поиск данных, исчез, и ему на замену пришло LINQ-выражение, которое выполняет точно ту же логику — ищет телефон по определенному номеру.

Механизм доступа к данным через ADO.NET Dataset в общем случае производительнее, чем через ORM, вроде LINQ для SQL или Entity Framework, поскольку при работе с объектами типа Dataset нет затрат на реализацию объектной модели базы данных. Работа с данными происходит напрямую через SQL-запросы или вызов хранимых процедур. Разработчик сам контролирует весь процесс получения и использования данных, что дает больше возможностей и прирост производительности, но с другой стороны, увеличивает объем написания необходимого кода.

Поскольку основная часть времени при запросе тратится на его выполнение, то использование ADO.NET вместо популярных ORM для доступа к данным оправдано только там, где ставятся повышенные требования к потреблению памяти и производительности. В большинстве же случаев затраты на ORM окупаются скоростью и простотой разработки, единой моделью доступа к данным и меньшему количеству кода.

 

LINQ для SQL

Работа с LINQ для SQL на платформе .NET Framework осуществляется с помощью классов пространства имен System.Linq и System.Data.Linq.

Перечислим основные классы этих пространств имен.

□ System.Linq:

• Enumerable — предоставляет набор статичных методов для работы с объектами, реализующими интерфейс IEnumerable.

• Queryable — предоставляет набор статичных методов для работы с объектами, реализующими интерфейс IQueryable.

Методы этих классов, вроде Where, Select, Sum и др., используются в любом LINQ-выражении для построения запросов, обработки и фильтрации данных.

□ System.Data.Linq:

• DataContext — основной объект для работы с LINQ для SQL, предоставляет контекст базы данных, через который осуществляется доступ ко всем сущностям базы данных;

• EntitySet, EntityRef (структура) — обеспечивают связь между сущностями в LINQ для SQL;

• Table — представляет таблицу с возможностью изменения объектов;

• CompiledQuery — предоставляет возможность компилировать и повторно использовать запросы.

Контекст базы данных, наследующий от класса DataContext, в LINQ для SQL принято создавать с помощью мастера, который автоматически сгенерирует LINQ для SQL-классов. После такой генерации работа с объектной моделью становится очень простой, например, приведенный в разделе ADO.NET пример в исполнении LINQ для SQL будет выглядеть так:

using (MyDatabaseDataContext dataContext = new MyDatabaseDataContext())

{

  string phone = dataContext.Customers

    .Where(x => x.name == "Сергей Петров")

    .FirstOrDefault().phone;

}

Согласитесь, это заметно более простое решение по сравнению с вариантом, написанным с использованием ADO.NET Dataset.

 

Entity Framework

Entity Framework можно использовать только на .NET Framework версии 3.5 с установленным пакетом обновления SP1. Для работы с Entity Framework предлагаются следующие пространства имен: System.Data.Entities, System.Data.Objects, System.Data.EntityClient и др.

В отличие от LINQ для SQL модель данных Entity Framework (EDM) состоит из трех частей:

□ концептуальная модель (CSDL) — позволяет создавать сущности, не равнозначные сущностям базы данных, например, комплексные сущности, состоящие из элементов нескольких таблиц, или сущности, наследующие от других сущностей;

□ модель хранения данных (SSDL) — определяет логическую модель базы данных;

□ модель сопоставления хранения данных и концептуальной модели (MSL) — определяет, как логическая модель хранения базы данных сопоставляется с концептуальной моделью.

При работе с Entity Framework в Visual Studio 2008 SP1 предлагается мастер автоматического создания модели на базе заданных объектов базы данных. Вы можете использовать его для генерации всех трех частей EDM. Результатом работы мастера станет файл *.edmx, который будет содержать все три модели сразу. Контекст базы данных и отображение на классы C# будут сгенерированы в другой файл *.Designer.cs.

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

Другим отличием Entity Framework от LINQ для SQL является разнообразие доступа к модели данных. Существует три варианта работы с EDM:

□ LINQ для сущностей — аналог LINQ для SQL с полной поддержкой всех особенностей Entity Framework;

□ Entity SQL — особенный язык, диалект SQL, который служит для работы с моделью EDM. Имеет ряд отличий и ограничений по сравнению с обычным SQL;

□ третий вариант совмещает в себе первые два, с помощью LINQ-выражений можно строить запросы на языке Entity SQL.

Подробное описание особенностей Entity Framework или Entity SQL выходит за рамки этой книги. Здесь мы приведем только очевидные отличия LINQ для сущностей от LINQ для SQL:

□ поддержка прозрачного мэппинга отношений "многие-ко-многим" в LINQ для сущностей. LINQ для SQL строго отображает структуру базы данных, поэтому промежуточная таблица также будет отображена, и ее потребуется использовать при работе с такими данными;

□ отсутствие в LINQ для сущностей поддержки методов Single и singleOrDefault, вместо которых рекомендуется использовать First и FirstOrDefault;

□ вместо методов DeleteOnSubmit и SubmitChanges в LINQ для сущностей предложены методы DeleteObject и saveChanges соответственно;

□ вместо метода XXX.InsertOnSubmit предложены обертки (автогенерируемые) AddToXXX, где XXX — это имя отображаемой сущности (таблицы).

Еще одно незначительное отличие в процессе автогенерации моделей с помощью мастеров в Visual Studio представляет собой изменение имени таблицы при отображении на класс в LINQ для SQL. Например, таблица Customers отобразится на класс Customer, без последней буквы "s". При создании классов в Entity Framework мастер не производит такие изменения.

 

NHibernate

Еще одним вариантом организации доступа к данным может стать популярная ORM-библиотека NHibernate — довольно старый механизм, портированный на платформу .NET Framework из Java-проекта Hibernate. Как ORM, Hibernate давно заслужила право называться зрелой, гибкой, мощной и главное производительной библиотекой. Адаптация под .NET, хотя и не совсем полностью реализует функционал Hibernate версии 3, но предлагает все тот же механизм, сравнимый по мощности и производительности с предком.

NHibernate, как и другие ORM, использует для реализации доступа к данным мэппинг в виде XML-файла. Этот файл должен быть оформлен согласно схеме nhibernate-mapping.xsd, которая идет в комплекте поставки. По традиции названия всех таких файлов мэппинга формируются как class_name.hbm.xml. Такой файл может выглядеть примерно так:

  namespace="MyNamespace" assembly="MyNamespace">

 

   

   

 

 

 

 

Здесь создается мэппинг класса Customer на таблицу Customers в базе данных, которая содержит ряд полей: customerId, name, phone, address. Для подключения к базе данных должен быть создан другой конфигурационный файл, похожий на этот:

 

   

      NHibernate.Connection.DriverConnectionProvider

    

   

      NHibernate.Driver.SqlClientDriver

    

   

      NHibernate.Dialect.MsSql2005Dialect

    

   

      Server=(local);

      Initial Catalog=MyDatabase;

      Integrated Security=SSPI;

    

   

       assembly="MyNamespace" />

 

К сожалению, для NHibernate отсутствует встроенная автоматизация генерации отображаемого кода и модели данных, а также файлов конфигурации. Поэтому описание модели ложится на плечи разработчика. С одной стороны, это рутинные операции, которые машина сделает лучше, но с другой, такой подход предоставляет разработчику больший простор для реализации его идей. Впрочем, в Интернете есть проекты с открытым исходным кодом, призванные облегчить и этот и другие процессы при работе с NHibernate:

□ Fluent NHibernate (http://fluentNHibernate.org/);

□ MyGeneration (http://www.mygenerationsoftware.com/portal/default.aspx);

□ NHibernate 1.2 Generator (http://gennit.com/);

□ NHibernate Query Generator

(http://ayende.com/projects/downloads/NHibernate-query-generator.aspx);

□ NHibernate Entity Generator (http://www.softpedia.com/get/Programming/Other-Programming-Files/NHibernate-Entity-Generator.shtml);

□ NHibernate Helper Kit (http://www.codeproject.com/KB/dotnet/NHibernate_Helper_Kit.aspx);

□ многие другие (http://stackoverflow.com/questions/41752/nhibernate-generators).

Для работы с NHibernate используются пространства имен NHibernate и NHibernate.Cfg. Второе служит для загрузки файла конфигурации, например, как показано в следующем примере:

ISessionFactory sessionFactory = new Configuration()

  .Configure("Nhibernate.cfg.xml").BuildSessionFactory();

где Nhibernate.cfg.xml — ваш файл конфигурации.

NHibernate основывается на объектах ISession, которые представляют собой пользовательскую сессию подключения к базе данных, поэтому для работы с данными необходимо создать экземпляр ISession. В качестве примера работы с NHibernate можно привести такой шаблон кода:

ISession session;

ITransaction tran;

try

{

  session = sessionFactory.OpenSession();

  tran = session.BeginTransaction();

  // делаем какую-то работу

  tran.Commit();

  session.Close();

}

catch (Exception ex)

{

  tran.Rollback();

  session.Close();

}

Как вы можете заметить, NHibernate реализует механизм транзакций с возможностью откатывать ненужные изменения.

Работа с объектами NHibernate возможна с помощью одного из трех вариантов:

□ Hibernate Query Language (HQL) — языка во многом похожего на язык LINQ c SQL-синтаксисом;

□ Query By Criteria (QBC) — объектный вариант, который позволяет строить выражения запросов динамически через объектно-ориентированный API;

□ через обыкновенные SQL-запросы.

Приведем простой пример использования HQL:

string result = (string)session.createQuery(@"select phone

         from Customer as c

         where c.name = 'Сергей Иванов'").UniqueResult();

Пример вернет телефон заказчика по его имени. Перепишем этот пример с использованием Query By Criteria:

ICriteria crit = session.CreateCriteria(typeof(Customer));

crit.Add(Expression.Eq("name", "Сергей Иванов"));

crit.SetMaxResults(1);

string phone = (string) crit.UniqueResult();

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

Кроме того, в будущих версиях NHibernate может появиться полноценная поддержка LINQ. По крайней мере, частичная реализация уже существует в NHibernate Contrib (https://nhcontrib.svn.sourceforge.net/svnroot/nhcontrib/trunk/). Эта возможность позволит оперировать NHibernate-объектами в привычном для LINQ-разработчиков стиле и, наверняка, увеличит привлекательность библиотеки.

 

Сравнение механизмов доступа к данным

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

□ удобство и единообразие доступа к данным;

□ объем кода, который может стать источником ошибок;

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

И, хотя понятие простоты субъективно, все же мы можем попытаться оценить описанные технологии по простоте работы:

□ на первом месте LINQ для SQL, как простой, но все-таки эффективный Framework для отображения структуры базы данных на код;

□ NHibemate и Entity Framework на втором месте по простоте, как механизмы схожие во многом с LINQ для SQL, но все-таки в силу своей комплексности и обширным возможностям более сложны при построении слоя доступа к данным;

□ более сложным вариантом построения механизма доступа к данным является использование ADO.NET либо других методов, вроде прямого доступа к XML-файлам. Этот вариант требует поддержки большого объема самописного кода, большого внимания к его написанию, он потенциально более незащищен в связи с возможными уязвимостями.

 

Рекомендации по выбору механизма доступа к данным

Используйте низкоуровневые механизмы, вроде ADO.NET, в тех проектах, где скорость доступа к данным — это основная задача, и главное требование к проекту — высокая производительность. Для проектов, ориентированных на SQL Server, которые не предполагают высоконагруженной работы с данными или не содержат сложной структуры базы данных, вполне возможно использовать LINQ для SQL. В случае, когда простоты LINQ для SQL не хватает, либо используется база данных, отличная от SQL Server, хорошим решением станет один из ORM Entity Framework или NHibernate, в зависимости от ваших пристрастий и предпочтений.

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