aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/relation/delegation.rb
blob: 615309964c8b80e1b9f489600a8575610607a898 (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
require 'thread'
require 'thread_safe'

module ActiveRecord
  module Delegation # :nodoc:
    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, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
    delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
             :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass

    module ClassSpecificRelation
      extend ActiveSupport::Concern

      included do
        @delegation_mutex = Mutex.new
      end

      module ClassMethods
        def name
          superclass.name
        end

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

            if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
              module_eval <<-RUBY, __FILE__, __LINE__ + 1
                def #{method}(*args, &block)
                  scoping { @klass.#{method}(*args, &block) }
                end
              RUBY
            else
              module_eval <<-RUBY, __FILE__, __LINE__ + 1
                def #{method}(*args, &block)
                  scoping { @klass.send(#{method.inspect}, *args, &block) }
                end
              RUBY
            end
          end
        end

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

      protected

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

    module ClassMethods
      @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2)

      def new(klass, *args)
        relation = relation_class_for(klass).allocate
        relation.__send__(:initialize, klass, *args)
        relation
      end

      # This doesn't have to be thread-safe. relation_class_for guarantees that this will only be
      # called exactly once for a given const name.
      def const_missing(name)
        const_set(name, Class.new(self) { include ClassSpecificRelation })
      end

      private
      # Cache the constants in @@subclasses because looking them up via const_get
      # make instantiation significantly slower.
      def relation_class_for(klass)
        if klass && (klass_name = klass.name)
          my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new }
          # This hash is keyed by klass.name to avoid memory leaks in development mode
          my_cache.compute_if_absent(klass_name) do
            # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name
            const_get("#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}", false)
          end
        else
          ActiveRecord::Relation
        end
      end
    end

    def respond_to?(method, include_private = false)
      super || Array.method_defined?(method) ||
        @klass.respond_to?(method, include_private) ||
        arel.respond_to?(method, include_private)
    end

    protected

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