Недавно писал проект на Rails 2.3 + MongoDB + MongoMapper. Не могу сказать, что все было хорошо - для того, чтобы существующие плагины для рельсов заработали с MongoMapper-ом пришлось немного повозиться, но в итоге все закончилось хорошо. :)
А мой сегодняшний рассказ пойдет о некоторых особенностях MongoDB и MongoMapper-а, с которыми вы, скорее всего столкнетесь, если будете использовать эти библиотеки.
Для начала поговорим немного о MongoDB. Что это такое?
MongoDB — документо-ориентированная система управления базами данных (СУБД) с открытым исходным кодом, не требующая описания схемы таблиц. Написана на языке C++. СУБД управляет наборами JSON-подобных документов, хранимых в двоичном виде в формате BSON.
Если попробовать вкратце охарактеризовать эту базу данных, то получится что-то вроде этого: аналог реляционной СУБД без join-ов и транзакций, зато с поддержкой структур данных (массивов, хешей).
MongoMapper - это “ORM” для MongoDB, написанный на руби. “ORM” в кавычках потому что сложно себе представить ORM для нереляционной базы данных. Я бы скорее назвал это высокоуровневой оберткой над API, которое предоставляет MongoDB, с поддержкой ассоциаций между записями и много чем еще.
Теперь, собственно, о “особенностях” MongoDB и MongoMapper-а.
Вложенные документы
Вложенные документы в понятии MongoMapper-а - это когда одни обьекты хранят внутри себя другие. Для примера, возьмем следующую модель:
class User
include MongoMapper::Document
key :login
key :password
key :salt
many :posts
many :addresses
end
class Post
include MongoMapper::Document
key :title, String
key :body, String
timestamps!
end
class Address
include MongoMapper::EmbeddedDocument
key :type, String
key :country_id, ObjectId
key :city_id, ObjectId
key :city_address, String
end
Чтобы понимать, что тут происходит нужно, во-первых знать Ruby :) и во вторых - прочесть пост John Nunemaker (даже не знаю, как правильно перевести :)) о MongoMapper-е. Вот еще один, кстати, тоже интересный.
Таким образом имеем модель пользователя, которая хранить набор постов, написанных этим пользователем и набор адресов пользователя (домашний, рабочий, etc).
Особенности работы со встроенными документами легче показать на примере:
user = User.first
user.posts
Последняя комманда вернет “scope”/”relation”, а не просто массив элементов (люди, знакомые с named_scope в ActiveRecord версии < 3 поймут).
Соответственно, можно дродолжить эту комманду, например, так:
user.posts.all(:conditions => { :created_at => { "$gt" => (Date.today - 10.days) } })
В то время как
user.addresses
вернет массив и всю дополнительную фильтрацию прийдется производить с помощью Ruby.
Манипуляции со вложенными документами
У EmbeddedDocument нет понятия “id”, т.е. отличать обьекты Address один от другого можно только по их индексу в массиве user.addresses либо вводя “свои” идентификаторы. Но даже в этом случае выбирать соответствующий обьект прийдется “вручную”:
user.addresses.detect { |address| address.identifier == params[:address_identifier] }
PS. Для тех, кто в танке: detect - это аналог select { |address| … }.first.
Соответственно - вложенные обьекты легко добавлять, выбирать всю связь полностью, но с ними становится “сложно” работать, как с отдельными обьектами. В частности - неудобно обновлять из-за проблем с выборкой отдельного элемента и неудобно удалять.
С удалением вообще интересная история. Удалить обьект имея только ссылку на него - нельзя, нужна еще и ссылка на массив, в котором он хранится:
user.addresses.first.delete/destroy
не работает, т.к. таких методов у вложенных обьектов нет. Нужно делать так:
user.addresses.delete_at(address_index)
если вы знаете индекс в массиве адресов, либо
user.addresses.delete_if { |address| address.identifier == address_identifier }
если знаете идентификатор или удаляете по какому-либо другому полю.
Несложно, но проблемы на первых порах с этим бывают.
“идентификаторы” обьектов MongoDB
Каждому обьекту в бд MongoDB присваивает свой идентификатор. Как он по умолчанию строится можно посмотреть здесь. Обычно эти идентификаторы совсем не мешают, но иногда хочется просто засунуть вместо них какие-нибудь данные.
Например, вместо такой модели:
class Tag
include MongoMapper::Document
key :name, String
end
иметь такую
class Tag
include MongoMapper::Document
# id-шник явно никогда не прописывается
end
И создавать обьект так:
Tag.create(:_id => "ruby on rails")
MongoDB это позволяет делать. Более того - драйвер Ruby для MongoDB это тоже может делать. Проблема в том, что это не умеет делать MongoMapper. Совершенно. Как обойти эту проблему я на данный момент не знаю (я в итоге сделал модель первого типа и забил), есди кто знает решение - напишите, будет интересно.
И напоследок поговорим о
Has And Belongs To Many в MongoDB
… или habtm, знакомый многим по ActiveRecord.
Приведу пример из моего проекта: есть домены, есть их модераторы, связть many-to-many. Для каждого домена хочется видеть список модераторов и для каждого модератора хочется видеть список доменов, которые он модерирует.
Честно говоря, задача поставила меня в тупик. Т.е. как все мы решаем эти задачи с помощью RDBMS? Правильно - связующей таблицей. Так же можно было поступить и здесь, но… Как-то это было некрасиво, на мой взгляд, использовать этот подход вв документоориентированной базе данных.
Первое решение, которое мне пришло в голову - хранить массив id-шников доменов для каждого пользователя и хранить массив id-шников пользователей для каждого домена и синхронизировать их.
Последние два слова очень смущали - синхронизация вносила излишнюю сложность. Т.е. реализовать-то её можно, но очень не хотелось.
Собственно, обратился за помощью к знакомым и вышел на одного человека (ник - Necromant, сайта его я не знаю :) ), который помог решить мне эту проблему.
Итак, решение - хранить только один список id-шников (или в юзерах или в доменах) и сделать по нему индекс, соответственно, вывод как доменов по юзеру так и юзеров по домену будет достаточно быстрый.
Я хранил массив юзеров для домена, т.к. если бы я сделал иначе, то при удалении домена пришлось бы пройтись по всем юзерам и удалить все связи на домен, а в выбранном варианте этого делать не нужно (а удаление юзеров - это пока что вообще не существующее событие для моего проекта):
class Domain
include MongoMapper::Document
# ...
key :moderator_ids, Array, :index => true
def moderators
User.find moderator_ids
end
end
class User
include MongoMapper::Document
# ...
def moderator_of?(domain)
domain.moderator_ids.include? id.to_s
end
def moderated_domains
Domain.all :moderator_ids => id.to_s
end
end
Еще хочу рассказать о том, как подружить MongoMapper и Clearance, но пост вроде и так не маленький получился, так что ждите еще один пост о MongoMapper-е в ближайшие дни.
PS. Совсем писать разучился… :(
PPS. Если вам понравился этот пост, проголосуйте за него на Habrahabr-е. :)






May 11th, 2010 at 07:33
А зачем ты решил использовать MongoDB? Если проект just for fun то все понятно, но если это проект на продакшен и под заказчика то я не вижу чего-то что нельзя было бы сделать на олдскульных БД без таких костылей и такой-то матери.
May 11th, 2010 at 11:25
Это долгая история :) Проект – на продакшн, но все же было решено использовать MongoDB.
А на RDBMS вообще все что угодно можно сделать, так что использовать их везде, где они решают как-нибудь проблему – это не лучший подход. Хотя в данном случае мне бы было легче решать проблему с юзерами и доменами. Но фана было бы меньше. :)
May 11th, 2010 at 11:44
Фана может и меньше, зато практической пользы больше…
May 11th, 2010 at 11:55
Если бы я проект не сделал – тогда да, а так – практической пользы от использования MongoDB больше, т.к. я кроме того, что сделал проект еще и познакомился с документоориентированными СУБД.
May 11th, 2010 at 13:26
>> кроме того, что сделал проект еще и познакомился с документоориентированными СУБД.
You are quite naive. You think a moment when project goes to production is the final moment?
Who is going to support your mishmash in production?
Do you have many MongoDB professionals available in your company? :)
May 11th, 2010 at 13:40
I will :) Cause that’ll give me more experience of working with MongoDB :)
May 11th, 2010 at 14:05
Наверное в описании MongoMapper модели User закралась ошибка, там написано:
many :tags
many :addresses
В то время, как дальше по тексту идет разговор про **посты** и адреса: “Таким образом имеем модель пользователя, которая хранить набор постов, написанных этим пользователем и набор адресов пользователя (домашний, рабочий, etc).”
А за статью спасибо. Сам как раз разбираюсь с монгомаппером и самой монгой… Но застрял на хранении файлов в GridFS, что-то не хочет Joint работать. Нету никаких толковых статей по этому вопросу?
May 11th, 2010 at 14:34
Tnx, поправил. Сначала думал сделать теги, но получалось не очень удачно – переделал на посты, а модель поправить забыл.
GridFS не юзал и не смотрел по этому поводу ничего, так что не подскажу.
May 11th, 2010 at 14:42
Можешь удалить этот коммент… Но у тебя и дальше по коду идет использование User.tags вместо User.posts, исправь :)
user = User.first
user.tags
и еще вот здесь:
user.tags.all(:conditions => { :created_at => { “$gt” => (Date.today – 10.days) } })
May 11th, 2010 at 15:56
Мдя. Как-то я криво поправил. Что-то на работе совсем не о руби думается уже, а о Objective-C и iPhone :)
May 17th, 2010 at 11:49
А как насчет производительности проекта? по вашим впечатлениям mongo быстрее mysql?
May 17th, 2010 at 19:05
Пока ничего не могу сказать
January 19th, 2011 at 15:40
А вы до сих пор считаете использование массива указателей в объекте хоста верным?
January 26th, 2011 at 01:18
Это ты к чему?
January 26th, 2011 at 09:40
К тому что данные нужно организовывать так, чтобы при загрузке из базы их грузилось мало, а не много. Пользователей явно больше, чем хостов. Много маленьких массивов лучше, чем один большой.