Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
CheckBootSpeed - это диагностический пакет на основе скриптов PowerShell, создающий отчет о скорости загрузки Windows 7 ...
Вы когда-нибудь хотели создать установочный диск Windows, который бы автоматически установил систему, не задавая вопросо...
Если после установки Windows XP у вас перестала загружаться Windows Vista или Windows 7, вам необходимо восстановить заг...
Программа подготовки документов и ведения учетных и отчетных данных по командировкам. Используются формы, утвержденные п...
Red Button – это мощная утилита для оптимизации и очистки всех актуальных клиентских версий операционной системы Windows...
OSzone.net Microsoft Разработка приложений .NET Framework Встраивание RavenDB в приложение ASP.NET MVC 3 RSS

Встраивание RavenDB в приложение ASP.NET MVC 3

Текущий рейтинг: 5 (проголосовало 1)
 Посетителей: 1823 | Просмотров: 4411 (сегодня 0)  Шрифт: - +

Внимание к движению NoSQL в сообществе Microsoft .NET Framework растет по мере того, как мы все чаще слышим о компаниях, которые делятся своим опытом реализации в известных нам и используемых нами приложениях. С ростом осведомленности возникает желание покопаться и понять, как хранилище данных NoSQL могло бы дать преимущества или другие потенциально возможные решения для программного обеспечения, разрабатываемого нами в настоящее время. Но откуда начинать и насколько NoSQL труден в изучении? Или даже более актуальная обеспокоенность: сколько времени и усилий потребуется на создание нового решения в области хранилищ данных и подготовку к написанию кода, который мог бы работать с этим решением? В конце концов, вы ведь уже досконально владеете процессом подготовки SQL Server для разработки нового приложения, правильно?

Донеслась весть до .NET-сообщества о новом варианте реализации уровня данных по типу NoSQL. RavenDB (ravendb.net) — база данных документов, спроектированная для платформы .NET/Windows и упакованная всем, что нужно для того, чтобы начать работу с нереляционным хранилищем данных. RavenDB хранит документы в формате JSON без схем. Для прямого взаимодействия с хранилищем предусмотрен RESTful API, но настоящее преимущество дает API .NET-клиента. Он реализует шаблон Unit of Work и использует синтаксис LINQ для работы с документами и запросами. Если вы имели дело с каким-либо ORM-средством (object-relational mapper), например Entity Framework (EF) или NHibernate, или если применяли WCF-сервис данных, то почувствуете себя как дома благодаря архитектуре этого API для работы с документами в RavenDB.

Обучение работе с экземпляром RavenDB не требует много времени. По сути, больше времени уйдет на планирование лицензионной стратегии (хотя и оно минимально). RavenDB предлагает лицензию открытого исходного кода для проектов с открытым исходным кодом, но для коммерческих проектов с закрытым исходном кодом нужна коммерческая лицензия. Детали лицензирования и ценовой политики см. по ссылке ravendb.net/licensing. На этом сайте утверждается, что для начинающих компаний и тех, кто намерен использовать продукт в некоммерческих проектах с закрытым исходном кодом, доступно бесплатное лицензирование. В любом случае имеет смысл вкратце рассмотреть доступные варианты, чтобы понять долгосрочный потенциал реализации до того, как приступать к созданию прототипа или к разработке в изолированной программной среде («песочнице»).

Встраиваемая RavenDB и MVC

RavenDB может работать в трех режимах:

  • как Windows-служба;
  • как IIS-приложение;
  • встроенная в .NET-приложение.

Первые два режима не требуют сложного процесса установки, но создают некоторые издержки стратегии реализации. Третий вариант, встроенный, крайне прост в подготовке. На самом деле для этого существует NuGet-пакет. Вызов следующей команды в Package Manager Console в Visual Studio 2010 (или поиск термина «ravendb» в диалоге Manage NuGet Packages) предоставит вам все ссылки, необходимые для того, чтобы приступить к работе со встроенной версией RavenDB:

Install-Package RavenDB-Embedded

Подробное описание этого пакета вы найдете в галерее NuGet по ссылке bit.ly/ns64W1.

Донеслась весть до .NET-сообщества о новом варианте реализации уровня данных по типу NoSQL.

Для включения встраиваемой версии RavenDB в приложение ASP.NET MVC 3 достаточно добавить пакет через NuGet и указать каталог для файлов хранилища данных. Поскольку у приложений ASP.NET есть известный каталог данных в инфраструктуре — App_Data и поскольку большинство компаний-хостеров предоставляет доступ для чтения и записи в этот каталог с минимальными изменениями в конфигурации (или вообще без изменений), это хорошее место для хранения файлов данных. Когда RavenDB создает свое файловое хранилище, она формирует целый набор каталогов и файлов по заданному пути. То есть она не создает один каталог верхнего уровня для хранения всего и вся. Зная это, имеет смысл добавить ASP.NET-папку с именем App_Data через контекстное меню Project в Visual Studio 2010, а затем создать подкаталог в каталоге App_Data для данных RavenDB (рис. 1).

*
Рис. 1. Структура каталогов в App_Data

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

При работе с хранилищем данных через RavenDB Client API нужно создать и инициализировать экземпляр объекта, реализующего интерфейс Raven.Client.IDocumentStore. В этом API два класса: DocumentStore и EmbeddedDocumentStore; они реализуют этот интерфейс и могут использоваться в зависимости от режима, в котором выполняется RavenDB. В течение жизненного цикла приложения должен существовать только один экземпляр на каждое хранилище данных. Я могу создать класс для управления единственным соединением со своим хранилищем документов, которое позволит мне обращаться к экземпляру объекта IDocumentStore через статическое свойство; кроме того, в нем будет статический метод для инициализации экземпляра (рис. 2).

Рис. 2. Класс для DocumentStore

public class DataDocumentStore
{
  private static IDocumentStore instance;
  public static IDocumentStore Instance
  {
    get
    {
      if(instance == null)
       throw new InvalidOperationException(
          "IDocumentStore has not been initialized.");
      return instance;
    }
  }
  public static IDocumentStore Initialize()
  {
    instance = new EmbeddableDocumentStore {
      ConnectionStringName = "RavenDB" };
    instance.Conventions.IdentityPartsSeparator = "-";
    instance.Initialize();
    return instance;
  }
}

Аксессор get статического свойства проверяет закрытое статическое поле на наличие пустого объекта и, если он пуст (null), генерирует InvalidOperationException. Я генерирую здесь исключение вместо вызова метода Initialize, чтобы код оставался безопасным в многопоточной среде (thread-safe). Если бы свойству Instance было разрешено делать этот вызов и приложение ссылалось бы на это свойство для выполнения инициализации, тогда нельзя было бы исключить вероятность того, что к приложению одновременно обратится не один пользователь, а это привело бы к одновременным вызовам метода Initialize. В логике метода Initialize я создаю новый экземпляр Raven.Client.Embedded.EmbeddableDocumentStore и записываю в свойство ConnectionStringName имя строки подключения, добавленной в файл web.config file при установке NuGet-пакета RavenDB. В web.config я записываю значение строки подключения по синтаксису, понятному RavenDB, чтобы сконфигурировать его на использование встроенной локальной версии хранилища данных. Я также сопоставляю каталог файлов с каталогом Database, который я создал в каталоге App_Data проекта MVC:

<connectionStrings>
<add name="RavenDB " connectionString="DataDir =
    ~\App_Data\Database" />
</connectionStrings>

Интерфейс IDocumentStore содержит все методы для работы с хранилищем данных. Я получаю и сохраняю объект EmbeddableDocumentStore как экземпляр интерфейсного типа IDocumentStore, чтобы у меня была возможность переключаться на создание экземпляра объекта EmbeddedDocumentStore в серверной версии (DocumentStore), если мне понадобится отказаться от встроенной версии. Тем самым весь код моей логики, который обеспечивает управление объектами документов, будет избавлен от необходимости знать, в каком режиме выполняется RavenDB.

По умолчанию RavenDB будет создавать идентификационные ключи документов в формате, подобном REST. Объект Item получил бы ключ в формате «items/104». Имя объектной модели переводится в буквы нижнего регистра и во множественное число, а после прямого слеша добавляется уникальное идентификационное число при каждом создании нового документа. Это может вызвать проблемы в MVC-приложении, так как прямой слеш приведет к разбору нового параметра маршрута (route parameter). RavenDB Client API предоставляет возможность заменять прямой слеш заданием значения IdentityPartsSeparator. Чтобы избежать этой проблемы с маршрутизацией, в моем методе DataDocumentStore.Initialize перед вызовом метода Initialize для объекта EmbeddableDocumentStore указывается тире с помощью значения IdentityPartsSeparator.

Добавление вызова статического метода DataDocumentStore.Initialize в метод Application_Start в файле Global.asax.cs моего MVC-приложения настраивает экземпляр IDocumentStore при первом запуске приложения:

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();
  RegisterGlobalFilters(GlobalFilters.Filters);
  RegisterRoutes(RouteTable.Routes);
  DataDocumentStore.Initialize();
}

Обучение работе с экземпляром RavenDB не требует много времени.

Отсюда я могу использовать объект IDocumentStore со статическим вызовом свойства DataDocumentStore.Instance, чтобы работать с объектами документов из встроенного в MVC-приложение хранилища данных.

Объекты RavenDB

Чтобы лучше понять, как работает RavenDB, я создам приложение-прототип для хранения закладок и управления ими. RavenDB рассчитан на работу с POCO-объектами (Plain Old CLR Objects), поэтому для их сериализации нет нужды добавлять атрибуты свойств. Создание класса, представляющего закладку, осуществляет довольно прямолинейно. На рис. 3 показан класс Bookmark.

Рис. 3. Класс Bookmark

public class Bookmark
{
  public string Id { get; set; }
  public string Title { get; set; }
  public string Url { get; set; }
  public string Description { get; set; }
  public List<string> Tags { get; set; }
  public DateTime DateCreated { get; set; }
  public Bookmark()
  {
    this.Tags = new List<string>();
  }
}

При сохранении документа RavenDB сериализует данные этого объекта в JSON-структуру. Для операций с идентификационным ключом документа используется общеизвестное именованное свойство Id. RavenDB создает это значение — при условии, что свойство Id пустое или содержит null на момент вызова для создания нового документа, — и сохраняет его в элементе @metadata документа (он используется для операций с ключом документа на уровне хранилища данных). При запросе документа RavenDB Client API присваивает идентификационный ключ документа свойству Id в момент загрузки объекта документа.

Для включения встраиваемой версии RavenDB в приложение ASP.NET MVC 3 достаточно добавить пакет через NuGet и указать каталог для файлов хранилища данных.

JSON-сериализация документа-примера Bookmark представляется следующей структурой:

{
"Title": "The RavenDB site",
  "Url": "http://www.ravendb.net",
  "Description": "A test bookmark",
  "Tags": ["mvc","ravendb"],
  "DateCreated": "2011-08-04T00:50:40.3207693Z"
}

Класс Bookmark будет нормально работать с хранилищем документов, но свойство Tags создаст проблему на уровне UI. Мне нужно, чтобы пользователь вводил список разделяемых запятыми тегов в одно текстовое поле, а средство связывания модели MVC сопоставляло все поля данных безо всякого кода логики в представлениях или операциях контроллера. Для этого я использую собственное средство связывания модели (model binder) и сопоставляю поле формы с именем TagsAsString с полем Bookmark.Tags. Для начала я создам класс собственного средства связывания модели (рис. 4).

Рис. 4. BookmarkModelBinder.cs

public class BookmarkModelBinder : DefaultModelBinder
{
  protected override void OnModelUpdated(ControllerContext
    controllerContext, ModelBindingContext bindingContext)
  {
    var form = controllerContext.HttpContext.Request.Form;
    var tagsAsString = form["TagsAsString"];
    var bookmark = bindingContext.Model as Bookmark;
    bookmark.Tags = string.IsNullOrEmpty(tagsAsString)
      ? new List<string>()
      : tagsAsString.Split(',').Select(i => i.Trim()).ToList();
  }
}

Затем я обновляю файл Globals.asax.cs, чтобы добавить BookmarkModelBinder к средствам связывания модели при запуске приложения:

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();
  RegisterGlobalFilters(GlobalFilters.Filters);
  RegisterRoutes(RouteTable.Routes);
  ModelBinders.Binders.Add(typeof(Bookmark),
    new BookmarkModelBinder());
  DataDocumentStore.Initialize();
}

Для обработки заполнения текстового поля HTML текущими тегами в модели я добавлю метод расширения, который преобразует объект List<string> в строку с разделителями в виде запятых:

public static string ToCommaSeparatedString(
this List<string> list)
{
  return list == null ? string.Empty : string.Join(", ", list);
}

Шаблон Unit of Work

RavenDB Client API основан на шаблоне Unit of Work. Для работы с документами из хранилища вы должны сначала открыть новый сеанс, выполнить операции, сохранить результаты и закрыть сеанс. Сеанс отслеживает изменения и действует по аналогии с контекстом данных в EF. Вот пример создания нового документа:

using (var session = documentStore.OpenSession())
{
  session.Store(bookmark);
  session.SaveChanges();
}

Оптимально поддерживать сеанс в течение обработки HTTP-запроса, чтобы он мог отслеживать изменения, использовать кеш первого уровня и т. д. Я создам базовый контроллер, который будет открывать новый сеанс для выполняемой операции с помощью DocumentDataStore.Instance, сохранять изменения, а затем освобождать объект сеанса (рис. 5). Это позволяет выполнять всю работу при выполнении кода операции с использованием одного открытого экземпляра сеанса.

Рис. 5. BaseDocumentStoreController

public class BaseDocumentStoreController : Controller
{
  public IDocumentSession DocumentSession { get; set; }
  protected override void OnActionExecuting(
    ActionExecutingContext filterContext)
  {
    if (filterContext.IsChildAction)
      return;
    this.DocumentSession =
      DataDocumentStore.Instance.OpenSession();
    base.OnActionExecuting(filterContext);
  }
  protected override void OnActionExecuted(
    ActionExecutedContext filterContext)
  {
    if (filterContext.IsChildAction)
      return;
    if (this.DocumentSession != null &&
        filterContext.Exception == null)
      this.DocumentSession.SaveChanges();
    this.DocumentSession.Dispose();
    base.OnActionExecuted(filterContext);
  }
}

Реализация контроллера и представления MVC

Операции BookmarksController имеют дело непосредственно с объектом IDocumentSession из базового класса и управляют всеми CRUD-операциями (Create, Read, Update, Delete) над документами. На рис. 6 показан код для контроллера закладок.

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

Рис. 6. Класс BookmarksController

public class BookmarksController : BaseDocumentStoreController
{
  public ViewResult Index()
  {
    var model = this.DocumentSession.Query<Bookmark>()
      .OrderByDescending(i => i.DateCreated)
      .ToList();
    return View(model);
  }
  public ViewResult Details(string id)
  {
    var model = this.DocumentSession.Load<Bookmark>(id);
    return View(model);
  }
  public ActionResult Create()
  {
    var model = new Bookmark();
    return View(model);
  }
  [HttpPost]
  public ActionResult Create(Bookmark bookmark)
  {
    bookmark.DateCreated = DateTime.UtcNow;
    this.DocumentSession.Store(bookmark);
    return RedirectToAction("Index");
  }
  public ActionResult Edit(string id)
  {
   var model = this.DocumentSession.Load<Bookmark>(id);
   return View(model);
  }
  [HttpPost]
  public ActionResult Edit(Bookmark bookmark)
  {
    this.DocumentSession.Store(bookmark);
    return RedirectToAction("Index");
  }
  public ActionResult Delete(string id)
  {
    var model = this.DocumentSession.Load<Bookmark>(id);
    return View(model);
  }
  [HttpPost, ActionName("Delete")]
  public ActionResult DeleteConfirmed(string id)
  {
    this.DocumentSession.Advanced.DatabaseCommands.Delete(
      id, null);
    return RedirectToAction("Index");
  }
}

Метод IDocumentSession.Query<T> в операции Index возвращает объект результата, который реализует интерфейс IEnumerable, поэтому я могу использовать LINQ-выражение OrderByDescending для сортировки элементов и вызвать метод ToList для захвата данных в свой возвращаемый объект. Метод IDocumentSession.Load в операции Details принимает значение идентификационного ключа документа и десериализует соответствующий документ в объект типа Bookmark.

Метод Create с атрибутом HttpPost задает свойство CreateDate в элементе-закладке и вызывает метод IDocumentSession.Store из объекта сеанса, чтобы добавить запись о новом документе в хранилище. Метод Update с атрибутом HttpPost тоже может вызывать метод IDocumentSession.Store, так как значение Id у объекта Bookmark уже будет установлено. RavenDB распознает Id и обновляет существующий документ с соответствующим ключом вместо создания нового документа. Операция DeleteConfirmed вызывает метод Delete из объекта IDocumentSession.Advanced.DatabaseCommands, что позволяет удалять документ по ключу без предварительной загрузки этого объекта. Во всех этих операциях мне не требуется вызывать метод IDocumentSession.SaveChanges, поскольку базовый контроллер осуществляет такой вызов в текущей выполняемой операции.

Все представления достаточно просты. Они могут быть строго типизированы классом Bookmark в разметках Create, Edit и Delete и списком закладок в разметке Index. Каждое представление может напрямую ссылаться на свойства модели, относящиеся к полям ввода и отображаемым полям. Единственное место, где мне придется варьировать ссылку на свойство объекта, — поле ввода для тегов. Я буду использовать метод расширения ToCommaSeparatedString в представлениях Create и Edit со следующим кодом:

@Html.TextBox("TagsAsString", Model.Tags.ToCommaSeparatedString())

Это даст возможность пользователю вводить и редактировать связанные с закладкой теги в формате с разделением тегов запятыми, причем делать все это в одном текстовом поле.

Поиск объектов

Теперь можно переключить внимание на добавление одной из последних частей функциональности: возможность фильтрации списка закладок по тегам. Объект, возвращаемый методом IDocumentSession.Query реализует не только интерфейс IEnumerable, но и .NET-интерфейсы IOrderedQueryable и IQueryable. Это позволяет использовать LINQ для фильтрации и сортировки запросов. Вот, например, запрос на получение закладок, созданных за последние пять дней:

var bookmarks = session.Query<Bookmark>()
.Where( i=> i.DateCreated >= DateTime.UtcNow.AddDays(-5))
  .OrderByDescending(i => i.DateCreated)
  .ToList();

А это запрос, который позволяет пролистывать полный список закладок:

var bookmarks = session.Query<Bookmark>()
.OrderByDescending(i => i.DateCreated)
  .Skip(pageCount * (pageNumber – 1))
  .Take(pageCount)
  .ToList();

RavenDB формирует динамические индексы, основанные на выполнении этих запросов, и они сохраняются «в течение некоторого времени», а потом удаляются. Когда вновь запускается похожий запрос с той же структурой параметров, используется временный динамический индекс. Если некий индекс используется достаточно часто в течение отведенного периода, он становится постоянным.

Для получения закладок по тегу я могу добавить в свой класс BookmarksController следующий метод операции:

public ViewResult Tag(string tag)
{
  var model = new BookmarksByTagViewModel { Tag = tag };
  model.Bookmarks = this.DocumentSession.Query<Bookmark>()
    .Where(i => i.Tags.Any(t => t == tag))
    .OrderByDescending(i => i.DateCreated)
    .ToList();
  return View(model);
}

Я предполагаю, что эта операция будет регулярно вызываться пользователями моего приложения. Если так и будет, RavenDB преобразует этот динамический запрос в постоянный индекс без моего участия.

С появлением RavenDB .NET-сообщество наконец-то получило NoSQL-решение в области хранилищ документов.

Ворон, ниспосланный пробудить нас

С появлением RavenDB .NET-сообщество наконец-то получило NoSQL-решение в области хранилищ документов, позволяющее разработчикам и компаниям, ориентированным на технологии Microsoft, вступить в мир нереляционных баз данных, уже поддерживаемых многими другими инфраструктурами и их языками. RavenDB дает возможность .NET-разработчикам быстро приступить к работе с нереляционными хранилищами данных, устанавливая четкий клиентский API, который имитирует применяемые этими разработчиками методики управления данными. Хотя споры между сторонниками реляционных и нереляционных баз данных явно никогда не утихнут, возможность быстро опробовать нечто «новенькое» поможет лучше понять, где и как в архитектуре приложения можно задействовать нереляционное решение.

Автор: Джастин Шварценбергер  •  Иcточник: MSDN Magazine  •  Опубликована: 19.03.2012
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:   ASP.NET, RavenDB, MVC 3.


Оценить статью:
Вверх
Комментарии посетителей
Комментарии отключены. С вопросами по статьям обращайтесь в форум.