mongoid-mirrored 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in mongoid-rspec.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Alexandre Angelim
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ Mongoid::Mirrored
2
+ ====================
3
+
4
+ Helper module for mirroring root documents in embedded collections. Works with [Mongoid](https://github.com/mongoid/mongoid).
5
+
6
+ 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.
7
+
8
+ 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.
9
+
10
+ I couldn't find anything that helped me with that, but this [discussion](http://groups.google.com/group/mongoid/browse_thread/thread/b5e2bccf77457043) at the mongoid group where Durran stated that it was unlikely that mongoid would go in that direction.
11
+
12
+ This gem has been inspired by that conversation and also [Mongoid::Denormalize](https://github.com/logandk/mongoid_denormalize) which you should definitelly check out if you have similar needs.
13
+
14
+ 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.
15
+
16
+ Installation
17
+ ------------
18
+
19
+ Add the gem to your Bundler `Gemfile`:
20
+
21
+ gem 'mongoid-mirrored'
22
+
23
+ Or install with RubyGems:
24
+
25
+ $ gem install mongoid-mirrored
26
+
27
+
28
+ Usage
29
+ -----
30
+
31
+ In your root(master) model:
32
+
33
+ # Include the helper module
34
+ include Mongoid::Mirrored
35
+
36
+ # Define wich fields and methods will be shared among root and embedded classes
37
+ mirrored_in :articles, :users, :sync_direction => :both, :sync_events => :all do
38
+ field :contents, :type => String
39
+ field :vote_ratio, :type => Float
40
+
41
+ def calculate_vote_ratio
42
+ # do something with votes
43
+ end
44
+ end
45
+
46
+
47
+ Example
48
+ -------
49
+
50
+ # The conventioned name for the mirrored class is "#{embedding_class}::{root_class}"
51
+
52
+ class Post
53
+ embeds_many :comments, :class_name => "Post::Comment"
54
+ end
55
+
56
+ class User
57
+ embeds_many :comments, :class_name => "User::Comment"
58
+ end
59
+
60
+ # The Root Class should establish with which classes it is supposed to sync documents.
61
+ # Everything declared within the block will be shared between the root and mirrored
62
+ # documents (eg. fields, instance methods, class methods)
63
+
64
+ class Comment
65
+ mirrored_in :post, :user, :sync_direction => :both do
66
+ field :contents
67
+ field :vote_ratio, :type => Integer
68
+ def foo
69
+ "bar"
70
+ end
71
+ end
72
+ end
73
+
74
+ Options
75
+ -------
76
+
77
+ sync_events
78
+ -----------
79
+ :all(default) => sync master and mirrored documents on :after_create, :after_update and :after_destroy callbacks
80
+ :create => syncs only on :after_create
81
+ :update => syncs only on :after_update
82
+ :destroy => syncs only on :after_destroy
83
+ this options accepts an Array of events (eg: [:create, :destroy])
84
+
85
+ sync_direction
86
+ --------------
87
+ :both(default) => syncs documents on both directions. From master(root) to mirrors and from mirror to master
88
+ :from_root => syncs only from master to mirrors
89
+ :from_mirror => syncs only from mirror to master
90
+
91
+ replicate_to_siblings
92
+ ---------------------
93
+ true(default) => perform operations on the mirror's siblings
94
+ (eg: article.comments.create(:user_id => user.id) will replicate the document on the User::Comment collection)
95
+
96
+ inverse_of
97
+ ----------
98
+ :one =>
99
+ :many(default)
100
+
101
+ index
102
+ -----
103
+ false(default) => determines whether the root collection will create an index on the embedding collection's foreign_key
104
+
105
+ index_background
106
+ ----------------
107
+ false(default) => determines whether the aforementioned index will run on background
108
+
109
+
110
+ Rake tasks
111
+ ----------
112
+
113
+ should include tasks to re-sync
114
+
115
+
116
+ Known issues
117
+ ------------
118
+
119
+ - The helper does not support multiple calls of the mirrored_in method
120
+ - Changing parents from the embedded association does not update target documents. Use the master collection to change associations.
121
+ - eg post.comments.first.update_attribute(:post_id => Post.create) does not include the comment in the new Post comments list
122
+
123
+ Performance
124
+ ------------
125
+
126
+ perf/performance.rb
127
+
128
+ Credits
129
+ -------
130
+
131
+ Copyright (c) 2011 Alexandre Angelim, released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ require 'bundler'
4
+ require "rspec"
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new("spec:functional") do |spec|
8
+ spec.rspec_opts = %w(--format progress)
9
+ spec.pattern = "spec/functional/**/*_spec.rb"
10
+ end
11
+
12
+ task :default => :spec
13
+ task :test => :spec
14
+
15
+ require 'rake/rdoctask'
16
+ require "mongoid-mirrored/version"
17
+ Rake::RDocTask.new do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = "mongoid-mirrored #{Mongoid::Mirrored::VERSION}"
20
+ rdoc.rdoc_files.include('README*')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,2 @@
1
+ require 'mongoid'
2
+ require 'mongoid-mirrored/mirrored_in'
@@ -0,0 +1,39 @@
1
+ module Mongoid
2
+ module Mirrored
3
+ module HelperMethods
4
+ def extract_options(*args)
5
+ options = args.extract_options!
6
+ self.embedding_models = args
7
+ self.embedding_options = options
8
+
9
+ # set defaults
10
+ self.embedding_options[:sync_events] ||= :all
11
+ self.embedding_options[:sync_events] = [self.embedding_options[:sync_events]] unless embedding_options[:sync_events].is_a? Array
12
+ self.embedding_options[:sync_direction] ||= :both
13
+ self.embedding_options[:replicate_to_siblings] = true if self.embedding_options[:replicate_to_siblings].nil?
14
+ self.embedding_options[:inverse_of] ||= :many
15
+ self.embedding_options[:index] = false if self.embedding_options[:index].nil?
16
+ self.embedding_options[:background_index] = false if self.embedding_options[:background_index].nil?
17
+ end
18
+
19
+ # name of the association used by the embedding class
20
+ # eg: comments
21
+ def root_association
22
+ inverse_of = @root_klass.name.underscore
23
+ inverse_of = inverse_of.pluralize if embedding_options[:inverse_of] == :many
24
+ inverse_of
25
+ end
26
+
27
+ def embedding_klass(embedding_sym)
28
+ begin
29
+ _embedding_klass = embedding_sym.to_s.classify.constantize
30
+ rescue
31
+ Object.const_set embedding_sym.to_s.classify, Class.new
32
+ ensure
33
+ _embedding_klass = embedding_sym.to_s.classify.constantize
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,146 @@
1
+ module Mongoid
2
+ module Mirrored
3
+ module MirrorMethods
4
+
5
+ def embeds_mirror_in(_embedding_model, mirror_klass)
6
+ # mongoid macro embedded_in
7
+ # eg: embedded_in :post, :inverse_of => :comments
8
+
9
+ mirror_klass.class_eval <<-EOT
10
+ embedded_in :#{_embedding_model}, :inverse_of => :#{root_association}
11
+ EOT
12
+ end
13
+
14
+ def define_after_create_callback(_embedding_model, mirror_klass)
15
+ unless mirror_klass.instance_methods.include?(:_create_root)
16
+ mirror_klass.class_eval <<-EOF
17
+ after_create :_create_root
18
+ EOF
19
+ end
20
+
21
+ mirror_klass.class_eval <<-EOF
22
+ def _create_root
23
+ #{@root_klass}.collection.insert(attributes.merge(:#{_embedding_model}_id => #{_embedding_model}.id))
24
+ end
25
+ EOF
26
+ end
27
+ def define_after_update_callback(_embedding_model, mirror_klass)
28
+ unless mirror_klass.instance_methods.include?(:_update_root)
29
+ mirror_klass.class_eval <<-EOF
30
+ after_update :_update_root
31
+ EOF
32
+ end
33
+
34
+ mirror_klass.class_eval <<-EOF
35
+ def _update_root
36
+ #{@root_klass}.collection.update({ :_id => id }, '$set' => attributes.except('_id'))
37
+ end
38
+ EOF
39
+ end
40
+
41
+ def define_after_destroy_callback(_embedding_model, mirror_klass)
42
+ unless mirror_klass.instance_methods.include?(:_destroy_root)
43
+ mirror_klass.class_eval <<-EOF
44
+ after_destroy :_destroy_root
45
+ EOF
46
+ end
47
+
48
+ mirror_klass.class_eval <<-EOF
49
+ def _destroy_root
50
+ #{@root_klass}.collection.update({ :_id => id }, '$set' => attributes.except('_id'))
51
+ end
52
+ EOF
53
+ end
54
+
55
+ def define_after_create_siblings(_embedding_model, mirror_klass)
56
+ unless mirror_klass.instance_methods.include?(:_create_siblings)
57
+ mirror_klass.class_eval <<-EOF
58
+ after_create :_create_siblings
59
+ EOF
60
+ end
61
+ mirror_klass.class_eval <<-EOF
62
+ def _create_siblings
63
+ #{embedding_models}.each do |sibling|
64
+ _sibling = sibling.to_s
65
+ next if sibling == :#{_embedding_model}
66
+ sibling_klass = sibling.to_s.classify.constantize
67
+ if self[_sibling+"_id"]
68
+ sibling_klass.collection.update({ :_id => self[_sibling+"_id"] }, '$push' => {:#{root_association} => attributes.merge(:#{_embedding_model}_id => #{_embedding_model}.id)})
69
+ end
70
+ end
71
+ end
72
+ EOF
73
+ end
74
+
75
+ def define_after_update_siblings(_embedding_model, mirror_klass)
76
+ unless mirror_klass.instance_methods.include?(:_update_siblings)
77
+ mirror_klass.class_eval <<-EOF
78
+ after_update :_update_siblings
79
+ EOF
80
+ end
81
+ mirror_klass.class_eval <<-EOF
82
+ def _update_siblings
83
+ #{embedding_models}.each do |sibling|
84
+ _sibling = sibling.to_s
85
+ next if sibling == :#{_embedding_model}
86
+ sibling_klass = sibling.to_s.classify.constantize
87
+ if self[_sibling+"_id"]
88
+ nested_attr = {}
89
+ attributes.except("#{_embedding_model}_id").each_pair do |k,v|
90
+ nested_attr["#{root_association}.$."+k] = v
91
+ end
92
+ sibling_klass.collection.update({"#{root_association}._id" => id}, '$set' => nested_attr )
93
+ end
94
+ end
95
+ end
96
+ EOF
97
+ end
98
+
99
+ def define_after_destroy_siblings(_embedding_model, mirror_klass)
100
+ unless mirror_klass.instance_methods.include?(:_destroy_siblings)
101
+ mirror_klass.class_eval <<-EOF
102
+ after_destroy :_destroy_siblings
103
+ EOF
104
+ end
105
+ mirror_klass.class_eval <<-EOF
106
+ def _destroy_siblings
107
+ #{embedding_models}.each do |sibling|
108
+ _sibling = sibling.to_s
109
+ next if sibling == :#{_embedding_model}
110
+ sibling_klass = sibling.to_s.classify.constantize
111
+ if self[_sibling+"_id"]
112
+ sibling_klass.collection.update({ :_id => self[_sibling+"_id"] }, '$pull' => {:#{root_association} => { :_id => id}})
113
+ end
114
+ end
115
+ end
116
+ EOF
117
+ end
118
+
119
+ # Define callbacks for mirror class that don't trigger callbacks on the root class
120
+ def define_mirror_callbacks_for(_embedding_model, mirror_klass)
121
+ if [:both, :from_mirror].include?(embedding_options[:sync_direction])
122
+ if embedding_options[:sync_events].include?(:create) || embedding_options[:sync_events] == [:all]
123
+ define_after_create_callback(_embedding_model, mirror_klass)
124
+ if embedding_options[:replicate_to_siblings] && embedding_models.size > 1
125
+ define_after_create_siblings(_embedding_model, mirror_klass)
126
+ end
127
+ end
128
+
129
+ if embedding_options[:sync_events].include?(:update) || embedding_options[:sync_events] == [:all]
130
+ define_after_update_callback(_embedding_model, mirror_klass)
131
+ if embedding_options[:replicate_to_siblings] && embedding_models.size > 1
132
+ define_after_update_siblings(_embedding_model, mirror_klass)
133
+ end
134
+ end
135
+
136
+ if embedding_options[:sync_events].include?(:destroy) || embedding_options[:sync_events] == [:all]
137
+ define_after_destroy_callback(_embedding_model, mirror_klass)
138
+ if embedding_options[:replicate_to_siblings] && embedding_models.size > 1
139
+ define_after_destroy_siblings(_embedding_model, mirror_klass)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,41 @@
1
+ require "mongoid-mirrored/mirror/mirror_methods.rb"
2
+ require "mongoid-mirrored/root/root_methods.rb"
3
+ require "mongoid-mirrored/helper_methods.rb"
4
+
5
+ module Mongoid
6
+ module Mirrored
7
+ def self.included(base)
8
+ base.send(:extend, ClassMethods)
9
+ base.send :cattr_accessor, :embedding_models
10
+ base.send :cattr_accessor, :embedding_options
11
+ end
12
+
13
+ module ClassMethods
14
+ include Mongoid::Mirrored::MirrorMethods
15
+ include Mongoid::Mirrored::RootMethods
16
+ include Mongoid::Mirrored::HelperMethods
17
+
18
+ def mirrored_in(*args, &block)
19
+ extract_options(*args)
20
+ write_fields_with_options { yield }
21
+ @root_klass = self
22
+ # creates a Mirrored class for each embedding model
23
+ embedding_models.each do |embedding_model|
24
+ mirror_klass = Class.new do
25
+ include Mongoid::Document
26
+
27
+ # includes all fields and methods declared when calling mirrored_in
28
+ class_eval &block
29
+ end
30
+
31
+ define_mirror_callbacks_for(embedding_model, mirror_klass)
32
+ embeds_mirror_in(embedding_model, mirror_klass)
33
+ _embedding_klass = embedding_klass(embedding_model)
34
+
35
+ # Creates the mirrored class Embedding::Root
36
+ _embedding_klass.const_set self.name, mirror_klass
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,124 @@
1
+ module Mongoid
2
+ module Mirrored
3
+ module RootMethods
4
+ # Define callbacks for root class that don't trigger callbacks on the mirror classes
5
+ def define_root_callbacks
6
+ if [:both, :from_root].include?(embedding_options[:sync_direction])
7
+ if embedding_options[:sync_events].include?(:create) || embedding_options[:sync_events] == [:all]
8
+ define_create_mirrors
9
+ end
10
+
11
+ if embedding_options[:sync_events].include?(:update) || embedding_options[:sync_events] == [:all]
12
+ define_update_mirrors
13
+ end
14
+
15
+ if embedding_options[:sync_events].include?(:destroy) || embedding_options[:sync_events] == [:all]
16
+ define_destroy_mirrors
17
+ end
18
+ end
19
+ end
20
+
21
+ def define_create_mirrors
22
+ unless self.instance_methods.include?(:_create_mirrors)
23
+ self.class_eval <<-EOF
24
+ after_create :_create_mirrors
25
+ EOF
26
+ end
27
+ define_method :_create_mirrors do
28
+
29
+ # each embedded class will be touched by the callbacks
30
+ # this should be used with care or write operations could get very slow
31
+ embedding_models.each do |embedding_model|
32
+ # attributes that will be used to define the root and embedding classes and instances
33
+ embedding_string = embedding_model.to_s
34
+ _embedding_klass = self.class.embedding_klass(embedding_model)
35
+ embedding_instance = eval(embedding_string)
36
+
37
+ # Only tries to create mirrored document if the embedding instance is given
38
+ if embedding_instance
39
+ _embedding_klass.collection.update({ :_id => embedding_instance.id }, '$push' => {self.class.root_association => attributes.except("#{embedding_string}_id")})
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def define_update_mirrors
46
+ unless self.instance_methods.include?(:_update_mirrors)
47
+ self.class_eval <<-EOF
48
+ before_update :_update_mirrors
49
+ EOF
50
+ end
51
+ # updates the mirrored document when one or more attributes of the parent document is changed
52
+ # if the root document changes the embedding document, the mirrored document is deleted from the previous list
53
+ # and another mirrored document is created for the new embedding document
54
+ define_method :_update_mirrors do
55
+ embedding_models.each do |embedding_model|
56
+
57
+ # attributes that will be used to define the root and embedding classes and instances
58
+ embedding_string = embedding_model.to_s
59
+ embedding_instance = eval(embedding_string)
60
+ _embedding_klass = self.class.embedding_klass(embedding_model)
61
+
62
+ if embedding_instance
63
+ if eval("#{embedding_string}_id_changed?")
64
+ _create_mirrors
65
+ _destroy_mirrors(eval("#{embedding_string}_id_was"))
66
+ else
67
+ # using positional modifier $ to find embedded document to be updated
68
+ # traverses the attributes hash to inject positional modifier
69
+ # eg: contents => ;comments.$.contents =>
70
+ nested_attr = {}
71
+ attributes.except("#{embedding_string}_id").each_pair do |k,v|
72
+ nested_attr["#{self.class.root_association}.$.#{k}"] = v
73
+ end
74
+ _embedding_klass.collection.update({"#{self.class.root_association}._id" => id}, '$set' => nested_attr )
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ def define_destroy_mirrors
82
+ unless self.instance_methods.include?(:_destroy_mirrors)
83
+ self.class_eval <<-EOF
84
+ after_destroy :_destroy_mirrors
85
+ EOF
86
+ end
87
+ # destroys the mirrored document when the destroy method is called on the root document
88
+ # or when the root document establishes a relationship with another embedding document
89
+ define_method :_destroy_mirrors do |changed_embedding_instance = nil|
90
+ embedding_models.each do |embedding_model|
91
+
92
+ # attributes that will be used to define the root and embedding classes and instances
93
+ embedding_string = embedding_model.to_s
94
+ _embedding_klass = self.class.embedding_klass(embedding_model)
95
+ embedding_instance = eval(embedding_string)
96
+ if embedding_instance
97
+ id_to_destroy = changed_embedding_instance || embedding_instance.id
98
+ _embedding_klass.collection.update({ :_id => id_to_destroy }, '$pull' => {self.class.root_association => { :_id => id}})
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ # writes the block passed to the mirrored_in method in the Root Calss
105
+ # defines instance methods used in callbacks triggered by the root documents
106
+ def write_fields_with_options(&block)
107
+ index_params = ""
108
+ index_params << ", :index => true" if embedding_options[:index]
109
+ index_params << ", :background => true" if embedding_options[:index] && embedding_options[:background_index]
110
+
111
+ embedding_models.each do |embedding_model|
112
+ self.class_eval <<-EOT
113
+ belongs_to :#{embedding_model} #{index_params}
114
+ EOT
115
+ end
116
+ # writes shared fields and methods to root document
117
+ yield
118
+
119
+ define_root_callbacks
120
+
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module Mirrored
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
@@ -0,0 +1,161 @@
1
+ require "benchmark"
2
+ require "mongoid"
3
+
4
+ def rand_post(posts)
5
+ posts[rand(posts.size)]
6
+ end
7
+
8
+ Mongoid.configure do |config|
9
+ config.master = Mongo::Connection.new.db("mongoid_perf_test")
10
+ end
11
+ Mongoid.logger = Logger.new($stdout)
12
+
13
+ Mongoid.master.collections.select {|c| c.name !~ /system/ }.each(&:drop)
14
+
15
+ class Post
16
+ include Mongoid::Document
17
+ include Mongoid::Timestamps
18
+
19
+ field :title
20
+ # embeds_many :comments, :class_name => "Post::Comment"
21
+ has_many :comments
22
+
23
+ end
24
+
25
+ class Comment
26
+ include Mongoid::Document
27
+ include Mongoid::Timestamps
28
+ field :contents
29
+ # include Mongoid::Mirrored
30
+
31
+ # mirrored_in :post, :belongs => true do
32
+ # field :contents
33
+ # field :counter, :type => Integer
34
+ # end
35
+ belongs_to :post, :index => true
36
+ end
37
+
38
+ Benchmark.bm do |bm|
39
+ 10000.times do
40
+ Post.create
41
+ end
42
+
43
+ posts = Post.all.to_a
44
+
45
+ bm.report "criando comments da raiz" do
46
+ 10000.times do
47
+ Comment.create(:post => rand_post(posts))
48
+ end
49
+ end
50
+
51
+ bm.report "atualizando comments da raiz" do
52
+ Comment.all.each_with_index do |c,i|
53
+ c.update_attribute(:contents, i)
54
+ end
55
+ end
56
+
57
+
58
+ bm.report "leitura de var no relacionamento" do
59
+ posts.each do |p|
60
+ p.comments.each do |c|
61
+ (con ||= []) << c.contents
62
+ end
63
+ end
64
+ end
65
+
66
+ bm.report "apagando comments da raiz" do
67
+ Comment.all.map(&:destroy)
68
+ end
69
+
70
+ bm.report "criando comments do embedded" do
71
+ 10000.times do
72
+ p = rand_post(posts)
73
+ p.comments.create
74
+ end
75
+ end
76
+
77
+ bm.report "atualizando comments da embedded" do
78
+ posts.each do |p|
79
+ p.comments.each_with_index do |c,i|
80
+ c.update_attribute(:contents, i)
81
+ end
82
+ end
83
+ end
84
+
85
+ bm.report "apagando comments da embedded" do
86
+ posts.each do |p|
87
+ p.comments.map(&:destroy)
88
+ end
89
+ end
90
+ end
91
+
92
+ Mongoid.master.collections.select {|c| c.name !~ /system/ }.each(&:drop)
93
+
94
+ class Post
95
+ embeds_many :comments, :class_name => "Post::Comment"
96
+ # Post.collection.create_index "comments._id"
97
+ end
98
+
99
+ class Comment
100
+ include Mongoid::Mirrored
101
+
102
+ mirrored_in :post, :belongs => true do
103
+ field :contents
104
+ field :counter, :type => Integer
105
+ end
106
+ end
107
+
108
+
109
+ Benchmark.bm do |bm|
110
+ 10000.times do
111
+ Post.create
112
+ end
113
+
114
+ posts = Post.all.to_a
115
+
116
+ bm.report "criando comments da raiz" do
117
+ 10000.times do
118
+ Comment.create(:post => rand_post(posts))
119
+ end
120
+ end
121
+
122
+ bm.report "atualizando comments da raiz" do
123
+ Comment.all.each_with_index do |c,i|
124
+ c.update_attribute(:contents, i)
125
+ end
126
+ end
127
+
128
+
129
+ bm.report "leitura de var no relacionamento" do
130
+ posts.each do |p|
131
+ p.comments.each do |c|
132
+ (con ||= []) << c.contents
133
+ end
134
+ end
135
+ end
136
+
137
+ bm.report "apagando comments da raiz" do
138
+ Comment.all.map(&:destroy)
139
+ end
140
+
141
+ bm.report "criando comments do embedded" do
142
+ 10000.times do
143
+ p = rand_post(posts)
144
+ p.comments.create
145
+ end
146
+ end
147
+
148
+ bm.report "atualizando comments da embedded" do
149
+ posts.each do |p|
150
+ p.comments.each_with_index do |c,i|
151
+ c.update_attribute(:contents, i)
152
+ end
153
+ end
154
+ end
155
+
156
+ bm.report "apagando comments da embedded" do
157
+ posts.each do |p|
158
+ p.comments.map(&:destroy)
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mongoid-mirrored/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "mongoid-mirrored"
7
+ s.version = Mongoid::Mirrored::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Alexandre Angelim"]
10
+ s.email = %q{angelim@angelim.com.br}
11
+ s.homepage = %q{http://github.com/angelim/mongoid-mirrored}
12
+ s.summary = %q{Mirrored Embeds for Mongoid}
13
+ s.description = %q{Create mirrors of root documents embeded in other models and keep them in sync}
14
+
15
+ s.rubyforge_project = "mongoid-mirrored"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_runtime_dependency(%q<mongoid>, ["~> 2.0"])
23
+ s.add_development_dependency("rspec", ["~> 2.6"])
24
+ s.add_development_dependency("bson_ext", ["~> 1.3"])
25
+ end
@@ -0,0 +1,146 @@
1
+ # test methods
2
+ # test from_mirror
3
+ require 'spec_helper'
4
+
5
+ describe "Comment" do
6
+ let(:article) { Article.create }
7
+ let(:user) { User.create }
8
+ context "creating from the root collection" do
9
+ it "should create document in root collection" do
10
+ c = Comment.create(:article => article, :contents => "root", :author => "root_author" )
11
+ Comment.where(:contents => "root").first.should == c
12
+ end
13
+
14
+ it "should create similar document in each embedding collection" do
15
+ c = Comment.create(:article => article, :user => user, :contents => "root", :author => "root_author" )
16
+ article.reload
17
+ article.comments.find(c.id.to_s).should_not be_nil
18
+ user.reload
19
+ user.comments.find(c.id.to_s).should_not be_nil
20
+ end
21
+
22
+ end
23
+
24
+ context "creating from the embedding collection" do
25
+ it "should create document in root collection" do
26
+ c = article.comments.create(:contents => "embedding", :author => "embedding_author")
27
+ Comment.find(c.id).should_not be_nil
28
+ end
29
+
30
+ it "should create similar document in embedding collection" do
31
+ c = article.comments.create(:contents => "embedding", :author => "embedding_author")
32
+ article.reload
33
+ c.article.should == article
34
+ end
35
+
36
+ it "should replicate document to sibling collections" do
37
+ c = article.comments.create(:user_id => user.id, :contents => "embedding", :author => "embedding_author")
38
+ user.reload
39
+ user.comments.find(c.id).should_not be_nil
40
+ end
41
+ end
42
+
43
+ context "updating from the root collection" do
44
+ it "should update document in root collection" do
45
+ c = Comment.create(:article => article, :contents => "root", :author => "root_author" )
46
+ c.update_attributes(:contents => "new_root", :author => "new_author_root")
47
+ Comment.where(:contents => "new_root", :author => "new_author_root").first.should == c
48
+
49
+ end
50
+
51
+ it "should update document in embedding collection with new attributes" do
52
+ c = Comment.create(:article => article, :user => user, :contents => "root", :author => "root_author" )
53
+ c.update_attributes(:contents => "new_root", :author => "new_author_root")
54
+ article.reload
55
+ article.comments.where(:contents => "new_root", :author => "new_author_root").first.id.should == c.id
56
+ user.reload
57
+ user.comments.where(:contents => "new_root", :author => "new_author_root").first.id.should == c.id
58
+ end
59
+ context "switching embedding document" do
60
+ it "should delete comment from embedding original collection" do
61
+ c = article.comments.create(:contents => "embedding", :author => "embedding_author")
62
+ Comment.first.update_attributes(:article_id => Article.create.id, :user_id => User.create.id)
63
+ article.reload
64
+ article.comments.should be_empty
65
+ user.reload
66
+ user.comments.should be_empty
67
+ end
68
+ it "should create comment in new embedding collection" do
69
+ c = article.comments.create(:contents => "embedding", :author => "embedding_author")
70
+ Comment.first.update_attribute(:article_id, Article.create.id)
71
+ Article.last.comments.where(:contents => "embedding").should_not be_empty
72
+ end
73
+ end
74
+ end
75
+
76
+ context "updating from the embedding collection" do
77
+ it "should update document in root collection with new attributes" do
78
+ c = article.comments.create(:contents => "embedding", :author => "embedding_author")
79
+ c.update_attributes(:contents => "new_embedding", :author => "new_author_embedding")
80
+ Comment.where(:contents => "new_embedding", :author => "new_author_embedding").first.id.should == c.id
81
+ end
82
+
83
+ it "should update similar document in embedding collection" do
84
+ c = article.comments.create(:contents => "embedding", :author => "embedding_author")
85
+ c.update_attributes(:contents => "new_embedding", :author => "new_author_embedding")
86
+ article.reload
87
+ article.comments.where(:contents => "new_embedding", :author => "new_author_embedding").first.id.should == c.id
88
+ end
89
+
90
+ it "should replicate changes to sibling collections" do
91
+ c = article.comments.create(:user_id => user.id, :contents => "embedding", :author => "embedding_author")
92
+ c.update_attributes(:contents => "new_embedding", :author => "new_author_embedding")
93
+ user.reload
94
+ user.comments.find(c.id).contents.should == "new_embedding"
95
+ end
96
+ end
97
+
98
+ context "destroying from the root collection" do
99
+ it "should destroy document in root collection" do
100
+ c = Comment.create(:article => article, :user => user, :contents => "root", :author => "root_author" )
101
+ c.destroy
102
+ Comment.where(:content => "root").should be_empty
103
+ end
104
+
105
+ it "should destroy similar document in each embedding collection" do
106
+ c = Comment.create(:article => article, :user => user, :contents => "root", :author => "root_author" )
107
+ c.destroy
108
+ article.reload
109
+ article.comments.where(:contents => "root").should be_empty
110
+ user.reload
111
+ user.comments.where(:contents => "root").should be_empty
112
+ end
113
+ end
114
+
115
+ context "destroy from the embedding collection" do
116
+ it "should destroy document in root collection" do
117
+ c = article.comments.create(:contents => "embedding", :author => "embedding_author")
118
+ c.destroy
119
+ Comment.where(:content => "embedding").should be_empty
120
+ end
121
+
122
+ it "should destroy similar document from embedding collection" do
123
+ c = article.comments.create(:contents => "embedding", :author => "embedding_author")
124
+ c.destroy
125
+ article.reload
126
+ article.comments.where(:content => "embedding").should be_empty
127
+ end
128
+
129
+ it "should destroy document from sibling collections" do
130
+ c = article.comments.create(:user_id => user.id, :contents => "embedding", :author => "embedding_author")
131
+ c.destroy
132
+ user.reload
133
+ user.comments.where(:_id => c.id).should be_empty
134
+ end
135
+ end
136
+
137
+ context "shared methods between root and mirrored classes" do
138
+ it "should be able to call shared methods from the root collection" do
139
+ Comment.new.foo.should == "bar"
140
+ end
141
+ it "should be able to call shared methods from the mirrored collection" do
142
+ article.comments.new.foo.should == "bar"
143
+ end
144
+ end
145
+
146
+ end
@@ -0,0 +1,11 @@
1
+ class Article
2
+ include Mongoid::Document
3
+
4
+ field :title
5
+ index :title
6
+ field :content
7
+ field :published, :type => Boolean, :default => false
8
+
9
+ embeds_many :comments, :class_name => "Article::Comment"
10
+ end
11
+
@@ -0,0 +1,15 @@
1
+ class Comment
2
+ include Mongoid::Document
3
+ include Mongoid::Mirrored
4
+
5
+ field :dummy
6
+ mirrored_in :article, :user do
7
+ field :author
8
+ field :contents
9
+ field :like_count, :type => Integer, :default => 0
10
+
11
+ def foo
12
+ "bar"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ class CommentWithLessOption
2
+ include Mongoid::Document
3
+ include Mongoid::Mirrored
4
+
5
+ field :dummy
6
+ mirrored_in :article,
7
+ :sync_direction => :from_root,
8
+ :replicate_to_siblings => true do
9
+
10
+ field :author
11
+ field :contents
12
+ field :like_count, :type => Integer, :default => 0
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ class CommentWithOption
2
+ include Mongoid::Document
3
+ include Mongoid::Mirrored
4
+
5
+ field :dummy
6
+ mirrored_in :article,
7
+ :inverse_of => :one,
8
+ :sync_direction => :from_mirror,
9
+ :sync_events => [:create, :update],
10
+ :index => true,
11
+ :background_index => true,
12
+ :replicate_to_siblings => false do
13
+
14
+ field :author
15
+ field :contents
16
+ field :like_count, :type => Integer, :default => 0
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ class User
2
+ include Mongoid::Document
3
+ field :name
4
+ embeds_many :comments, :class_name => "User::Comment"
5
+ end
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))
3
+ MODELS = File.join(File.dirname(__FILE__), "models")
4
+ $LOAD_PATH.unshift(MODELS)
5
+
6
+ require "rubygems"
7
+ require "bundler"
8
+ Bundler.setup
9
+
10
+ require 'rspec'
11
+ require 'mongoid'
12
+ require 'mongoid-mirrored'
13
+
14
+ Mongoid.configure do |config|
15
+ name = "mongoid-mirrored-test"
16
+ host = "localhost"
17
+ config.master = Mongo::Connection.new.db(name)
18
+ config.autocreate_indexes = true
19
+ end
20
+
21
+ Dir[ File.join(MODELS, "*.rb") ].sort.each { |file| require File.basename(file) }
22
+
23
+ RSpec.configure do |config|
24
+ config.mock_with :rspec
25
+ config.after :each do
26
+ Mongoid.master.collections.select {|c| c.name !~ /system/ }.each(&:drop)
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Mongoid::Mirrored" do
4
+
5
+ context "Embedding Class" do
6
+ it "should embeds the mirrored document under the correct class name" do
7
+ Article.reflect_on_association(:comments).class_name.should == "Article::Comment"
8
+ end
9
+ end
10
+
11
+ context "Root Class" do
12
+ let(:comment) { Comment.new }
13
+
14
+ it "should reflect on association with Article" do
15
+ comment.fields.should have_key "article_id"
16
+ comment.reflect_on_association(:article).macro.should == :referenced_in
17
+ end
18
+ end
19
+
20
+ context "Article::Comment Mirror Class" do
21
+ let(:mirror) { Article::Comment.new }
22
+
23
+ it "should reflect on association with Article" do
24
+ mirror.reflect_on_association(:article).macro.should == :embedded_in
25
+ mirror.reflect_on_association(:article).inverse_of.should == :comments
26
+ end
27
+
28
+ end
29
+
30
+ context "Article::CommentWithOption Mirror Class" do
31
+ let(:mirror) { Article::CommentWithOption.new }
32
+
33
+ it "should reflect on association with Article" do
34
+ mirror.reflect_on_association(:article).macro.should == :embedded_in
35
+ mirror.reflect_on_association(:article).inverse_of.should == :comment_with_option
36
+ end
37
+
38
+ end
39
+
40
+ context "Article::CommentWithLessOption Mirror Class" do
41
+ let(:mirror) { Article::CommentWithLessOption.new }
42
+
43
+ it "should reflect on association with Article" do
44
+ mirror.reflect_on_association(:article).macro.should == :embedded_in
45
+ mirror.reflect_on_association(:article).inverse_of.should == :comment_with_less_options
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Mongoid::Mirrored" do
4
+
5
+ context "Root Class" do
6
+ let(:comment) { Comment.new }
7
+
8
+ it "should define callbacks if sync strategy is ..." do
9
+ Comment._create_callbacks.map(&:filter).should include :create_mirror
10
+ Comment._update_callbacks.map(&:filter).should include :update_mirror
11
+ Comment._destroy_callbacks.map(&:filter).should include :destroy_mirror
12
+ end
13
+
14
+ end
15
+
16
+ context "Article::Comment Mirror Class" do
17
+ let(:mirror) { Article::Comment.new }
18
+
19
+ it "should define callbacks for sync strategy" do
20
+ mirror._create_callbacks.map(&:filter).should include :_create_root
21
+ mirror._update_callbacks.map(&:filter).should include :_update_root
22
+ mirror._destroy_callbacks.map(&:filter).should include :_destroy_root
23
+ end
24
+ end
25
+
26
+ context "Article::CommentWithOption Mirror Class" do
27
+ let(:mirror) { Article::CommentWithOption.new }
28
+
29
+ it "should define callbacks for sync strategy" do
30
+ mirror._create_callbacks.map(&:filter).should include :_create_root
31
+ mirror._update_callbacks.map(&:filter).should include :_update_root
32
+ mirror._destroy_callbacks.map(&:filter).should_not include :_destroy_root
33
+ end
34
+ end
35
+
36
+ context "Article::CommentWithLessOption Mirror Class" do
37
+ let(:mirror) { Article::CommentWithLessOption.new }
38
+
39
+ it "should define callbacks for sync strategy" do
40
+ mirror._create_callbacks.map(&:filter).should_not include :_create_root
41
+ mirror._update_callbacks.map(&:filter).should_not include :_update_root
42
+ mirror._destroy_callbacks.map(&:filter).should_not include :_destroy_root
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Mongoid::Mirrored" do
4
+
5
+
6
+ context "Article::CommentWithOption Mirror Class" do
7
+ let(:mirror) { Article::CommentWithOption.new }
8
+
9
+ it "should create index on association with Article" do
10
+ CommentWithOption.collection.index_information().should include("article_id_1")
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Mongoid::Mirrored" do
4
+ it "#sync_events" do
5
+ Comment.embedding_options[:sync_events].should == [:all]
6
+ CommentWithOption.embedding_options[:sync_events].should == [:create, :update]
7
+ CommentWithLessOption.embedding_options[:sync_events].should == [:all]
8
+ end
9
+
10
+ it "#sync_direction" do
11
+ Comment.embedding_options[:sync_direction].should == :both
12
+ CommentWithOption.embedding_options[:sync_direction].should == :from_mirror
13
+ CommentWithLessOption.embedding_options[:sync_direction].should == :from_root
14
+ end
15
+
16
+ it "#replicate_to_siblings" do
17
+ Comment.embedding_options[:replicate_to_siblings].should == true
18
+
19
+ CommentWithOption.embedding_options[:replicate_to_siblings].should == false
20
+
21
+ CommentWithLessOption.embedding_options[:replicate_to_siblings].should == true
22
+ end
23
+
24
+ it "#inverse_of" do
25
+ Comment.embedding_options[:inverse_of].should == :many
26
+ CommentWithOption.embedding_options[:inverse_of].should == :one
27
+ CommentWithLessOption.embedding_options[:inverse_of].should == :many
28
+ end
29
+
30
+ it "#index" do
31
+ Comment.embedding_options[:index].should == false
32
+ CommentWithOption.embedding_options[:index].should == true
33
+ CommentWithLessOption.embedding_options[:index].should == false
34
+ end
35
+
36
+ it "#background_index" do
37
+ Comment.embedding_options[:background_index].should == false
38
+ CommentWithOption.embedding_options[:background_index].should == true
39
+ CommentWithLessOption.embedding_options[:background_index].should == false
40
+ end
41
+
42
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Mongoid::Mirrored" do
4
+
5
+
6
+ context "Root Class" do
7
+ let(:comment) { Comment.new }
8
+
9
+ it "should include shared fields with correct type and default definition" do
10
+ comment.fields.should have_key "author"
11
+ comment.fields.should have_key "contents"
12
+ comment.fields.should have_key "like_count"
13
+ comment.fields["like_count"].type.should == Integer
14
+ comment.fields["like_count"].default.should == 0
15
+ end
16
+ end
17
+
18
+ context "Article::Comment Mirror Class" do
19
+ let(:mirror) { Article::Comment.new }
20
+
21
+ it "should include shared fields with correct type and default definition" do
22
+ mirror.fields.should have_key "author"
23
+ mirror.fields.should have_key "contents"
24
+ mirror.fields.should have_key "like_count"
25
+ mirror.fields["like_count"].type.should == Integer
26
+ mirror.fields["like_count"].default.should == 0
27
+ end
28
+ end
29
+
30
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-mirrored
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 2
9
+ version: 0.0.2
10
+ platform: ruby
11
+ authors:
12
+ - Alexandre Angelim
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-05-24 00:00:00 -03:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: mongoid
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 2
30
+ - 0
31
+ version: "2.0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 2
44
+ - 6
45
+ version: "2.6"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: bson_ext
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 1
58
+ - 3
59
+ version: "1.3"
60
+ type: :development
61
+ version_requirements: *id003
62
+ description: Create mirrors of root documents embeded in other models and keep them in sync
63
+ email: angelim@angelim.com.br
64
+ executables: []
65
+
66
+ extensions: []
67
+
68
+ extra_rdoc_files: []
69
+
70
+ files:
71
+ - .document
72
+ - .gitignore
73
+ - .rvmrc
74
+ - Gemfile
75
+ - LICENSE
76
+ - README.md
77
+ - Rakefile
78
+ - lib/mongoid-mirrored.rb
79
+ - lib/mongoid-mirrored/helper_methods.rb
80
+ - lib/mongoid-mirrored/mirror/mirror_methods.rb
81
+ - lib/mongoid-mirrored/mirrored_in.rb
82
+ - lib/mongoid-mirrored/root/root_methods.rb
83
+ - lib/mongoid-mirrored/version.rb
84
+ - lib/performance/performance.rb
85
+ - mongoid-mirrored.gemspec
86
+ - spec/functional/mongoid/comment_spec.rb
87
+ - spec/models/article.rb
88
+ - spec/models/comment.rb
89
+ - spec/models/comment_with_less_option.rb
90
+ - spec/models/comment_with_option.rb
91
+ - spec/models/user.rb
92
+ - spec/spec_helper.rb
93
+ - spec/unit/associations_spec.rb
94
+ - spec/unit/callback_specs.rb
95
+ - spec/unit/index_spec.rb
96
+ - spec/unit/options_spec.rb
97
+ - spec/unit/shared_fields_spec.rb
98
+ has_rdoc: true
99
+ homepage: http://github.com/angelim/mongoid-mirrored
100
+ licenses: []
101
+
102
+ post_install_message:
103
+ rdoc_options: []
104
+
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ segments:
113
+ - 0
114
+ version: "0"
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ segments:
121
+ - 0
122
+ version: "0"
123
+ requirements: []
124
+
125
+ rubyforge_project: mongoid-mirrored
126
+ rubygems_version: 1.3.7
127
+ signing_key:
128
+ specification_version: 3
129
+ summary: Mirrored Embeds for Mongoid
130
+ test_files:
131
+ - spec/functional/mongoid/comment_spec.rb
132
+ - spec/models/article.rb
133
+ - spec/models/comment.rb
134
+ - spec/models/comment_with_less_option.rb
135
+ - spec/models/comment_with_option.rb
136
+ - spec/models/user.rb
137
+ - spec/spec_helper.rb
138
+ - spec/unit/associations_spec.rb
139
+ - spec/unit/callback_specs.rb
140
+ - spec/unit/index_spec.rb
141
+ - spec/unit/options_spec.rb
142
+ - spec/unit/shared_fields_spec.rb