I was trying to solve a problem where we want to pass in a handful of parameters to a method that will then scope an ActiveRecord model in Rails.
I have hit this same problem in multiple guises. Most times when trying to allow a User to create their own report based on filterable attributes, all of which may be optional.
Before
created_at_start_date = Date.current
users = [User.find(1), User.find(5)]
posts = Post.all
posts = posts.by_created_at_start_date(created_at_start_date) if created_at_start_date.present?
posts = posts.by_users(user) if users.any?
puts posts.to_sql
After
PostsQuery.report_filtering(
users: [User.find(1), User.find(5)],
created_at_start_date: Date.current,
created_at_end_date: Date.current, # ignored as there is no method
updated_at_start_date: Date.current, # ignored as there is no method
updated_at_end_date: Date.current, # ignored as there is no method
).to_sql
Magic
def self.report_filtering(filters)
filters.inject(self) do |klass, (method_name, args)|
klass.try("by_#{method_name}", args) || klass.all
end
end
This code loops over the filter attributes that are passed in and dynamically chains the corresponding methods into a single call.
The generated code may look like this under the hood:
def self.report_filtering(filters)
by_users(filters[:users])
.by_created_at_start_date(filters[:created_at_start_date])
.all
.all
.all
end
Where the loop fails to call the corresponding method - for example by_created_at_end_date
has not yet been defined - it will instead return all
which allows us to continue chaining queries.
Chaining .all
does not effect the SQL that is generated. Rails is clever enough to ignore them.