aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/recursion.rb
blob: 36589238e7edbb0e2f7f257762ea4513b4862efe (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
module ActiveRecord
  # Wraps a guard around #save to make sure that recursive calls don't actually
  # invoke save multiple times. Recursive calls to save can occur quite
  # easily, and unintentionally. Consider the following case:
  #
  #   class Project < ActiveRecord::Base
  #     has_and_belongs_to_many :people
  #     after_create :grant_access_to_admins
  #
  #     def grant_access_to_admins
  #       Person.admins.each do |admin|
  #         admin.projects.push_with_attributes(self, "access_level" => 42)
  #       end
  #     end
  #   end
  #
  #   class Person < ActiveRecord::Base
  #     has_and_belongs_to_many :projects
  #     ...
  #   end
  #
  #   teddy = Person.find_by_name("teddy")
  #   project = Project.new :name => "sumo wrestling"
  #   project.people << teddy
  #   project.save!
  #
  # The #push_with_attributes causes +self+ (the project) to be saved again,
  # even though we're already in the midst of doing a save. This results in
  # "teddy" _not_ being added to the project's people list, because the
  # recursive call resets the new-record status and thus ignores any
  # non-new records in the collection.
  #
  # Thus, the need for a recursive guard on save.
  module Recursion 
    def self.append_features(base) # :nodoc:
      super

      base.class_eval do
        alias_method :save_without_recursive_guard, :save
        alias_method :save, :save_with_recursive_guard
      end
    end    

    # Wrap the save call with a sentinel that prevents saves from occuring if
    # a save is already in progress.
    def save_with_recursive_guard(*args)
      critical = Thread.critical
      Thread.critical = true
      old_save_state = @currently_saving_record
      return true if @currently_saving_record
      @currently_saving_record = true
      Thread.critical = critical

      save_without_recursive_guard(*args)
    ensure
      Thread.critical = critical
      @currently_saving_record = old_save_state
    end
  end
end