Skip to content

Commit fa9cf26

Browse files
authored
Merge pull request rails#54186 from tenderlove/respond-to-alloc
Eliminate allocations on Model.respond_to? calls
2 parents 25f1357 + 9edf922 commit fa9cf26

File tree

1 file changed

+54
-69
lines changed

1 file changed

+54
-69
lines changed

activerecord/lib/active_record/dynamic_matchers.rb

Lines changed: 54 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -7,114 +7,99 @@ def respond_to_missing?(name, _)
77
if self == Base
88
super
99
else
10-
match = Method.match(self, name)
11-
match && match.valid? || super
10+
super || begin
11+
match = Method.match(name)
12+
match && match.valid?(self, name)
13+
end
1214
end
1315
end
1416

1517
def method_missing(name, ...)
16-
match = Method.match(self, name)
18+
match = Method.match(name)
1719

18-
if match && match.valid?
19-
match.define
20+
if match && match.valid?(self, name)
21+
match.define(self, name)
2022
send(name, ...)
2123
else
2224
super
2325
end
2426
end
2527

2628
class Method
27-
@matchers = []
28-
2929
class << self
30-
attr_reader :matchers
31-
32-
def match(model, name)
33-
klass = matchers.find { |k| k.pattern.match?(name) }
34-
klass.new(model, name) if klass
30+
def match(name)
31+
FindBy.match?(name) || FindByBang.match?(name)
3532
end
3633

37-
def pattern
38-
@pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
34+
def valid?(model, name)
35+
attribute_names(model, name.to_s).all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
3936
end
4037

41-
def prefix
42-
raise NotImplementedError
38+
def define(model, name)
39+
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
40+
def self.#{name}(#{signature(model, name)})
41+
#{body(model, name)}
42+
end
43+
CODE
4344
end
4445

45-
def suffix
46-
""
47-
end
48-
end
46+
private
47+
def make_pattern(prefix, suffix)
48+
/\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
49+
end
4950

50-
attr_reader :model, :name, :attribute_names
51+
def attribute_names(model, name)
52+
attribute_names = name.match(pattern)[1].split("_and_")
53+
attribute_names.map! { |name| model.attribute_aliases[name] || name }
54+
end
5155

52-
def initialize(model, method_name)
53-
@model = model
54-
@name = method_name.to_s
55-
@attribute_names = @name.match(self.class.pattern)[1].split("_and_")
56-
@attribute_names.map! { |name| @model.attribute_aliases[name] || name }
57-
end
56+
def body(model, method_name)
57+
"#{finder}(#{attributes_hash(model, method_name)})"
58+
end
5859

59-
def valid?
60-
attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
61-
end
60+
# The parameters in the signature may have reserved Ruby words, in order
61+
# to prevent errors, we start each param name with `_`.
62+
def signature(model, method_name)
63+
attribute_names(model, method_name.to_s).map { |name| "_#{name}" }.join(", ")
64+
end
6265

63-
def define
64-
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
65-
def self.#{name}(#{signature})
66-
#{body}
66+
# Given that the parameters starts with `_`, the finder needs to use the
67+
# same parameter name.
68+
def attributes_hash(model, method_name)
69+
"{" + attribute_names(model, method_name).map { |name| ":#{name} => _#{name}" }.join(",") + "}"
6770
end
68-
CODE
6971
end
72+
end
7073

71-
private
72-
def body
73-
"#{finder}(#{attributes_hash})"
74-
end
74+
class FindBy < Method
75+
@pattern = make_pattern("find_by", "")
7576

76-
# The parameters in the signature may have reserved Ruby words, in order
77-
# to prevent errors, we start each param name with `_`.
78-
def signature
79-
attribute_names.map { |name| "_#{name}" }.join(", ")
80-
end
77+
class << self
78+
attr_reader :pattern
8179

82-
# Given that the parameters starts with `_`, the finder needs to use the
83-
# same parameter name.
84-
def attributes_hash
85-
"{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}"
80+
def match?(name)
81+
pattern.match?(name) && self
8682
end
8783

8884
def finder
89-
raise NotImplementedError
85+
"find_by"
9086
end
91-
end
92-
93-
class FindBy < Method
94-
Method.matchers << self
95-
96-
def self.prefix
97-
"find_by"
98-
end
99-
100-
def finder
101-
"find_by"
10287
end
10388
end
10489

10590
class FindByBang < Method
106-
Method.matchers << self
91+
@pattern = make_pattern("find_by", "!")
10792

108-
def self.prefix
109-
"find_by"
110-
end
93+
class << self
94+
attr_reader :pattern
11195

112-
def self.suffix
113-
"!"
114-
end
96+
def match?(name)
97+
pattern.match?(name) && self
98+
end
11599

116-
def finder
117-
"find_by!"
100+
def finder
101+
"find_by!"
102+
end
118103
end
119104
end
120105
end

0 commit comments

Comments
 (0)