May 10

Недавно писал проект на 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-е. :)

written by fxposter \\ tags: , , ,


15 Responses to “MongoDB и MongoMapper – небольшой отчет об использовании”

  1. 1. Витя Says:

    А зачем ты решил использовать MongoDB? Если проект just for fun то все понятно, но если это проект на продакшен и под заказчика то я не вижу чего-то что нельзя было бы сделать на олдскульных БД без таких костылей и такой-то матери.

  2. 2. FX Poster Says:

    Это долгая история :) Проект – на продакшн, но все же было решено использовать MongoDB.

    А на RDBMS вообще все что угодно можно сделать, так что использовать их везде, где они решают как-нибудь проблему – это не лучший подход. Хотя в данном случае мне бы было легче решать проблему с юзерами и доменами. Но фана было бы меньше. :)

  3. 3. Леха Says:

    Фана может и меньше, зато практической пользы больше…

  4. 4. FX Poster Says:

    Если бы я проект не сделал – тогда да, а так – практической пользы от использования MongoDB больше, т.к. я кроме того, что сделал проект еще и познакомился с документоориентированными СУБД.

  5. 5. Alice Says:

    >> кроме того, что сделал проект еще и познакомился с документоориентированными СУБД.
    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? :)

  6. 6. FX Poster Says:

    I will :) Cause that’ll give me more experience of working with MongoDB :)

  7. 7. Sergey Kuznetsov Says:

    Наверное в описании MongoMapper модели User закралась ошибка, там написано:

    many :tags
    many :addresses

    В то время, как дальше по тексту идет разговор про **посты** и адреса: “Таким образом имеем модель пользователя, которая хранить набор постов, написанных этим пользователем и набор адресов пользователя (домашний, рабочий, etc).”

    А за статью спасибо. Сам как раз разбираюсь с монгомаппером и самой монгой… Но застрял на хранении файлов в GridFS, что-то не хочет Joint работать. Нету никаких толковых статей по этому вопросу?

  8. 8. FX Poster Says:

    Tnx, поправил. Сначала думал сделать теги, но получалось не очень удачно – переделал на посты, а модель поправить забыл.

    GridFS не юзал и не смотрел по этому поводу ничего, так что не подскажу.

  9. 9. Sergey Kuznetsov Says:

    Можешь удалить этот коммент… Но у тебя и дальше по коду идет использование User.tags вместо User.posts, исправь :)

    user = User.first
    user.tags

    и еще вот здесь:

    user.tags.all(:conditions => { :created_at => { “$gt” => (Date.today – 10.days) } })

  10. 10. FX Poster Says:

    Мдя. Как-то я криво поправил. Что-то на работе совсем не о руби думается уже, а о Objective-C и iPhone :)

  11. 11. swdeveloper Says:

    А как насчет производительности проекта? по вашим впечатлениям mongo быстрее mysql?

  12. 12. FX Poster Says:

    Пока ничего не могу сказать

  13. 13. FatDevil Says:

    А вы до сих пор считаете использование массива указателей в объекте хоста верным?

  14. 14. fxposter Says:

    Это ты к чему?

  15. 15. FatDevil Says:

    К тому что данные нужно организовывать так, чтобы при загрузке из базы их грузилось мало, а не много. Пользователей явно больше, чем хостов. Много маленьких массивов лучше, чем один большой.

Leave a Reply