Skip to content

Allow configuration of embedded models to fire callbacks on parent save (#237) #1058

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions lib/mongoid/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,54 @@ module Callbacks
define_model_callbacks :initialize, :only => :after
define_model_callbacks :create, :destroy, :save, :update
end

def run_callbacks(kind, *args, &block)
_run_callbacks_with_cascade(_cascade_targets(kind), kind, *args) do
super(kind, *args, &block)
end
end

protected

def _cascade_targets(kind)
cascadable_children = []
self.relations.each_pair do |name, metadata|
next unless metadata.embedded? && metadata.cascade_callbacks

target = self.send(name)

if metadata.macro == :embeds_many
cascadable_children += target
elsif metadata.macro == :embeds_one && target.present?
cascadable_children << target
end
end
cascadable_children.select { |child| _should_cascade(kind, child) }
end

def _should_cascade(kind, child)
[:create, :destroy].include?(kind) || child.changed? || child.new_record?
end

def _normalize_callback_kind(original_kind, child)
if original_kind == :update && child.new_record?
:create
else
original_kind
end
end

def _run_callbacks_with_cascade(children, kind, *args, &block)
if child = children.pop
_run_callbacks_with_cascade(children, kind, *args) do
kind = _normalize_callback_kind(kind, child)
child.run_callbacks(kind, *args) do
block.call
end
end
else
block.call
end
end
end
end
2 changes: 1 addition & 1 deletion lib/mongoid/relations/embedded/many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ def stores_foreign_key?
#
# @since 2.1.0
def valid_options
[ :as, :cyclic, :order, :versioned ]
[ :as, :cyclic, :order, :versioned, :cascade_callbacks ]
end

# Get the default validation setting for the relation. Determines if
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/relations/embedded/one.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def stores_foreign_key?
#
# @since 2.1.0
def valid_options
[ :as, :cyclic ]
[ :as, :cyclic, :cascade_callbacks ]
end

# Get the default validation setting for the relation. Determines if
Expand Down
12 changes: 12 additions & 0 deletions lib/mongoid/relations/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,18 @@ def order?
!!order
end

# Should embedded association cascade parent callbacks?
#
# @example Perform parent callbacks cascading?
# metadata.cascade_callbacks
#
# @return [ true, false ] If the cascade_callbacks is set.
#
# @since 2.1.0
def cascade_callbacks
!!self[:cascade_callbacks]
end

private

# Returns the class name for the relation.
Expand Down
47 changes: 46 additions & 1 deletion spec/models/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ class Artist
include Mongoid::Document
field :name
embeds_many :songs
embeds_many :labels
embeds_many :labels, :cascade_callbacks => true
embeds_one :instrument, :cascade_callbacks => true
embeds_one :address

before_create :before_create_stub
after_create :create_songs
Expand All @@ -27,10 +29,48 @@ def create_songs
end
end

class Address
include Mongoid::Document
field :street
embedded_in :artist

after_save :after_save_stub

private
def after_save_stub
end
end

class Instrument
include Mongoid::Document
field :name
field :key
embedded_in :artist

after_save :after_save_stub
before_create :upcase_name
before_update :tune_to_g_sharp

private
def after_save_stub; end
def upcase_name
self.name = self.name.upcase
end
def tune_to_g_sharp
self.key = "G#"
end
end

class Song
include Mongoid::Document
field :title
embedded_in :artist

after_save :after_save_stub

private
def after_save_stub
end
end

class Label
Expand All @@ -39,10 +79,15 @@ class Label
embedded_in :artist
before_validation :cleanup

after_save :after_save_stub

private
def cleanup
self.name = self.name.downcase.capitalize
end

def after_save_stub
end
end

class ValidationCallback
Expand Down
119 changes: 119 additions & 0 deletions spec/unit/mongoid/callbacks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,125 @@ class TestClass
end
end

describe "cascaded callbacks" do

let(:artist) do
Artist.new(:name => "Foo Fighters")
end

context "on parent update" do

before do
artist.save!
artist.build_instrument(:name => "Piano")
end

context "child is new" do

it "should trigger create" do
artist.save!
artist.instrument.name.should == 'PIANO'
end

it "should not trigger update" do
artist.save!
artist.instrument.key.should_not == 'G#'
end
end

context "child is persisted" do

before do
artist.save!
end

context "child is dirty" do

before do
artist.instrument.name = 'Tuba'
end

it "should trigger update" do
artist.save!
artist.instrument.key.should == 'G#'
end
end

context "child is not dirty" do

it "should not trigger update" do
artist.save!
artist.instrument.key.should_not == 'G#'
end
end
end
end

context "when enabled" do

let(:label) do
Label.new(:name => "Tower Records")
end

let(:instrument) do
Instrument.new(:name => "Harpsichord")
end

before do
artist.labels << label
artist.instrument = instrument
end

context "embeds_many" do

it "should cascade callbacks" do
label.expects(:after_save_stub)
artist.save!
end
end

context "embeds_one" do

it "should cascade callbacks" do
instrument.expects(:after_save_stub)
artist.save!
end
end
end

context "when disabled" do

let(:song) do
Song.new
end

let(:address) do
Address.new
end

before do
artist.songs << song
artist.address = address
end

context "embeds_many" do

it "should not cascade callbacks" do
song.expects(:after_save_stub).never
artist.save!
end
end

context "embeds_one" do

it "should not cascade callbacks" do
address.expects(:after_save_stub).never
artist.save!
end
end
end
end

describe ".before_create" do

before do
Expand Down
2 changes: 1 addition & 1 deletion spec/unit/mongoid/relations/embedded/many_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@

it "returns the valid options" do
described_class.valid_options.should ==
[ :as, :cyclic, :order, :versioned ]
[ :as, :cyclic, :order, :versioned, :cascade_callbacks ]
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/unit/mongoid/relations/embedded/one_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@

it "returns the valid options" do
described_class.valid_options.should ==
[ :as, :cyclic ]
[ :as, :cyclic, :cascade_callbacks ]
end
end

Expand Down