Мелочи, о которых стоит помнить при использовании RavenDB

от автора

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

Краткое введение

RavenDB – документно-ориентированная база данных. Подразумевается, что она гибкая, удобная, быстрая и в ней еще много всяких вкусностей…

И это так.

Но с некоторыми оговорками.

Самое главное

Первое, что мы встречаем при поиске «архитектура проекта с RavenDB» это фраза – не работайте с RavenDb как с реляционной базой данных.

Эта фраза значит, что Вам необходимо провести денормализацию данных там, где это необходимо. RavenDB подталкивает нас к решению хранить всю нужную информацию в одной сущности.

Рассмотрим пример.

Пусть у нас есть следующая сущность.

public class Article {     public long Id {get;set;}     public string Title {get;set;}     public string Content{get;set;} } 

И сделаем допущение, что у нашей сущности Article могут быть комментарии.

public class Comment {     public long Id {get;set;}     public string Author {get;set;}     public string Content{get;set;} } 

Все, что нужно нам сделать для корректной записи в БД статьи с комментариями, это:

  1. Добавить свойство Comments в класс Article
  2. Удалить свойства Id из комментария.

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

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

Кстати, в RavenDB нет таблиц. Там есть коллекции сущностей, причем принадлежность сущности к коллекции задается очень просто. Но об этом ниже.

Метаданные

Стоит заметить, что RavenDB хранит сущности в виде JSON-объектов. Как следствие, у них нет определенной структуры, за исключением некоторых служебных свойств. Основное служебное свойство это @metadata. В этом объекте находятся все управляющие данные нашего документа: Id внутри RavenDB, тип на сервере (наш Article, к примеру) и многие другие свойства.

За принадлежность сущности к коллекции отвечает свойство Raven-Entity-Name. Если его изменить, то изменится и коллекция, в которой находится объект. Id автоматически не меняется.

Кстати, идентификаторы в RavenDB по умолчанию мапятся на свойство Id, но Вы можете сделать любое поле идентификатором и определить собственную стратегию генерирования идентификаторов. Более подробно описано тут. Надо только пролистать немного вниз.

Кстати, важная вещь которую я говорил ранее, но еще раз повторю: Отношения между сущностями — плохо. Все, с чем работает сущность, должно находиться в ней.
Конечно, может возникнуть ситуация, когда нужно определить принадлежность одной сущности к другой, но если это Вам приходится делать постоянно, спросите себя — правильно ли Вы используете RavenDB и нужна ли она Вам на проекте?

Поиск по сущностям

Рассмотрим тривиальную задачу — нам нужно получить список всех постов.

List<Blog> blogs = null; using (var session = store.OpenSession()) {     blogs = session.Query<Blog>().ToList(); } 

Что происходит в этом маленьком кусочке кода?
Во-первых, мы создаем подключение к RavenDB (это тривиально).
Во-вторых, сессия отдает нам ровно 128 первых сущностей, удовлетворяющих условию. Почему 128? Потому что это поведение по умолчанию. В конфиге можно увеличить это значение до 1024, но, согласитесь, это не совсем то поведение, которое требуется.

Это происходит из-за того, что RavenDb настоятельно советует использовать пагинацию (pagination) для работы с большим объемом данных. И было бы клево, если бы это поведение было уже прописано в API, но этого нет! Вместо этого приходится каждый раз писать свой велосипед для пагинации. Сначала нам нужно узнать, сколько всего страниц будет, а потом выдернуть конкретную.
Да, задача тривиальная, но раздражает.

Кстати, вот код(возможно, с вашей точки зрения, неоптимальный), упрощающий работу с пагинацией.

public static int GetPageCount<T> (this IRavenQueryable<T> queryable, int pageSize) {    if (pageSize < 1)    {          throw new ArgumentException("Page size is less then 1");    }     RavenQueryStatistics stats;    queryable.Statistics(out stats).Take(0).ToArray(); //Без перечисления статистика работать не будет.     var result = stats.TotalResults / pageSize;     if (stats.TotalResults % pageSize > 0) // Округляем вверх    {         result++;    }     return result; }  public static IEnumerable<T> GetPage<T>(this IRavenQueryable<T> queryable, int page, int pageSize) {    return queryable    .Skip((page - 1)*pageSize)               .Take(pageSize)               .ToArray(); }   

Однако и это не все.
В указанном выше примере RavenDB отдает нам сущности, отсортированные по последней дате изменения. Это свойство Last-Modified в объекте @metadata, о котором я говорил ранее.

Интересный факт — сортировать по Id’у нельзя. Вылетает ошибка, либо ничего не происходит.
Решение простое — создаем поле Created и сортируем по нему.

Использование RavenDB для запросов

Стоит помнить, что сессия ограничена 30 запросами, после истечения этого лимита происходит исключение при попытке отправить запрос к БД. Таким образом создатели этой во всех отношениях замечательной базы данных говорят нам о том, что следует создавать отдельную сессию на каждый запрос. В принципе, это оправданно, потому что сессия представляет собой UnitOfWork и, как следствие, легковесна. Но постоянное создание сессий может привести Ваш код к нечитаемому виду, поэтому можно поступить иным образом:

private IDocumentSession Session                 {                         get                         {                                 if (_session == null)                                 {                                         _session = _store.OpenSession();                                 }                                  if (_session.Advanced.NumberOfRequests ==                                      _session.Advanced.MaxNumberOfRequestsPerSession)                                 {                                         _session.Dispose();                                         _session = _store.OpenSession();                                 }                                 return _session;                         }                 } 

Использование RavenDB в проекте

Создатель вышеупомянутой базы данных Ayende Rahien говорит: «Используйте RavenDB на таком высоком уровне, как это возможно». И приводит в пример доступ к БД напрямую из контроллера. Возможно, для маленьких проектов это и оправдано. Однако я отдаю предпочтение старой доброй трехзвенке с unit-тестированием, поэтому этот путь не для меня.

Мое решение — это прокси над сессией RavenDB, которая делает то, что мне нужно.
Самая главная причина для создания этого компонента — это затруднения с моком сессии. Если Load еще как-то можно замочить, то Query — практически нереально. В то время как надстройку — очень просто.

И еще одно следует сказать про тесты с RavenDB. Может такое случится, что вам необходимо проверить работу с реальной базой данных. В таком случае используйте EmbeddableStore.
Одна из причин использования реальной базы – тестирование индексов. Но индексы в RavenDB — это обширная тема, о которой стоит написать отдельную статью. =)

ссылка на оригинал статьи http://habrahabr.ru/post/190028/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *