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






Последние комментарии