Skip to content

Commit

Permalink
Merge pull request #16 from rehevkor5/feature/obey_inverse_of
Browse files Browse the repository at this point in the history
Obey inverse_of
  • Loading branch information
rehevkor5 committed Jun 18, 2013
2 parents d3a3838 + 0df5895 commit ed8d1a3
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 67 deletions.
15 changes: 15 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
source "http://rubygems.org"
gemspec

gem "rake"
gem "bson_ext"

group :development do
gem "jeweler"
gem "guard-rspec"
end

group :test do
gem "pry"
gem "rspec"
end
73 changes: 73 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
PATH
remote: .
specs:
mongoid_denormalize (0.3.0)
mongoid (>= 2.1.9)

GEM
remote: http://rubygems.org/
specs:
activemodel (3.2.3)
activesupport (= 3.2.3)
builder (~> 3.0.0)
activesupport (3.2.3)
i18n (~> 0.6)
multi_json (~> 1.0)
bson (1.6.2)
bson_ext (1.6.2)
bson (~> 1.6.2)
builder (3.0.0)
coderay (1.0.6)
diff-lcs (1.1.3)
ffi (1.0.11)
git (1.2.5)
guard (1.0.1)
ffi (>= 0.5.0)
thor (~> 0.14.6)
guard-rspec (0.7.0)
guard (>= 0.10.0)
i18n (0.6.0)
jeweler (1.8.3)
bundler (~> 1.0)
git (>= 1.2.5)
rake
rdoc
json (1.6.6)
method_source (0.7.1)
mongo (1.6.2)
bson (~> 1.6.2)
mongoid (2.4.8)
activemodel (~> 3.1)
mongo (~> 1.3)
tzinfo (~> 0.3.22)
multi_json (1.3.2)
pry (0.9.9.3)
coderay (~> 1.0.5)
method_source (~> 0.7.1)
slop (>= 2.4.4, < 3)
rake (0.9.2.2)
rdoc (3.12)
json (~> 1.4)
rspec (2.9.0)
rspec-core (~> 2.9.0)
rspec-expectations (~> 2.9.0)
rspec-mocks (~> 2.9.0)
rspec-core (2.9.0)
rspec-expectations (2.9.1)
diff-lcs (~> 1.1.3)
rspec-mocks (2.9.0)
slop (2.4.4)
thor (0.14.6)
tzinfo (0.3.33)

PLATFORMS
ruby

DEPENDENCIES
bson_ext
guard-rspec
jeweler
mongoid_denormalize!
pry
rake
rspec
8 changes: 8 additions & 0 deletions Guardfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# vim:set filetype=ruby:
# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard :rspec, all_after_pass: false, cli: "--fail-fast --tty --format documentation --colour" do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |match| "spec/#{match[1]}_spec.rb" }
end
42 changes: 30 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ In your model:
denormalize :name, :email, :from => :user



Example
-------

Expand All @@ -42,6 +41,7 @@ Example
include Mongoid::Denormalize

has_many :comments
has_many :moderated_comments, :class_name => "Comment", :inverse_of => :moderator

field :name
field :email
Expand All @@ -54,6 +54,7 @@ Example
include Mongoid::Denormalize

belongs_to :user
belongs_to :moderator, :class_name => "User", :inverse_of => :moderated_comments

field :body
Expand All @@ -78,30 +79,46 @@ Options
Denormalization can happen in either or both directions. When using the `:from` option, the associated objects will fetch the values from
the parent. When using the `:to` option, the parent will push the values to its children.

# Basic denormalization. Will set the user_name attribute with the associated user's name.
denormalize :name, :from => :user

# Basic denormalization. Will set the user_name attribute of "self.comments" to the value of "self.name".
denormalize :name, :to => :comments

# Multiple fields. Will set the user_name and user_email attributes with the associated user's name and email.
denormalize :name, :email, :from => :user


# Multiple children. Will set the user_name attribute of "self.posts" and "self.comments" with "self.name".
denormalize :name, :to => [:posts, :comments]

# With custom field name prefix. Will set the commenter_name attribute of "self.comments".
# Basic denormalization, obeying :inverse_of. Will set the moderator_name attribute of "self.moderated_comments"
denormalize :name, :email, :to => :moderated_comments

# With custom field name prefix. Will set the commenter_name attribute of "self.comments" (takes precedence over
# inverse_of).
denormalize :name, :to => :comments, :as => :commenter

You must specify the type of all denormalizations when using the `:from` option, unless the denormalized type is `String`, the default.

# Basic denormalization. Will set the user_name attribute with the associated user's name.
denormalize :name, :from => :user

# Multiple fields. Will set the user_name and user_email attributes with the associated user's name and email.
denormalize :name, :email, :from => :user

# Basic denormalization. Will set the moderator_name attribute with the associated author user's name.
denormalize :name, :from => :moderator

When using `:from`, if the type of the denormalized field is anything but `String` (the default),
you must specify the type with the `:type` option.

# in User
field :location, :type => Array
denormalize :location, :to => :posts

# in Post
denormalize :location, :type => Array, :from => :user

A few notes on behavior:

`:from` denormalizations are processed as `before_save` callbacks.

`:to` denormalizations are processed as `after_save` callbacks.

With `:to`, validations are not run on the object(s) containing the denormalized field(s) when they are saved.

Rake tasks
----------

Expand All @@ -126,7 +143,7 @@ So, if User has_many :posts and User has_many :comments, but Comments are embedd
Contributing
-------

Clone the repository and install jeweler with `gem install jeweler` so that you can run the rake tasks.
Clone the repository and run Bundler with `bundle install` so that you can run the rake tasks.

Contributors
-------
Expand All @@ -135,6 +152,7 @@ Contributors
* Austin Bales (https://github.com/arbales)
* Isaac Cambron (https://github.com/icambron)
* Shannon Carey (https://github.com/rehevkor5)
* Sebastien Azimi (https://github.com/suruja)


Credits
Expand Down
104 changes: 54 additions & 50 deletions lib/mongoid_denormalize.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
# Helper module for denormalizing association attributes in Mongoid models.
module Mongoid::Denormalize
extend ActiveSupport::Concern

included do
cattr_accessor :denormalize_definitions

before_save :denormalize_from
after_save :denormalize_to
end
Expand Down Expand Up @@ -36,7 +36,7 @@ def denormalize(*args)
fields.each { |name| field "#{options[:from]}_#{name}", :type => options[:type] || String }
end
end

def is_denormalized?
true
end
Expand All @@ -50,65 +50,69 @@ def denormalized_valid?
def repair_denormalized!
self.save! unless denormalized_valid?
end

private
def denormalize_from
self.denormalize_definitions.reject do |definition|
definition[:options][:to]
end.each do |definition|
definition[:fields].each do |name|
field = definition[:options][:from]
# force reload if :from method is an association ; call it normally otherwise
associated = self.class.reflect_on_association(field) ? self.send(field, true) : self.send(field)
self.send("#{field}_#{name}=", associated.try(name))
end
def denormalize_from
Array(self.denormalize_definitions).reject do |definition|
definition[:options][:to]
end.each do |definition|
definition[:fields].each do |name|
field = definition[:options][:from]
# force reload if :from method is an association ; call it normally otherwise
associated = self.class.reflect_on_association(field) ? self.send(field, true) : self.send(field)
self.send("#{field}_#{name}=", associated.try(name))
end
end

def denormalize_to
self.denormalize_definitions.find_all do |definition|
definition[:options][:to]
end.each do |definition|
as = definition[:options][:as]
prefix = as ? as : self.class.name.underscore
end

assignments = definition[:fields].collect do |source_field|
{
:source_field => source_field.to_s,
:denormalized_field => "#{prefix}_#{source_field}",
:value => self.send(source_field)
}
end
def denormalize_to
Array(self.denormalize_definitions).find_all do |definition|
definition[:options][:to]
end.each do |definition|
as = definition[:options][:as]

assignments = definition[:fields].collect do |source_field|
{
:source_field => source_field.to_s,
:value => self.send(source_field)
}
end

Array(definition[:options][:to]).each do |association|
relation = []
reflect = self.class.reflect_on_association(association)
relation = reflect.relation.macro unless reflect.nil? || reflect.relation.nil?
Array(definition[:options][:to]).each do |association|
relation = []
reflect = self.class.reflect_on_association(association)
relation = reflect.relation.macro unless reflect.nil? || reflect.relation.nil?

reflect.klass.skip_callback(:save, :before, :denormalize_from) if reflect.klass.try(:is_denormalized?)
reflect.klass.skip_callback(:save, :before, :denormalize_from) if reflect.klass.try(:is_denormalized?)

c = self.send(association)
if [:embedded_in, :embeds_one, :referenced_in, :references_one, :has_one, :belongs_to].include? relation
unless c.blank?
assign_and_save(c, assignments)
end
else
c.to_a.each{|c| assign_and_save(c, assignments)}
associated = self.send(association)
prefix = (as || reflect.inverse_of || reflect.inverse_class_name).to_s.underscore

if [:embedded_in, :embeds_one, :referenced_in, :references_one, :has_one, :belongs_to].include? relation
unless associated.blank?
assign_and_save(associated, assignments, prefix)
end
reflect.klass.set_callback(:save, :before, :denormalize_from) if reflect.klass.try(:is_denormalized?)
else
associated.to_a.each { |c| assign_and_save(c, assignments, prefix) }
end

reflect.klass.set_callback(:save, :before, :denormalize_from) if reflect.klass.try(:is_denormalized?)
end
end
end

def assign_and_save(obj, assignments)
assigned_any = false
assignments.each do |assignment|
if self.changed_attributes.has_key?(assignment[:source_field])
assigned_any = true
obj.send("#{assignment[:denormalized_field]}=", assignment[:value])
end
def assign_and_save(obj, assignments, prefix)
attributes_hash = Hash[assignments.collect do |assignment|
if self.changed_attributes.has_key?(assignment[:source_field])
["#{prefix}_#{assignment[:source_field]}", assignment[:value]]
end
obj.save if assigned_any
end]

unless attributes_hash.empty?
# The more succinct update_attributes(changes, :without_protection => true) requires Mongoid 3.0.0,
# but we only require 2.1.9
obj.assign_attributes(attributes_hash, :without_protection => true)
obj.save(:validate => false)
end
end
end
36 changes: 36 additions & 0 deletions perf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require "benchmark"
require 'rubygems'
require 'mongoid'

Mongoid.configure do |config|
config.master = Mongo::Connection.new.db("mongoid_denormalize_development")
end

def bm(context = "")
print("#{context}\t")
Benchmark.bm do |bm|
[10, 100, 1000, 10000].each do |i|
@user = User.create
i.times do
@user.articles.create
end

bm.report("#{i}\t") do
@user.update_attributes :name => 'John Doe', :email => 'john@doe.com'
end

@user.articles.destroy_all
@user.delete
end
end
end

lib_path = 'lib/mongoid_denormalize.rb'
lib_full_path = File.expand_path("../#{lib_path}", __FILE__)

eval `git show a83c4fc3e86f19f9c494b9f068f3bd44d876db1a:#{lib_path}`
Dir["#{File.dirname(__FILE__)}/spec/app/models/*.rb"].each { |f| require f }
bm "Before"

load lib_full_path
bm "After"
12 changes: 12 additions & 0 deletions spec/app/models/article.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Article
include Mongoid::Document
include Mongoid::Denormalize

field :title
field :body
field :created_at, :type => Time

belongs_to :author, :class_name => 'User'

denormalize :name, :email, :from => :author
end
8 changes: 5 additions & 3 deletions spec/app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ class User
has_one :post
has_many :comments
has_many :moderated_comments, :class_name => "Comment", :inverse_of => :moderator

has_many :articles, :inverse_of => :author

denormalize :name, :email, :location, :to => [:post, :comments]
denormalize :nickname, :to => :moderated_comments, :as => :moderator
end
denormalize :nickname, :to => :moderated_comments, :as => :mod
denormalize :name, :email, :to => :articles
end
Loading

0 comments on commit ed8d1a3

Please sign in to comment.