Dynamische Anwendung von Scopes

Was tun, wenn man Scopes auf eine ActiveRecord-Klasse dynamisch anwenden möchte ohne zig Klassenmethoden dafür implementieren zu müssen oder schlecht lesbare if-elsif-Konstrukte zu generieren? Diese Frage habe ich mir vor kurzem gestellt und ich bin zu folgendem Ergebnis gekommen.

Vorgeplänkel

Gehen wir für das folgende Beispiel davon aus, dass wir eine ActiveRecord-Klasse Human implementiert haben. Man beachte, dass die Scopes mit squeel definiert worden sind.

class Human < ActiveRecord::Base
  
  scope :order_by_created_at, lambda { order{created_at.desc} }
  scope :order_by_updated_at, lambda { order{updated_at.desc} }
  scope :order_by_name, lambda { order{name.desc} }

end

Beispiellösung

In meinem Fall war die Anforderung eine Klassenmethode auf Human zu definieren, die einen Hash mit Sortierungsparametern entgegen nimmt und anschließend Humans entsprechend der übergebenen Parameter sortiert zurückliefert. Möchten wir nun - aus welchen Gründen auch immer - die oben definierten Scopes anwenden um Humans sortiert abzufragen, können wir dies wie unten gezeigt bewerkstelligen.

class Human < ActiveRecord::Base
  
  # ...

  def self.order_by(*options)
    options = options.first || {}
    order    = ( options[:order].nil? || !options[:order].is_a?(Hash) ) ? {} : options.delete(:order)

    # Dynamically add scopes that order the humans by created_at, updated_at and name
    order.each do |key, value|
      scope_chain << "order_by_name" if key == :name
      scope_chain << "order_by_updated_at" if key == :updated_at
      scope_chain << "order_by_created_at" if key == :created_at
    end
          
    # Chain the relevant scopes and call each on Human
    scope_chain.inject(Human) { |obj, scope| obj.send(scope) }

  end

end

Der Methode order_by kann ein Hash order mit definierten Keys übergeben werden (hier sind es die Keys :name, :updated_at sowie created_at). Die Werte für jeden Key spielen keine Rolle. Alternativ hätte man auch ein Array nehmen können. Für jeden Hash-Key wird nun geprüft, ob dazu ein definierter Scope existiert. Ist dies der Fall wird der Scope-Name als String in ein Array scope_chain geschrieben. Schließlich wird ausgehend von Human mittels der inject-Methode jeder Scope durch send auf Human angewendet. Das Ergebnis wird zurückgeliefert.

Dieses Beispiel ist sehr kurz gehalten. In meiner Implementierung kommen noch einige Scopes mehr dazu sowie ein Defaut-Scoping, dass immer angewendet wird.