Pull to refresh

Comments 44

Спасибо, очень актуально. Никак не могу «привыкнуть» к денормализации, и прочим гибкостям. В связи с чем мне пока достаточно сложно строить модели данных в Mongo.
Хорошая статья, буду давать ссылку всем тем кто никак не может отвыкнуть от SQL подхода и использует реляционную модель в документных базах.
Кстати, у автора оригинала в конечной структуре вроде бы ошибка.
У Подписчика присутствует коллекция memberships, а должна быть subscriptions.
Автор вообще не оперирует понятием подписка, он как раз оперирут понятием membership. Я им не смог адекватно оперировать в переводе, поэтому заменил подходящим по смыслу термином.
Верно. Им я и не смог адекватно оперировать.
Да, понятно. Но по-моему стоит это как раз-таки разделить. Согласитесь, почти в любом другом примере уже не удалось бы одинаково назвать коллекции и в той и в другой сущности.
Допустим если отношение много ко многим между проектами и сотрудниками. У сотрудника может быть коллекция проектов, а у проекта коллекция сотрудников.
На мой взгляд что даже если есть возможность именовать одинаково с обоих сторон, лучше придумать разные названия которые будут лучше отражать направление связи.
Т.е. вы предлагаете называть коллекцию проектов одним названием, а коллекцию сотрудников — другим? Может пример приведете, а то у меня ощущение, что я не очень понимаю.
Ну смотрите, пусть у вас в системе есть две сущности проекты и сотрудники.
Они состоят в отношении многие ко многим.
Получается если строить модель со вложенными элементами а не нормализированную, то у вас может быть два варианта:
1) Коллекция Сотрудников у которых вложенная коллекция Проектов
2) Коллекция Проектов с вложенной коллекцией Сотрудников

Вы уже не можете в обоих этих случаях одинаково назвать вложенную коллекцию как получилось в примере группами и участниками (и то только на английском языке).
Я бы вообще в этом случае или назвал бы коллекцию участников в группе members, или коллекцию групп у юзера назвал бы groups/subscriptions.
Понял. Соглашусь. Однако, для примера все же думаю правилнее оперировать одним и тем же понятием. Думаю, грамотный человек и так поймет, как лучше, когда будет делать свою схему.
Для Вашего примера надо так: «сотрудники проекта» и «проекты сотрудника».
Возможно стоит написать имена коллекций в коде моделей.
Для стратегии «все встроено» — это Groups, а для стратегии «частичного встраивания» — это Users.
Ну да, стандартная проблема Embedding vs Linking, которая на сайте монги поднимается.
Правда, я почему-то не увидел здесь минусов встраивания, и примеров когда Linking более актуален и логичен, а без этого статья немного неполна.
А где вы видели про плюсы и минусы встраивания и линкинга? Можете дать ссылку, хочу почиать?

Я уже третий день пытаюсь определиться со схемой для своего проекта, понять плюсы и минусы, но так до сих пор и не нашел правильного решения.

Сергей Матвеенко на вопрос в гугловских группах, что и в каких случаях лучше встраивать, а на что ссылаться при моделировании схемы, посоветовал:
Критерии примерно следующие:
  1. (Самый важный) какие запросы вы будете делать. Данные, которые в
    SQL присоединяются джойнами, можно складывать внутрь документа, для
    которого они вам нужны.
  2. (Похож на первый) отображение. Какие данные вы хотите показать
    вместе с этим объектом? Их и надо пихать внутрь него. Идеальный
    вариант в вакууме: одна страница интерфейса — один и только один
    документ.
  3. Как вы будете вставлять и модифицировать данные. Тут важно нужно ли
    вам в каком-то месте обеспечить транзакционность.
  4. Масштабируемость. Здесь важен выбор ключа шардинга, но его можно
    впихнуть потом отдельным полем (если данных будет не слишком много),
    но можно и сразу завести отдельное поле, например, «shkey», и уже
    писать в него что-нибудь осмысленное. Так легче будет поменять логику
    шардинга в будущем.

Не видел, опираюсь на опыт и полигональные испытания :)
Критерии у меня по-проще:
1. Удобство использования (== количество и сложность запросов).
2. Необходимость client-side обработки частей составного отображения. (== нужно ли как-то хитро фильтровать данные перед join'ом).
3. Производительность (== вставки + апдейты + выборки).
4. Размер данных. Если нужно хранить, скажем, миллиард записей, а нужно при выборки 10-100 строк, то каждое лишнее поле при embedding'e будет больно бить по размеру коллекции.
В таком порядке.
Но опять же иногда сходу невозможно оценить, и нужно попробовать несколько вариантов.
Все проще: Linking для справочных данных и Embedding для событий или тех, чья информационная целостность критична.

Данные, описывающие событие не должны каскадно меняться, поскольку описывают состояние события на определенный момент времени. Linking в этом случае нарушает его целостность и способно скомпроментировать его как факт.

Справочные данные наоборот, являются «строительными кирпичами» событий. Они могут образовывать множество связей между собой и это негативно скажется на попытке поддежания актуальности информации во всех связанных документах, если будет использован Embedding подход. Так же справочные элементы не являются каким либо событием, а просто набором атрибутов для некоего абстрактного объекта, тип которого так же закладывается в этих атрибутах.
О как… Опять вы меня смущаете. Задам вам тогда вопрос, на который отвечал Сергей:
Я собираюсь использовать MongoDB для сбора диагностических данных c промышленных объектов. Основная сущность — пром.объект. У этого объекта есть такие характеристики, как: принадлежность к стране, региону, предприятию, филиалу этого предприятия, эксплуатирующей организации, которая находится в подчинении у филиала, географическое положение и.т.п. В реляционной модели я бы сделал все это в разных таблицах. А как лучше сделать в MongoDB? Плюсом в БД будут стекаться динамические данные с этих пром.объектов, примерно раз в секунду что-то типа среза всех данных по объекту.

Тут как раз, получется, есть справочные данные (характеристики объекта) и события (срез данных по объекту на момент времени). Т.е. исходя из вашего утверждения, данные о принадлежности к стране, региону, предприятию и т.п. нужно хранить отдельно, оставляя в документе ссылки, а срезы данных встраивать в внутрь?
Да, и во всяком случае я бы так поступил. Пром. объект содержал бы Embedded данные справочных документов(страна, филиал, организация и т.п.) на момент своего создания, а все справочные данные были бы отдельным документами, из которых можно собрать еще один пром. объект. А то, что организация находится в стране Б, можно оставить ссылку на эту страну — в контексте задачи это все справочные данные. Если организация Foo передает пром.объект организации Bar, то это событие стоит зафиксировать отдельным документом — как бонус получите историю «движения» пром. объекта по организациям.
Ещё одна вещь, которую стоит заранее знать — по слинкованным данным нет поиска, поэтому линковка — это отнюдь не замена джойнам и придётся так или иначе обращаться к Map/Reduce или городить каскады запросов, что не айс. Хотя, возможно ситуация меняется с новым фреймворком запросов, но я пока не изучал его досконально, чтобы ответить на этот вопрос.
Как это нет? вы же делаете линковку в обе стороны — найти все группы где в участниках есть такой-то _id.
Возможно под поиском мы понимаем что-то различное? Или я чего-то таки не знаю, что наверно, скорее.
Не могли бы Вы привести пример запроса по слинкованным данным?

Хотя, Вы говорите об _id, но обычно _id при поиске интересует в последнюю очередь, а поиск ведётся по определённым характеристикам связанного объекта — именно это я имел в виду, когда говорил, что поиск по слинкованным данным невозможен. Например, найти всех участников общих со мной групп, старше 18 — без дублирования информации или каскада запросов — трудно представить, как такое сделать.
Эммм, для такие запросов есть Aggregation Framework . А так да, чтобы сделать поиск (Query) такого типа сначала надо будет получить все записи (всех участников общих со мной групп), а потом по ним пройтись (старше 18).
Допустим форум. Вся информация о пользователе встроена, есть массив DBRef'ов на его сообщения. Есть ветка форума. Все сообщения в ней сделаны через DBRef, чтобы избежать лока. Я использую так: если документ обновляется редко и только одним пользователем — встраиваю, если документ можнт обновляться из разных мест и часто (посты в ветку пишут разные пользователи, а профаил обновляется только владельцем) — линкую.
Спасибо.
Недавно я натыкался на текст, в котором было написано что MongoDB не годится для heavy write приложений из-за однопоточной записи.
Действительно это так?
Что-то такое есть. На сайте монги в какой-то презентации говорится, что при записи блокируется база данных. При этом оговоривается, что это занимает ничтожно малый промежуток вермени, потому что «mongodb is extremely fast», и мол это почти сказывается на работе.

Тимофей Миронов, который использует монгу 2 года в продакшне в своем проекте timeliner, говорил на недавней конференции Дамп, что это бич монги и разработчики пытаются его всячески преодолеть и, якобы, в предстоящем релизе 2.2 блокирование будет на уровне коллекции.
Да, есть global lock во время апдейта, на весь монго, по всему кластеру :( даже если там реально разные БД. Сейчас занимаются исправлением этой ситуации, начиная с 2.2. В начале database lock, потом collection lock. Как я понимаю в 2.2 ожидается только уровня database.
Как при этом получить список всех адресов для рассылки по GROUP_ID?
Уточню вопрос. Вот мы делаем «user.find({'memberships.group':42}, {memberships:1})», в результате получаем список юзеров, в каждом есть массив из подписок, одна из которых является искомой. Как из списка юзеров сформировать список email адресов для собственно рассылки? Руками сканировать каждый массив?
У меня сейчас, к сожалению, нет под рукой консоли, предположу, что так:
user.find({'memberships.group':42}, {'memberships.adress':1})
Эта операция вернёт все адреса всех подписок пользователей, которые подписаны на рассылку №42. Кроме того, нам нужно ещё и оригинальное имя.
Видимо, исходя из таких вот вопросов и нужно проектировать модель данных в монге. Когда дело идет чуть дальше заполонивших интернет примеров про блог с комментариями, появляется куча вопросов. Поэтому я уже третий день не могу разработать нормальную схему, у меня тоже вокруг крутится куча «если».
Да, монга всегда поднимает весь документ полностью, как вариант можете использовать map-reduce или aggregation framework.
Как вариант, можно вместо списка использовать словарь:
> db.user.insert({ name:'Jo', memberships:{ 1:{ email:'x@y', grname:'blabla' }, 5:{ email:'Jo@gmail', grname:'for me' } } })
> db.user.insert({ name:'Jo2', memberships:{ 2:{ email:'x@y', grname:'blabla' }, 5:{ email:'Jo2@gmail', grname:'for me2' } } })
> db.user.insert({ name:'Jo3', memberships:{ 2:{ email:'x@y', grname:'blabla' }, 4:{ email:'Jo3@gmail', grname:'for me3' } } })
> GROUP = 5
> query = {}
> query['memberships.'+GROUP] = { $exists:true }
> fields = {}
> fields['memberships.'+GROUP] = 1
> db.user.find(query, fields)
{ "_id" : ObjectId("4fc50d81151b7c848b19d110"), "memberships" : { "5" : { "email" : "Jo@gmail", "grname" : "for me" } } }
{ "_id" : ObjectId("4fc50ee1151b7c848b19d111"), "memberships" : { "5" : { "email" : "Jo2@gmail", "grname" : "for me2" } } }

Код группы (желательно) продублировать в массив и проиндексировать.

А ещё с 2,1 есть такой функционал для сложных запросов.
Вариант. Только не понятно, как по этим словарям строить индексы, для каждой группы по индексу жирновато будет, даже если частичный.
>Код группы (желательно) продублировать в массив и проиндексировать.

Например: «groups» — индексированный массив, список подписанных групп, поиск по этому же ключу.
по одной команде для подписки: db.user.update({ _id:_id }, { $addToSet:{ groups:GROUP }, $set:{ memberships:… } })
для отписки: db.user.update({ _id:_id }, { $pull:{ groups:GROUP }, $unset:{ memberships:… } })
Ну просто поиск по документам, а затем клиентская фильтрация:
db.People.find({«memberships.group»: XXX})
Или если хочется только монгу, то простой map-reduce легко с этим справляется.
Замечательно, таким образом мы узнали список юзеров, подписанных на эту рассылку. Но ведь нам надо узнать адрес и название этой рассылки для каждого пользователя, верно? Сканировать массивы в MapReduce, (а следовательно, переливать в отдельную коллекцию, потом ходить по ней, и рассылат письма) — выглядит как-то не очень лаконично. Что-то не так с этой моделью.
> (а следовательно, переливать в отдельную коллекцию, потом ходить по ней, и рассылат письма)
{inline: 1} придёт на помощь :)
UFO just landed and posted this here
Добавлю еще, что нужно учитывать то, что для запросов наподобие:

user.find({'a.b':42}, {'b.c':1})


нельзя создать составной индекс если a и b это массивы

user.ensureIndex({'a.b':1, 'b.c':1})
cannot index parallel arrays [b] [a]


Sign up to leave a comment.

Articles