Reusable filtering concern for Rails models
I wanted to implement a generic filtering concern for Rails models, and decided to do things a bit differently from what Justin Weiss and Fabio Pitino did.
Justin Weiss laid the foundation for a Filterable
concern in 2016 in
Search and Filter Rails Models Without Bloating Your Controller.
Then I read Fabio Pitino’s post (Enhanced Filterable concern for Rails models)
in which he describes a way to improve it using a search_scope
class method.
It makes it clear in the model which scope
s are used for filtering, and simplifies listing the permitted params.
However, I found that Fabio’s code didn’t work in Rails 6 (the search_scope
wasn’t initialized on time),
and naming didn’t quite reflect what was happening.
Here is what I ended up doing, building upon Justin and Fabio’s work.
-
Adjust the name and syntax of the concern for clarity:
# app/models/concerns/filterable.rb module Filterable extend ActiveSupport::Concern class_methods do attr_reader :filter_scopes def filter_scope(name, *args) scope name, *args @filter_scopes ||= [] @filter_scopes << name end def filter_by(params) filtered = all params.permit(*filter_scopes).each do |key, value| filtered = filtered.public_send(key, value) if value.present? end filtered end end end
-
Include this concern in all models inheriting from ApplicationRecord.
# app/models/application_record.rb class ApplicationRecord include Filterable end
-
Add filtering scopes in the models that need them.
# app/models/widget.rb class Widget < ApplicationRecord filter_scope :foo, { where(foo: "bar") } end
-
Apply filters in controllers.
# app/controllers/widgets_controller.rb class WidgetsController < ApplicationRecord def index @widgets = Widget.filter_by(params) end end
I’m still not completely satisfied with this solution because it seems a little crude,
so I looked into HeartCombo’s has_scope
gem for inspiration.
Controllers need to declare which scopes can be used in that gem, which adds noticeable overhead.
I can see a few ways to improve this pattern:
- Automating filtering with a
before_action
in controllers, like CanCanCan does inload_and_authorize_resource
. - Creating a FormObject which
form_for
could use to preselect form values from the current request query. - Hardening security, which
has_scope
does more preemptively, to prevent SQL injections. - Simplify querying boolean scopes, and those which take more than one parameter (start/end date for instance).