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

в 10:01, , рубрики: .net, nosql, ravendb, кто читает теги?, метки: , ,

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

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

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 — это обширная тема, о которой стоит написать отдельную статью. =)

Автор: mrakolice

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js