mongoid_max_denormalize 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
-