mongoid_max_denormalize 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -6,8 +6,13 @@ It was designed for a minimum number of queries to the database.
6
6
 
7
7
  For now, support only Mongoid 3.
8
8
 
9
- * Propagate only when needed
10
- * Take advantage of atomic operations on multi documents of MongoDB
9
+ * Denormalize fields
10
+ * Denormalize methods (only in One to Many situations for now)
11
+ * Denormalize `count` in Many to One situations
12
+ * Propagate only when needed:
13
+ * for fields: when there are actual changes
14
+ * for methods: always (we can't know in an inexpensive way what is the old value to figure out if there is a change)
15
+ * Take advantage of atomic operations on multiple documents of MongoDB
11
16
 
12
17
  *This is a pre-version not suitable for production.*
13
18
 
@@ -34,6 +39,13 @@ Add `include Mongoid::Max::Denormalize` in your model and also:
34
39
  denormalize relation, field_1, field_2 ... field_n, options
35
40
 
36
41
 
42
+ ### Warming up
43
+
44
+ If there are existing records prior to the denormalization setup, you have to warm up. See below for each relation type.
45
+
46
+ Note: you can't warm up from both sides of the relation. Only the most efficient is available.
47
+
48
+
37
49
  ### One to Many
38
50
 
39
51
  **Supported fields:** normal Mongoid fields, and methods.
@@ -58,7 +70,7 @@ Example :
58
70
  include Mongoid::Document
59
71
  include Mongoid::Max::Denormalize
60
72
 
61
- belons_to :post
73
+ belongs_to :post
62
74
  denormalize :post, :title, :slug
63
75
  end
64
76
 
@@ -72,6 +84,10 @@ Example :
72
84
  @comment.post_title #=> "All Must Share The Burden"
73
85
  @comment.post_slug #=> "all-must-share-the-burden"
74
86
 
87
+ To warm up the denormalization for an existing collection:
88
+
89
+ Post.denormalize_to_comments!
90
+
75
91
  **Tips :** In your views, do not use `@comment.post` but `@comment.post_id` or `@comment.post_id?`
76
92
  to avoid a query that checks/retrieve for the post. We want to avoid it, don't we ?
77
93
 
@@ -116,7 +132,7 @@ Example :
116
132
  class Comment
117
133
  include Mongoid::Document
118
134
 
119
- belons_to :post
135
+ belongs_to :post
120
136
 
121
137
  field :rating
122
138
  field :stuff
@@ -130,10 +146,14 @@ Example :
130
146
  @post.comments_rating #=> [5, 3]
131
147
  @post.comments_stuff #=> ["A", "B"]
132
148
 
149
+ To warm up the denormalization for an existing collection:
150
+
151
+ Post.denormalize_from_comments!
152
+
133
153
  You can see that each denormalized field in stored in a separate array. This is wanted.
134
- An option `:group` will come to allow :
154
+ An option `:group` will come to allow the way below (and maybe permit methods denormalization) :
135
155
 
136
- @post.comments_fields #=> [{:rating => 5, :stuff => "A"},{:rating => 5, :stuff => "B"}]
156
+ @post.comments_fields #=> [{:rating => 5, :stuff => "A"}, {:rating => 5, :stuff => "B"}]
137
157
 
138
158
 
139
159
  ### Many to One
@@ -142,6 +162,16 @@ To come...
142
162
 
143
163
 
144
164
 
165
+ ## Planned
166
+
167
+ * Support for Many to Many
168
+ * Support for `:group` option in Many to One
169
+ * Support for methods denormalization in Many to One (depends on `:group` option)
170
+ * Support for `:sum` and `:mean` options in Many to One
171
+ * Support for `:touch` option to "touch" an `updated_at` field (for cache purpose)
172
+
173
+
174
+
145
175
  ## Contributing
146
176
 
147
177
  Contributions and bug reports are welcome.
@@ -13,8 +13,34 @@ module Mongoid
13
13
  @inverse_meta = inverse_meta
14
14
  @fields = fields
15
15
  @options = options
16
+
17
+ verify
18
+ end
19
+
20
+ def verify
21
+ # There are fields
22
+ raise ConfigError.new("Nothing to denormalize", klass, relation) if fields.empty? && options.empty?
23
+
24
+ # All fields/methods are well defined
25
+ fields.each do |field|
26
+ unless meta.klass.instance_methods.include? field
27
+ raise ConfigError.new("Unknown field or method :#{field}", klass, relation)
28
+ end
29
+ end
30
+
31
+ # All options are allowed
32
+ unless unallowed_options.empty?
33
+ raise ConfigError.new("Unknown or not supported options :#{unallowed_options.first}", klass, relation)
34
+ end
16
35
  end
17
36
 
37
+ def allowed_options
38
+ []
39
+ end
40
+
41
+ def unallowed_options
42
+ options.keys - allowed_options
43
+ end
18
44
 
19
45
  def relation
20
46
  @meta.name
@@ -4,8 +4,8 @@ module Mongoid
4
4
  module Denormalize
5
5
  class ConfigError < ::StandardError
6
6
 
7
- def initialize(summary, klass)
8
- super("[#{klass}.denormalize] #{summary}")
7
+ def initialize(summary, klass, relation=nil)
8
+ super("[%s.denormalize%s] %s" % [klass, relation && " :#{relation}", summary])
9
9
  end
10
10
 
11
11
  end
@@ -5,10 +5,17 @@ module Mongoid
5
5
 
6
6
  class ManyToOne < Base
7
7
 
8
- def attach
8
+ def verify
9
+ super
9
10
 
10
- fields.each do |field|
11
- klass.field "#{relation}_#{field}", type: Array
11
+ unless fields_methods.empty?
12
+ raise ConfigError.new("Methods denormalization not supported for Many to One", klass, relation)
13
+ end
14
+ end
15
+
16
+ def attach
17
+ fields_only.each do |field|
18
+ klass.field "#{relation}_#{field}", type: Array, default: []
12
19
  end
13
20
 
14
21
  if has_count?
@@ -16,26 +23,36 @@ module Mongoid
16
23
  end
17
24
 
18
25
  callback_code = <<EOM
19
- before_save :denormalize_from_#{relation}
26
+ before_create :denormalize_from_#{relation}
20
27
 
21
- def denormalize_from_#{relation}
22
- return unless #{meta.key}_changed?
28
+ def denormalize_from_#{relation}(force=false)
29
+ #{relation}_retrieved = nil
23
30
 
24
- fields = [#{Base.array_code_for(fields)}]
25
- if #{meta.key}.nil?
26
- fields.each do |field|
27
- self.send(:"#{relation}_\#{field}=", nil)
28
- end
29
- else
30
- fields.each do |field|
31
- self.send(:"#{relation}_\#{field}=", #{relation}.send(field))
31
+ fields = [#{Base.array_code_for(fields_only)}]
32
+ unless fields.empty?
33
+ #{relation}_retrieved = #{relation}.unscoped.to_a
34
+ if #{relation}_retrieved.count > 0
35
+ fields.each do |field|
36
+ self.send(:"#{relation}_\#{field}=", #{relation}_retrieved.map(&field).compact)
37
+ end
32
38
  end
33
39
  end
34
40
 
41
+ if #{has_count?}
42
+ self.#{relation}_count = #{relation}_retrieved.nil? ? #{relation}.unscoped.count : #{relation}_retrieved.count
43
+ end
44
+
35
45
  true
36
46
  end
47
+
48
+ def self.denormalize_from_#{relation}!
49
+ each do |obj|
50
+ obj.denormalize_from_#{relation}(true)
51
+ obj.save!
52
+ end
53
+ end
37
54
  EOM
38
- #klass.class_eval callback_code
55
+ klass.class_eval callback_code
39
56
 
40
57
  callback_code = <<EOM
41
58
  around_save :denormalize_to_#{inverse_relation}
@@ -74,6 +91,8 @@ EOM
74
91
  end
75
92
  end
76
93
  end
94
+ elsif #{inverse_meta.key}.nil?
95
+ changed_fields = []
77
96
  else
78
97
  changed_fields = fields & changed.map(&:to_sym)
79
98
  changed_fields.each do |field|
@@ -82,11 +101,10 @@ EOM
82
101
  end
83
102
  end
84
103
 
85
- yield
104
+ yield if block_given?
86
105
  return if changed_fields.empty?
87
106
 
88
- to_update = { "$set" => {}, "$inc" => {} }
89
- to_push = {}
107
+ to_update = { "$set" => {}, "$inc" => {}, "$push" => {} }
90
108
  to_get = {}
91
109
 
92
110
  to_rem_fields = to_rem.reject {|k,v| v.nil?}.keys
@@ -94,7 +112,7 @@ EOM
94
112
 
95
113
  # Those to add only
96
114
  (to_add_only_fields = to_add_fields - to_rem_fields).each do |field|
97
- to_push[field] = to_add[field]
115
+ to_update["$push"][field] = to_add[field]
98
116
  end
99
117
 
100
118
  to_set_fields = (to_add_fields + to_rem_fields - to_add_only_fields).uniq
@@ -116,14 +134,11 @@ EOM
116
134
  to_update["$set"][field] = array
117
135
  end
118
136
 
119
-
120
137
  to_update["$inc"][:#{relation}_count] = 1 if #{has_count?} && (was_new || was_added)
121
138
  to_update["$inc"][:#{relation}_count] = -1 if #{has_count?} && (was_removed)
122
139
 
140
+ to_update.reject! {|k,v| v.empty?}
123
141
  #{klass}.collection.find(:_id => remote_id).update_all(to_update) unless to_update.empty?
124
-
125
-
126
- #{klass}.collection.find(:_id => remote_id).update_all({"$push" => to_push}) unless to_push.empty?
127
142
  end
128
143
 
129
144
  def denormalize_to_#{inverse_relation}_old
@@ -136,7 +151,7 @@ EOM
136
151
  to_rem[:"#{relation}_\#{field}"] = send(:"\#{field}_was")
137
152
  end
138
153
 
139
- to_update = { "$set" => {}, "$inc" => {} }
154
+ to_update = { "$set" => {}, "$inc" => {}, "$push" => {} }
140
155
  to_get = {}
141
156
 
142
157
  to_rem_fields = to_rem.reject {|k,v| v.nil?}.keys
@@ -159,6 +174,7 @@ EOM
159
174
 
160
175
  to_update["$inc"][:#{relation}_count] = -1 if #{has_count?}
161
176
 
177
+ to_update.reject! {|k,v| v.empty?}
162
178
  #{klass}.collection.find(:_id => remote_id).update_all(to_update) unless to_update.empty?
163
179
  end
164
180
 
@@ -176,10 +192,9 @@ EOM
176
192
  to_rem[:"#{relation}_\#{field}"] = send(field)
177
193
  end
178
194
 
179
- yield
195
+ yield if block_given?
180
196
 
181
- to_update = { "$set" => {}, "$inc" => {} }
182
- to_push = {}
197
+ to_update = { "$set" => {}, "$inc" => {}, "$push" => {} }
183
198
  to_get = {}
184
199
 
185
200
  to_rem_fields = to_rem.reject {|k,v| v.nil?}.keys
@@ -199,12 +214,17 @@ EOM
199
214
 
200
215
  to_update["$inc"][:#{relation}_count] = -1 if #{has_count?}
201
216
 
217
+ to_update.reject! {|k,v| v.empty?}
202
218
  #{klass}.collection.find(:_id => remote_id).update_all(to_update) unless to_update.empty?
203
219
  end
204
220
  EOM
205
221
  meta.klass.class_eval callback_code
206
222
  end
207
223
 
224
+ def allowed_options
225
+ super + [:count]
226
+ end
227
+
208
228
  def has_count?
209
229
  !options[:count].nil?
210
230
  end
@@ -6,7 +6,6 @@ module Mongoid
6
6
  class OneToMany < Base
7
7
 
8
8
  def attach
9
-
10
9
  fields.each do |field|
11
10
  field_meta = meta.klass.fields[field.to_s]
12
11
  klass.field "#{relation}_#{field}", type: field_meta.try(:type)
@@ -37,15 +36,15 @@ EOM
37
36
  callback_code = <<EOM
38
37
  around_save :denormalize_to_#{inverse_relation}
39
38
 
40
- def denormalize_to_#{inverse_relation}
41
- return unless changed?
39
+ def denormalize_to_#{inverse_relation}(force = false)
40
+ return unless changed? || force
42
41
 
43
42
  fields = [#{Base.array_code_for(fields)}]
44
43
  fields_only = [#{Base.array_code_for(fields_only)}]
45
44
  methods = [#{Base.array_code_for(fields_methods)}]
46
- changed_fields = fields_only & changed.map(&:to_sym)
45
+ changed_fields = force ? fields_only.dup : (fields_only & changed.map(&:to_sym))
47
46
 
48
- yield
47
+ yield if block_given?
49
48
 
50
49
  return if changed_fields.count == 0
51
50
 
@@ -57,12 +56,18 @@ EOM
57
56
  #{inverse_relation}.update to_set
58
57
  end
59
58
 
59
+ def self.denormalize_to_#{inverse_relation}!
60
+ each do |obj|
61
+ obj.denormalize_to_#{inverse_relation}(true)
62
+ end
63
+ end
64
+
60
65
  around_destroy :denormalize_to_#{inverse_relation}_destroy
61
66
 
62
67
  def denormalize_to_#{inverse_relation}_destroy
63
68
  fields = [#{Base.array_code_for(fields)}]
64
69
 
65
- yield
70
+ yield if block_given?
66
71
 
67
72
  to_set = {}
68
73
  fields.each do |field|
@@ -73,9 +78,6 @@ EOM
73
78
  end
74
79
  EOM
75
80
  meta.klass.class_eval callback_code
76
-
77
-
78
-
79
81
  end
80
82
 
81
83
  end
@@ -4,7 +4,7 @@ module Mongoid
4
4
  module Denormalize
5
5
  module Version
6
6
 
7
- STRING = '0.0.2'
7
+ STRING = '0.0.3'
8
8
 
9
9
  end
10
10
  end
@@ -20,26 +20,20 @@ module Mongoid
20
20
  # puts "#options : #{options.inspect}"
21
21
 
22
22
  meta = self.relations[relation.to_s]
23
- raise ConfigError.new("Unknown relation :#{relation}", self) if meta.nil?
23
+ raise ConfigError.new("Unknown relation", self, relation) if meta.nil?
24
24
  # puts "# meta : #{meta.inspect}"
25
25
 
26
26
  inverse_meta = meta.klass.relations[meta.inverse.to_s]
27
- raise ConfigError.new("Unknown inverse relation for :#{relation}", self) if inverse_meta.nil?
27
+ raise ConfigError.new("Unknown inverse relation", self, relation) if inverse_meta.nil?
28
28
  # puts "#inverse_meta : #{inverse_meta.inspect}"
29
29
 
30
- methods = []
31
- fields.each do |field|
32
- unless meta.klass.instance_methods.include? field
33
- raise ConfigError.new("Unknown field or method :#{field} in :#{relation}", self)
34
- end
35
- end
36
30
 
37
31
  if meta.relation == Mongoid::Relations::Referenced::In && inverse_meta.relation == Mongoid::Relations::Referenced::Many
38
32
  OneToMany.new(self, meta, inverse_meta, fields, options).attach
39
33
  elsif meta.relation == Mongoid::Relations::Referenced::Many && inverse_meta.relation == Mongoid::Relations::Referenced::In
40
34
  ManyToOne.new(self, meta, inverse_meta, fields, options).attach
41
35
  else
42
- raise ConfigError.new("Relation not supported :#{relation}", self)
36
+ raise ConfigError.new("Relation not supported", self, relation)
43
37
  end
44
38
 
45
39
  end
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ #
5
+ # The case
6
+ #
7
+ class Author
8
+ include Mongoid::Document
9
+
10
+ field :name, type: String
11
+
12
+ has_many :books
13
+ end
14
+
15
+ class Book
16
+ include Mongoid::Document
17
+ include Mongoid::Max::Denormalize
18
+
19
+ field :name, type: String
20
+
21
+ def slug
22
+ name.try(:parameterize)
23
+ end
24
+
25
+ belongs_to :author
26
+
27
+ has_many :reviews
28
+ end
29
+
30
+ class Review
31
+ include Mongoid::Document
32
+ include Mongoid::Max::Denormalize
33
+
34
+ belongs_to :book
35
+
36
+ field :rating, type: Integer
37
+ end
38
+
39
+ def reload!
40
+ [@author, @book, @review_1, @review_2].each(&:reload)
41
+ end
42
+
43
+ #
44
+ # The specs
45
+ #
46
+ describe "Case: Existing models" do
47
+ before(:each) do
48
+ @author = Author.create!(name: "Michael Crichton")
49
+ @book = Book.create!(name: "Jurassic Park", author: @author)
50
+ @review_1 = Review.create!(book: @book, rating: 4)
51
+ @review_2 = Review.create!(book: @book, rating: 5)
52
+
53
+ reload!
54
+ end
55
+
56
+ context "when defining the denormalization for Authors in Books" do
57
+ before { Book.denormalize :author, :name }
58
+
59
+ it "Authors denormalized fields should fill" do
60
+ @book.author_name.should be_nil
61
+
62
+ Author.denormalize_to_books!
63
+ reload!
64
+
65
+ @book.author_name.should eq("Michael Crichton")
66
+ end
67
+ end
68
+
69
+ context "when defining the denormalization for Books in Reviews" do
70
+ before { Review.denormalize :book, :name, :slug }
71
+
72
+ it "Books denormalized fields should fill" do
73
+ [@review_1, @review_2].each do |review|
74
+ review.book_name.should be_nil
75
+ review.book_slug.should be_nil
76
+ end
77
+
78
+ Book.denormalize_to_reviews!
79
+ reload!
80
+
81
+ [@review_1, @review_2].each do |review|
82
+ review.book_name.should eq("Jurassic Park")
83
+ review.book_slug.should eq("jurassic-park")
84
+ end
85
+ end
86
+ end
87
+
88
+ context "when defining the denormalization for Reviews in Books" do
89
+ before { Book.denormalize :reviews, :rating, :count => true }
90
+
91
+ it "Reviews denormalized fields should fill" do
92
+ @book.reviews_count.should be_nil
93
+ @book.reviews_rating.should be_nil
94
+
95
+ Book.denormalize_from_reviews!
96
+ reload!
97
+
98
+ @book.reviews_count.should eq 2
99
+ @book.reviews_rating.should eq [4, 5]
100
+ end
101
+ end
102
+
103
+ end
104
+
@@ -9,6 +9,7 @@ class Rating
9
9
 
10
10
  field :note, type: Integer
11
11
  field :comment, type: String
12
+ field :upset_level, type: Integer
12
13
 
13
14
  belongs_to :song
14
15
  end
@@ -19,7 +20,8 @@ class Song
19
20
 
20
21
  has_many :ratings
21
22
 
22
- denormalize :ratings, :note, :comment, count: true, mean: [:note]
23
+ #denormalize :ratings, :note, :comment, count: true, mean: [:note]
24
+ denormalize :ratings, :note, :comment, :upset_level, count: true
23
25
  end
24
26
 
25
27
 
@@ -55,12 +57,12 @@ describe "Case: a song and his ratings" do
55
57
  its(:ratings_count) { should eq 1 }
56
58
  its(:ratings_note) { should eq [5] }
57
59
  its(:ratings_comment) { should eq ["Good!"] }
58
-
59
- # its(:ratings_mean) { should eq 5 }
60
+ its(:ratings_upset_level) { should eq [] }
60
61
 
61
62
  context "when modifing the first rating (=4)" do
62
63
  before do
63
64
  @rating.note = 4
65
+ @rating.upset_level = 0
64
66
  @rating.save!
65
67
  @song.reload
66
68
  end
@@ -69,6 +71,7 @@ describe "Case: a song and his ratings" do
69
71
  its(:ratings_count) { should eq 1 }
70
72
  its(:ratings_note) { should eq [4] }
71
73
  its(:ratings_comment) { should eq ["Good!"] }
74
+ its(:ratings_upset_level) { should eq [0] }
72
75
  end
73
76
 
74
77
  context "when adding an other rating (=5)" do
data/spec/spec_helper.rb CHANGED
@@ -18,6 +18,8 @@ RSpec.configure do |config|
18
18
 
19
19
  # Drop all collections and clear the identity map before each spec.
20
20
  config.before(:each) do
21
+ Moped.logger.info("\n ### " << example.full_description)
22
+
21
23
  Mongoid.purge!
22
24
  Mongoid::IdentityMap.clear
23
25
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid_max_denormalize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -94,10 +94,10 @@ files:
94
94
  - lib/mongoid/max/denormalize/base.rb
95
95
  - lib/mongoid/max/denormalize.rb
96
96
  - spec/lib/mongoid_max_denormalize_spec.rb
97
- - spec/cases/existing_spec.rb
97
+ - spec/cases/song_and_ratings_spec.rb
98
+ - spec/cases/existing_models_spec.rb
98
99
  - spec/cases/contact_and_addresses_spec.rb
99
100
  - spec/cases/post_and_comments_spec.rb
100
- - spec/cases/song_and_notes_spec.rb
101
101
  - spec/spec_helper.rb
102
102
  homepage: http://github.com/maximeg/mongoid_max_denormalize
103
103
  licenses: []
@@ -125,8 +125,8 @@ specification_version: 3
125
125
  summary: MaxMapper, polyvalent ORM for Rails
126
126
  test_files:
127
127
  - spec/lib/mongoid_max_denormalize_spec.rb
128
- - spec/cases/existing_spec.rb
128
+ - spec/cases/song_and_ratings_spec.rb
129
+ - spec/cases/existing_models_spec.rb
129
130
  - spec/cases/contact_and_addresses_spec.rb
130
131
  - spec/cases/post_and_comments_spec.rb
131
- - spec/cases/song_and_notes_spec.rb
132
132
  - spec/spec_helper.rb
@@ -1,61 +0,0 @@
1
- # encoding: utf-8
2
- require 'spec_helper'
3
-
4
- #
5
- # The case
6
- #
7
- class Author
8
- include Mongoid::Document
9
-
10
- field :name, type: String
11
-
12
- has_many :books
13
- end
14
-
15
- class Book
16
- include Mongoid::Document
17
- include Mongoid::Max::Denormalize
18
-
19
- field :name, type: String
20
-
21
- belongs_to :author
22
-
23
- has_many :reviews
24
- end
25
-
26
- class Review
27
- include Mongoid::Document
28
- include Mongoid::Max::Denormalize
29
-
30
- belongs_to :book
31
-
32
- field :rating, type: Integer
33
- end
34
-
35
- #
36
- # The specs
37
- #
38
- describe "Case: Existing" do
39
-
40
- before do
41
- @author = Author.create!(name: "Michael Crichton")
42
- @book = Book.create!(name: "Jurassic Park", author: @author)
43
- @review_1 = Review.create!(book: @book, rating: 4)
44
- @review_2 = Review.create!(book: @book, rating: 5)
45
- end
46
-
47
- context "when defining the denormalization" do
48
- before do
49
- Book.denormalize :author, :name
50
- Book.denormalize :reviews, :rating, :count => true
51
- Review.denormalize :book, :name
52
- end
53
-
54
- it "denormalization should be empty" do
55
-
56
- end
57
-
58
- end
59
-
60
- end
61
-