Скоро будет год с момента моего знакомства с MongoDb. Я был далеко не первым, кто начал с ней работать, но, тем не менее, эта технология все еще воспринимается как экспериментальная.
В целом скажу так: работать с MongoDB удобнее чем с MS SQL. Регулряно встречаются сценарии, которые требуют больше усилий по сравнению с SQL, однако, в результате ты больше знаешь о том, как устроена твоя база данных и лучше контролируешь что будет тормозить, а что — нет.
На хабре полно приложений в стиле «Hello World», так что инициализацию среды опустим и перейдем сразу к более продвинутым вопросам, а именно:
- Почему удобнее хранить весь объект целиком, а не по таблицам?
- Как бороться с реляциями?
Почему удобнее хранить весь объект целиком, а не по таблицам?
Для многих программистов все еще не очевидно, что выборка записи даже по Primary Key — это существенные затраты времени. Вроде как, знать такое и не нужно — бери себе таблицу, делай хранимую процедуру для поиска и можно больше ни о чем не переживать. Однако на практике объекты редко бывают плоскими. Вот несколько примеров из реального опыта за последние полтора года:
— у товара есть произвольное количество картинок и видео
— у товара произвольное количество характеристик
— у категории есть товары, которые ее представляют
— в случае наследования, которое добавляет новые свойства, мы либо теряем место в таблице, либо имеем дополнительный подзапрос
— у объекта наименование и описание задано на произвольном количестве языков.
Описать любой такой сценарий на C# не представляет труда; а вот сделать эффективный слой данных, который бы работал на сотне тысяч записей, будет затруднительно.
В то же время, используя MongoDB, сохранить такой объект можно одним единственным вызовом:
DocumentCollection.Save<T>(document);
Загрузить его со всем вложенными классами тоже элементарно:
DocumentCollection.FindOneById(id);
К примеру, посмотрим на представление товара. За одно единственное обращение к базе данных — поиск по id категории и SeoFreindlyUrl, которое занимает 0,0012s (!) я получаю:
— собственно свойства товара
— его параметры (в данном случае всего два, но вообще их количество и типы произвольны)
— изображения (повторно используется в категориях; 2 штуки, каждое имеет url + размеры)
— видео (если бы были)
— похожие товары (ссылки)
— условия продажи (этот объект повторно используется в категориях и системных настройках для наследуемой конфигурации срока гарантии, возможности возврата)
— производитель
— Seo строки (заголовок браузера, мета тэги, сео текст)
И могу сразу переходить к рендерингу.
Для статистики: в таблице товаров на данный момент 154 тысячи записей; в среднем одна запись занимает 22KB; а размер таблицы — 4GB.
Наилучший вариант считывания такого сложного объекта, если бы мы использовали SQL Server, была бы ручная сериализация всех свойств в xml. MongoDB же все это дает нам без каких-либо усилий.
Вся наша система базируется на трех классах:
— BaseMongoClass (Id, Title, LastChanged)
— EntityRef (ссылка, содержит Id и Title, есть и более навороченные наследники)
— BaseRepository, который реализует все необходимые методы для работы. Мы выбрали GetById, Get(запрос), FirstOrDefault, GetAll, GetByIds(по списку id), GetByEntityRefs(по списку EntityRef), Save, DeleteById, DeleteByQuery.
Конкретный репозиторий просто наследуется от BaseRepository, указывая тип и имя коллекции (в терминах монго — это имя таблицы), и реализует какие-то операции уже уровня логики, такие как «найти товары по категории» и т.п.
MongoDB является самой удобной базой, когда нужно сохранять иерархическую информацию. Мир же таков, что плоских данных очень и очень мало.
PS: Листинг базовых класов и репозитория можно скачать здесь.
Как бороться с реляциями?
Конечно же, реляций в монго нет. В проекте abo.ua мы применяем следующий подход:
У товара может быть одна категория (у нас больше, но я немного упрощаю). В самом типе товара написано буквально следующее:
public EntityRef Category { get; set; }
[BsonIgnore] // Монго, не стоит сохранять это свойство
public Category CategoryValue
{
get
{
if (Category == null || Category .IsEmpty())
return null;
return AppRequestContext.Factory.BuildCategoryRepository().GetById(Category.Id);
}
}
Когда мы меняем категорию, мы задаем Category. Когда нам нужен удобный способ узнать что-то детальнее о категории — мы обращаемся к CategoryValue.
Для того, чтобы не терять время на вычитку и десериализацию категории, количество которых, конечно, и меняется относительно редко, CategoryRepository кэширует их все в оперативной памяти, в словаре ObjectId -> Category, скорость к которому превышает обращение к MongoDB.
Когда хоть какая-то категория меняется, мы перестраиваем весь словарь.
Можно, конечно, использовать Memory databases, однако эксперименты показали, что это принципиально медленнее, чем собственная память процесса.
Другая проблема реляций — обновление информации в связанных объектах. Например, категорию изменили/удалили, но мы хотим чтобы у товара была актуальная информация:
1. Всегда будьте готовы к нецелостной информации. Из кода приведённого выше, CategoryValue вернет null если такой категории уже нет; и вебсайт вернет код 404. Это еще простой сценарий. Когда мы вычитываем товары, мы сверяем свойства с определениями типов товаров: не удалили ли какое-то свойство? Не поменяли ли в нем список допустимых значений? Каково значение по умолчанию для добавленных в тип свойств? Звучит сложно, но на самом деле, когда все данные под рукой, мы успеваем просмотреть 1000 товаров в течении 0.1 с, чего вполне достаточно.
2. После того как вы научили код «самозалечивать» целостность данных, становится легко написать код, который корректирует данные в базе. Выглядит он примерно так:
var products = prodRepo.GetAll().OrderBy(p => p.Id).Skip(start).Take(portionSize).ToArray();
prodRepo.JoinPropertyTypes(products); // собственно это метод кторый проверяет правильность
products.AsParallel().ForAll(p => prodRepo.Save(p));
Осталось всего-то вызвать такой код (асинхронно) для всех объектов, которые были затронуты.
В следущих частях
Я опишу:
- Есть ли жизнь без group by?
- Как организовать полнотекстовый поиск с релевантностью в mongodb?
- Конфигурация реальной среды
Автор: osypchuk