Доброго всем времени суток. Я буду говорить о 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;}
}
Все, что нужно нам сделать для корректной записи в БД статьи с комментариями, это:
- Добавить свойство Comments в класс Article
- Удалить свойства 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