Pull to refresh

MongoDb в действии — интернет магазин

Reading time 4 min
Views 28K
Скоро будет год с момента моего знакомства с MongoDb. Я был далеко не первым, кто начал с ней работать, но, тем не менее, эта технология все еще воспринимается как экспериментальная.

В целом скажу так: работать с MongoDB удобнее чем с MS SQL. Регулярно встречаются сценарии, которые требуют больше усилий по сравнению с SQL, однако, в результате ты больше знаешь о том, как устроена твоя база данных и лучше контролируешь что будет тормозить, а что — нет.

На хабре полно приложений в стиле «Hello World», так что инициализацию среды опустим и перейдем сразу к более продвинутым вопросам, а именно:
  • Почему удобнее хранить весь объект целиком, а не по таблицам?
  • Как бороться с реляциями?


Почему удобнее хранить весь объект целиком, а не по таблицам?


Для многих программистов все еще не очевидно, что выборка записей даже по Primary Key — это существенные затраты времени. Вроде как, знать такое и не нужно — бери себе таблицу, делай хранимую процедуру для поиска и можно больше ни о чем не переживать. Однако на практике объекты редко бывают плоскими, и любимый всеми механизм lazy loading очень быстро катастрофически ухудшает время работы системы.
Вот несколько примеров комбинаций объектов, из моего реального опыта, за последние полтора года:
— у товара есть произвольное количество картинок и видео
— у товара произвольное количество характеристик
— у категории есть товары, которые ее представляют
— в случае наследования, которое добавляет новые свойства, мы либо теряем место в таблице, либо имеем дополнительный подзапрос
— у объекта наименование и описание задано на произвольном количестве языков.

Описать любой такой сценарий на 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?
  • Конфигурация реальной среды
Tags:
Hubs:
+37
Comments 93
Comments Comments 93

Articles