aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/scoping.rb
blob: a8f5e961906bd5a81fccfb56fdd194049e2006e3 (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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
require 'active_support/concern'

module ActiveRecord
  module Scoping
    extend ActiveSupport::Concern

    included do
      include Default
      include Named
    end

    module ClassMethods
      # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
      # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
      # <tt>:create</tt> parameters are an attributes hash.
      #
      #   class Article < ActiveRecord::Base
      #     def self.create_with_scope
      #       with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
      #         find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
      #         a = create(1)
      #         a.blog_id # => 1
      #       end
      #     end
      #   end
      #
      # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
      # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
      #
      # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
      # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
      # array of strings format for your joins.
      #
      #   class Article < ActiveRecord::Base
      #     def self.find_with_scope
      #       with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
      #         with_scope(:find => limit(10)) do
      #           all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
      #         end
      #         with_scope(:find => where(:author_id => 3)) do
      #           all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
      #         end
      #       end
      #     end
      #   end
      #
      # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
      #
      #   class Article < ActiveRecord::Base
      #     def self.find_with_exclusive_scope
      #       with_scope(:find => where(:blog_id => 1).limit(1)) do
      #         with_exclusive_scope(:find => limit(10)) do
      #           all # => SELECT * from articles LIMIT 10
      #         end
      #       end
      #     end
      #   end
      #
      # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
      def with_scope(scope = {}, action = :merge, &block)
        # If another Active Record class has been passed in, get its current scope
        scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)

        previous_scope = self.current_scope

        if scope.is_a?(Hash)
          # Dup first and second level of hash (method and params).
          scope = scope.dup
          scope.each do |method, params|
            scope[method] = params.dup unless params == true
          end

          scope.assert_valid_keys([ :find, :create ])
          relation = construct_finder_arel(scope[:find] || {})
          relation.default_scoped = true unless action == :overwrite

          if previous_scope && previous_scope.create_with_value && scope[:create]
            scope_for_create = if action == :merge
              previous_scope.create_with_value.merge(scope[:create])
            else
              scope[:create]
            end

            relation = relation.create_with(scope_for_create)
          else
            scope_for_create = scope[:create]
            scope_for_create ||= previous_scope.create_with_value if previous_scope
            relation = relation.create_with(scope_for_create) if scope_for_create
          end

          scope = relation
        end

        scope = previous_scope.merge(scope) if previous_scope && action == :merge

        self.current_scope = scope
        begin
          yield
        ensure
          self.current_scope = previous_scope
        end
      end

      protected

      # Works like with_scope, but discards any nested properties.
      def with_exclusive_scope(method_scoping = {}, &block)
        if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
          raise ArgumentError, <<-MSG
  New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:

  User.unscoped.where(:active => true)

  Or call unscoped with a block:

  User.unscoped do
  User.where(:active => true).all
  end

  MSG
        end
        with_scope(method_scoping, :overwrite, &block)
      end

      def current_scope #:nodoc:
        Thread.current["#{self}_current_scope"]
      end

      def current_scope=(scope) #:nodoc:
        Thread.current["#{self}_current_scope"] = scope
      end

      private

      def construct_finder_arel(options = {}, scope = nil)
        relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
        relation = scope.merge(relation) if scope
        relation
      end

    end

    def populate_with_current_scope_attributes
      return unless self.class.scope_attributes?

      self.class.scope_attributes.each do |att,value|
        send("#{att}=", value) if respond_to?("#{att}=")
      end
    end

  end
end