Gerade bei großen Datenmengen sind sie unverzichtbar: Die Möglichkeit zu filtern, und die Möglichkeit zu sortieren. Die API soll beispielsweise nicht alle Artikel im Shop zurückliefern, sondern nur diejenigen aus einer bestimmten Kategorie, und das aufsteigend sortiert nach dem Preis. Für Ruby on Rails-basierte Apps bietet das Gem filterameter eine bequeme Möglichkeit, Filter und Sortierungen einfach im Controller zu deklarieren. Die Bibliothek kann vielleicht nicht jeden komplexen Sonderfall abdecken, aber für die meisten Anwendungsfälle kann sie die Implementierung sehr vereinfachen. Als grobe Richtschnur: Solange man es auf Datenbank-Ebene abbilden kann, also in Rails beispielsweise in einem Scope, kann auch filterameter damit umgehen.

Der erste Schritt ist das Gem filterameter zum Bundle hinzuzufügen. Dann bindet man das Modul Filterameter::DeclarativeFilters ein. Das kann zentral im ApplicationController sein, wenn man die Funktionalität in allen Controllern haben möchte, oder separat im jeweiligen Controller:

class ApplicationController < ActionController::Base
  include Filterameter::DeclarativeFilters

  (..)
end

Außerdem muss man die gefilterte Query erzeugen. Dafür stehen zwei Wege zur Verfügung. Zum einen kann man build_filtered_query als before_action aufrufen, ebenfalls wahlweise zentral im ApplicationController oder in den Controllern, wo das Gem verwendet werden soll. In diesem Fall kann man die Zuweisung an eine Variable in der index-Methode entfernen. Die Query landet automatisch in einer Variablen mit dem pluralisierten Namen des Models. Für das Model Product und den Controller ProductsController steht den Views entsprechend @products zur Verfügung:

class ProductsController < ApplicationController
  include Filterameter::DeclarativeFilters

  before_action :build_filtered_query, only: :index

  def index
  end

  (..)
end

Alternativ kann man anstelle der before_action die Methode build_query_from_filters ausdrücklich aufrufen:

def index
  @products = build_query_from_filters
end

Bleibt noch der wichtigste Teil, nämlich die eigentlichen Filter und Sortierungen zu deklarieren. Wenn ein Product beispielsweise ein Attribut category hat, kann das so aussehen (im Folgenden seien das Modul und die before_action im ApplicationController eingebunden):

class ProductsController < ApplicationController
  filter :category

  (...)
end

Das genügt schon, damit man mit dem Query-Parameter ?filter[category]=books filtern kann. Wenn der Parameter einen anderen Namen haben soll, beispielsweise cat, sieht die Deklaration so aus:

filter :cat, name: :category

Was wenn die Kategorie kein direktes Attribut des Product ist, sondern ein assoziiertes Model? Das sieht dann so aus:

filter :category_id, association: :product_category

Und wenn man nicht anhand der ID des assoziierten Models filtern möchte, sondern beispielsweise nach product_category.name, braucht man einen passenden Scope:

# in Controller
filter :category, name: :by_category
# im Model
def self.by_category(category_name)
  joins(:product_categories).where("name = ?", category_name)
end

Der Scope muss in diesem Fall eine Klassenmethode sein, ein Inline-Scope funktioniert nicht.

Darüber hinaus gibt es unter anderem noch die Möglichkeit, partiell zu filtern. Ein Query-Parameter wie ?filter[category]=book kann dann beispielsweise auch picture book oder booklet finden. Man kann festlegen, ob das Suchfragment nur am Anfang, nur am Ende oder überall erlaubt sein soll. Man kann, zusätzlich zu Validierungen auf Model-Ebene, nur bestimmte Parameter-Werte erlauben. Auch das Filtern nach einer Range ist zulässig, etwa “Preis zwischen 50 und 100 Euro”. Dazu verrät das README des GitHub-Repos mehr.

Die meisten deklarierten Filter bringen automatisch eine Sortierfunktion mit. Wenn zum Beispiel ein Filter für das Attribut size deklariert ist, kann man mit ?filter[sort]=size sortieren. Etwas komplexer wird es bei Filtern, die auf einem Scope basieren. Auch hier verrät das README mehr. Standardmäßig sind Sortierungen aufsteigend. Man kann die Richtung festlegen, indem man vor den Parameterwert ein + oder - setzt, etwa ?filter[sort]=+size oder ?filter[sort]=-price.

Braucht man eine Sortierung, aber nicht den dazugehörigen Filter, kann man sie auch explizit deklarieren:

sort :size
sort :category_name, name: :name, association: product_category
sort :updated_at, name: :by_updated_at # bezieht sich auf Scope by_updated_at

Auf Basis einer bestehenden Sortierung kann man auch eine Standard-Sortierung festlegen:

sort :update_at, name: :by_updated_at
default_sort updated_at: :desc

filteramter bietet also eine ganze Reihe von Features, um seine Ruby on Rails-App mit Filtern und Sortierungen auszustatten. Gleichzeitig beschleunigt es die Entwicklung, weil man oft einfach nur kurze Deklarationen schreiben muss, und sorgt für aufgeräumteren Code. Was will man mehr?