mongoid-mirrored 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +131 -0
- data/Rakefile +22 -0
- data/lib/mongoid-mirrored.rb +2 -0
- data/lib/mongoid-mirrored/helper_methods.rb +39 -0
- data/lib/mongoid-mirrored/mirror/mirror_methods.rb +146 -0
- data/lib/mongoid-mirrored/mirrored_in.rb +41 -0
- data/lib/mongoid-mirrored/root/root_methods.rb +124 -0
- data/lib/mongoid-mirrored/version.rb +5 -0
- data/lib/performance/performance.rb +161 -0
- data/mongoid-mirrored.gemspec +25 -0
- data/spec/functional/mongoid/comment_spec.rb +146 -0
- data/spec/models/article.rb +11 -0
- data/spec/models/comment.rb +15 -0
- data/spec/models/comment_with_less_option.rb +14 -0
- data/spec/models/comment_with_option.rb +18 -0
- data/spec/models/user.rb +5 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/unit/associations_spec.rb +49 -0
- data/spec/unit/callback_specs.rb +45 -0
- data/spec/unit/index_spec.rb +14 -0
- data/spec/unit/options_spec.rb +42 -0
- data/spec/unit/shared_fields_spec.rb +30 -0
- metadata +142 -0
data/.document
ADDED
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.2
|
data/Gemfile
ADDED
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,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,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,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
|
data/spec/models/user.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|