« HE:labs
HE:labs

Simplificando com Query Objects

Postado por Rafael Fiuza em 28/01/2014

Um dos grandes motivos do RubyOnRails ter esse alcance que tem hoje é a quantidade de funcionalidades que ficaram essenciais por diminuirem, melhorarem ou tornarem divertidos o processo de desenvolvimento. Scopes, do ActiveRecord, é uma dessas funcionalidades. O problema surge quando o que é para ser solução, torna-se problema.

Atualmente, é muito comum termos models cheios de lógica. A expressão "Fat model, skinny controller" nunca foi tão seguida. E o scope tem a sua parcela de culpa nessa tendência no rails. Em geral, quando temos uma query é comum torná-la um scope, mesmo que ela só seja chamada em um lugar.

Uma das ótimas dicas que o Bryan Helmkamp deu no post chamado "7 Patterns to Refactor Fat ActiveRecord Models" é extrair as queries para suas próprias query objects. No entanto, sentia falta de poder usar as "scopes" em cadeia, já que essa técnica só permite juntar duas queries através de composição.

Eu estava com um model que estava se tornando um problema quando veio um amigo, @maurogeorge, e me mostrou essa forma de extração com um passo a mais.

Imagine um model chamado Pokemon (apesar de esse nome levantar uma série de suposições) e que esse model estava tornando-se complexo e os scopes não estavam ajudando.

 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

E eu precisava fazer a query em cadeia:

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

Uma das funcionalidades que não é muito divulgada é a possibilidade de extender qualquer objeto ActiveRecord::Relation com suas scopes. Dessa forma, é possível extrair os scopes para query objects e manter a chamada em cadeia:

 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

Assim, extraímos as queries do model e mantivemos a possibilidade de chamar em cadeia, como precisava:

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

Ou podemos adicionar métodos que já encadeiam dentro do próprio objeto, como:

 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

Thats all, folks!

Compartilhe

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