« HE:labs
HE:labs

Turn simple with Query Objects

Postado por Rafael Fiuza em 18/01/2014

Ruby on Rails provides features that can simplify and improve our development process. That is one of the reasons why it is so popular and fun to use. Scopes is one of those features. The problem is when what is to be the solution becomes the problem.

It is very common to have a model full of logic. The term "Fat model, skinny controller" has never been so true. And the scope has its share of blame in this trend on rails. Make every query a scope is also common. Even if it is only called in one place.

One of the greatest tips that Bryan Helmkamp gave in the post called "7 Patterns to refactor ActiveRecord Fat Models" is to extract the queries to their own query objects. However, I missed being able to use the "scopes" chain, since this technique only allows joining two queries using composition.

I had a model that was becoming huge. Mauro George showed me this extraction method with a step further.

Imagine a model called Pokemon (although that name raise a series of assumptions). And imagine that this model was becoming complex and the scopes were not helping.

 1 class Pokemon < ActiveRecord::Base
 2   # Validations and associations stuffs
 3 
 4   # Devise stuffs
 5 
 6   scope :with_skill, -> (skill){ joins(:pokemon_skills).where("pokemon_skills.name = ?", skill) }
 7   scope :is_available, -> (date){ joins(:schedules).where('schedules.day_of_week = ?', time(date).wday) }-
 8   scope :with_weakness, -> (weakness){ joins(:pokemon_weakness).where("pokemon_weakness.name = ?", weakness) }
 9 
10   # Lot of model logic and stuffs
11 
12 end

And I needed to do a chain scope like:

1 Pokemon.with_skill("lightning").with_weakness("water").is_available(DateTime.now)

There's a little unknown feature that allow to extend any ActiveRecord::Relation object with their scopes. Using that it's possible to extract the query objects and scopes in order to keep the call chain:

 1 class PokemonQuery
 2   def initialize(relation = Pokemon.all)
 3     @relation = relation.extending(Scopes)
 4   end
 5 
 6   def search
 7     @relation
 8   end
 9 
10   module Scopes
11     def with_skill(skill)
12       joins(:pokemon_skills).where("pokemon_skills.name = ?", skill)
13     end
14 
15     def is_available(date)
16       joins(:schedules).where('schedules.day_of_week = ?', time(date).wday)
17     end
18 
19     def with_weakness(weakness)
20       joins(pokemon_weakness).where("pokemon_weakness.name = ?", weakness)      
21     end
22   end
23 
24 end

This way we extract the queries of the model and maintain the ability to chain scope, as needed:

1 PokemonQuery.new.search.with_skill("lightning").with_weakness("water").is_available(DateTime.now)

Or we can add methods that already chain together inside the own object, such as:

 1 class PokemonQuery
 2   def initialize(relation = Pokemon.all)
 3     @relation = relation.extending(Scopes)
 4   end
 5 
 6   def search
 7     @relation
 8   end
 9 
10   def like_pikachu
11     search.with_skill('eletric').with_weakness('ground')
12   end
13 
14   module Scopes
15     def with_skill(skill)
16       joins(:pokemon_skills).where("pokemon_skills.name = ?", skill)
17     end
18 
19     def is_available(date)
20       joins(:schedules).where('schedules.day_of_week = ?', time(date).wday)
21     end
22 
23     def with_weakness(weakness)
24       joins(pokemon_weakness).where("pokemon_weakness.name = ?", weakness)      
25     end
26   end
27 end
28 
29 PokemonQuery.new.like_pikachu

That is all folks!

Compartilhe

Sabia que nosso blog agora está no Medium? Confira Aqui!