aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/relation/delegation.rb
blob: 257ae04ff480a639b3d423ee1be09871998c9aa7 (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
module ActiveRecord
  module Delegation # :nodoc:
    module DelegateCache # :nodoc:
      def relation_delegate_class(klass)
        @relation_delegate_cache[klass]
      end

      def initialize_relation_delegate_cache
        @relation_delegate_cache = cache = {}
        [
          ActiveRecord::Relation,
          ActiveRecord::Associations::CollectionProxy,
          ActiveRecord::AssociationRelation
        ].each do |klass|
          delegate = Class.new(klass) {
            include ClassSpecificRelation
          }
          mangled_name = klass.name.gsub("::".freeze, "_".freeze)
          const_set mangled_name, delegate
          private_constant mangled_name

          cache[klass] = delegate
        end
      end

      def inherited(child_class)
        child_class.initialize_relation_delegate_cache
        delegate = child_class.relation_delegate_class(ActiveRecord::Associations::CollectionProxy)
        delegate.include ActiveRecord::Associations::CollectionProxy::DelegateExtending
        super
      end
    end

    extend ActiveSupport::Concern

    # This module creates compiled delegation methods dynamically at runtime, which makes
    # subsequent calls to that method faster by avoiding method_missing. The delegations
    # may vary depending on the klass of a relation, so we create a subclass of Relation
    # for each different klass, and the delegations are compiled into that subclass only.

    delegate :to_xml, :encode_with, :length, :each, :uniq, :to_ary, :join,
             :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of,
             :to_sentence, :to_formatted_s, :as_json,
             :shuffle, :split, :index, to: :records

    delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
             :connection, :columns_hash, to: :klass

    module ClassSpecificRelation # :nodoc:
      extend ActiveSupport::Concern

      included do
        @delegation_mutex = Mutex.new
      end

      module ClassMethods # :nodoc:
        def name
          superclass.name
        end

        def delegate_to_scoped_klass(method)
          @delegation_mutex.synchronize do
            return if method_defined?(method)

            if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method)
              module_eval <<-RUBY, __FILE__, __LINE__ + 1
                def #{method}(*args, &block)
                  scoping { @klass.#{method}(*args, &block) }
                end
              RUBY
            else
              define_method method do |*args, &block|
                scoping { @klass.public_send(method, *args, &block) }
              end
            end
          end
        end

        def delegate(method, opts = {})
          @delegation_mutex.synchronize do
            return if method_defined?(method)
            super
          end
        end
      end

      private

        def method_missing(method, *args, &block)
          if @klass.respond_to?(method)
            self.class.delegate_to_scoped_klass(method)
            scoping { @klass.public_send(method, *args, &block) }
          elsif arel.respond_to?(method)
            self.class.delegate method, to: :arel
            arel.public_send(method, *args, &block)
          else
            super
          end
        end
    end

    module ClassMethods # :nodoc:
      def create(klass, *args)
        relation_class_for(klass).new(klass, *args)
      end

      private

        def relation_class_for(klass)
          klass.relation_delegate_class(self)
        end
    end

    private
      def respond_to_missing?(method, _)
        super || @klass.respond_to?(method) || arel.respond_to?(method)
      end

      def method_missing(method, *args, &block)
        if @klass.respond_to?(method)
          scoping { @klass.public_send(method, *args, &block) }
        elsif arel.respond_to?(method)
          arel.public_send(method, *args, &block)
        else
          super
        end
      end
  end
end