Mongoid::Mirrored

Helper module for mirroring root documents in embedded collections. Works with Mongoid.

When adopting the mindset of document base storages we can understand the advantages of representing a document with all its related content as opposed to the relational database approach of referencing information scattered across a number of other collections. That doesn't only help in understanding better the data model we have in our hands, but also usually provides better read performances in our applications. Although the document based mindset can come naturally for most of us, some still struggle when trying to adopt this new paradigm (myself included) and end up modeling our data closely of what we would have done in a relational database. Sometimes that happens because can't be sure of that data access patterns in advance or we just fear denormalization.

Mongoid provides an intuitive interface to reference documents among different collections, but that comes at a cost. As MongoDB doesn't support joins and relying heavily on relational data reads, that could be a problem for some applications. Unfortunately, Mongoid doens't support embedding collections while maintaining an independent root collection and that is exactly the intention with mirrored documents.

I couldn't find anything that helped me with that, but this discussion at the mongoid group where Durran stated that it was unlikely that mongoid would go in that direction.

This gem has been inspired by that conversation and also Mongoid::Denormalize which you should definitelly check out if you have similar needs.

I'm not an experienced developer(in fact I work as a Product Manager and develop only to bootstrap proofs of concept) and I'm new to document database storages. Please feel free to contribute to this gem or point out the correct approach for this.

Installation

Add the gem to your Bundler Gemfile:

gem 'mongoid-mirrored'

Or install with RubyGems:

$ gem install mongoid-mirrored

Usage

In your root(master) model:

# Include the helper module
include Mongoid::Mirrored

# Define wich fields and methods will be shared among root and embedded classes
mirrored_in :articles, :users, :sync_direction => :both, :sync_events => :all do
    field :contents, :type => String
    field :vote_ratio, :type => Float

    def calculate_vote_ratio
        # do something with votes
    end
end

Example

# The conventioned name for the mirrored class is "#{embedding_class}::{root_class}"

class Post
  embeds_many :comments, :class_name => "Post::Comment"
end

class User
  embeds_many :comments, :class_name => "User::Comment"
end

# The Root Class should establish with which classes it is supposed to sync documents.
# Everything declared within the block will be shared between the root and mirrored 
# documents (eg. fields, instance methods, class methods)

class Comment
    mirrored_in :post, :user, :sync_direction => :both do
        field :contents
        field :vote_ratio, :type => Integer
        def foo
         "bar"
        end
    end
end

Options

sync_events
-----------
    :all(default) => sync master and mirrored documents on :after_create, :after_update and :after_destroy callbacks
    :create => syncs only on :after_create
    :update => syncs only on :after_update
    :destroy => syncs only on :after_destroy
this options accepts an Array of events (eg: [:create, :destroy])   

sync_direction
--------------
    :both(default) => syncs documents on both directions. From master(root) to mirrors and from mirror to master
    :from_root => syncs only from master to mirrors
    :from_mirror => syncs only from mirror to master

replicate_to_siblings
---------------------
true(default) => perform operations on the mirror's siblings 
(eg: article.comments.create(:user_id => user.id) will replicate the document on the User::Comment collection)

inverse_of
----------
:one => 
:many(default)

index
-----
false(default) => determines whether the root collection will create an index on the embedding collection's foreign_key

index_background
----------------
false(default) => determines whether the aforementioned index will run on background

Rake tasks

should include tasks to re-sync

Known issues

  • The helper does not support multiple calls of the mirrored_in method
  • Changing parents from the embedded association does not update target documents. Use the master collection to change associations.
    • eg post.comments.first.update_attribute(:post_id => Post.create) does not include the comment in the new Post comments list

Performance

I ran a benchmark on my computer with the following results

Benchmark for referenced documents

                                                                     user     system      total        real
creating 10000 comments in 200 posts from root collection       7.820000   0.250000   8.070000   (8.252877)
updating 10000 comments from root collection                    5.080000   0.250000   5.330000   (5.583467)
# traversing posts with comments 10000 times                    0.670000   0.010000   0.680000   (0.772653)
finding posts from the 1000 newest comments                     0.530000   0.040000   0.570000   (0.647710)
deleting 10000 comments from root collection                    2.710000   0.180000   2.890000   (3.238750)
creating 10000 comments in 200 posts from embedding collection  8.270000   0.210000   8.480000   (8.530049)
updating 10000 comments from embedding collection               9.830000   0.360000  10.190000   (10.76156)
deleting 10000 comments from embedding collection               4.750000   0.240000   4.990000   (5.217647)

Benchmark for mirrored documents

                                                                      user     system      total        real
creating 10000 comments in 200 posts from root collection       14.870000    0.450000  15.320000  (15.476777)
updating 10000 comments from root collection                    25.590000    0.970000  26.560000  (36.910317)
# traversing posts with comments 10000 times                    0.200000     0.000000   0.200000   (0.211826)
finding posts from the 1000 newest comments                     0.190000   0.000000   0.190000     (0.198631)
deleting 10000 comments from root collection                    17.880000    0.820000  18.700000  (21.626592)
creating 10000 comments in 200 posts from embedding collection  7.100000     0.340000   7.440000  ( 8.562442)
updating 10000 comments from embedding collection               5.030000     0.300000   5.330000  ( 6.058490)
deleting 10000 comments from embedding collection               2.540000     0.130000   2.670000  ( 2.733644)

Credits

Copyright (c) 2011 Alexandre Angelim, released under the MIT license.