Scopes versus métodos

Artigos - 08/Jan/2020 - por João Almeida

A possibilidade de criar escopos em models no ActiveRecord é, com certeza, uma das funcionalidades favoritas de quem programa em Ruby on Rails. Neste artigo vamos apresentar um resumo do funcionamento de scopes e compará-los com o uso de métodos de classe.

Apresentando scopes

Indo direto ao ponto, imagine que você possui uma classe Product que representa os produtos disponíveis para venda em seu e-commerce e um dos atributos da classe é um boolean available, que indica se o produto está disponível ou não.

Para buscar todos produtos disponíveis você poderia escrever a consulta abaixo usando o método where do ActiveRecord:

Product.where(available: true)

O problema é que possivelmente você deve precisar consultar em diversas situações todos produtos disponíveis, criando a repetição da consulta acima em diferentes pontos do seu projeto.

Com o uso de scopes, essa mesma consulta poderia ser descrita no model Product:

class Product < ApplicationRecord
  scope :available, -> { where(available: true) }
end

Repare que um scope possui um nome, definido através de um symbol e uma expressão lambda contendo a consulta que deve ser realizada no banco de dados. Agora, é possível fazer a consulta com a chamada:

Product.available

Simples, não?! O próximo passo é criar scopes com parâmetros. Em nosso exemplo, vamos imaginar que queremos buscar produtos a partir de uma data de lançamento (release_date) específica.

Utilizando consultas do ActiveRecord teríamos:

Product.where("release_date > ?", release_date)

Já com scopes, precisamos enviar um parâmetro para a função lambda:

class Product < ApplicationRecord
  scope :released_after, ->(date) { where("release_date > ?", date) }
end

Pronto! Agora podemos consultar os produtos por data de lançamento com a chamada:

Product.released_after(1.week.ago)

E os métodos?

Avaliando como um scope é acionado nos exemplos acima, uma alternativa de código é o uso de métodos de classe do Ruby. Os scopes available e released_after poderiam ser definidos da seguinte forma:

class Product < ApplicationRecord
  def self.available
    where(available: true)
  end

  def self.released_after(date)
    where("release_date > ?", date)
  end
end

Na prática, a única diferença mais notável é a extensão do código que fica levemente maior, já que os scopes permitem usarmos uma sintaxe mais enxuta.

Além disso, ao executar tanto os scopes quanto os métodos de classe, a consulta ao banco de dados realizada pelo ActiveRecord é a mesma.

Com scopes:

Product.available.released_after(1.week.ago)
  Product Load (0.1ms)  SELECT  "products".* FROM "products" WHERE "products"."available" = ? AND (release_date > '2020-01-02 20:21:43.190632') LIMIT ?  [["available", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Relation []>

Com métodos:

Product.available.released_after(1.week.ago)
  Product Load (0.1ms)  SELECT  "products".* FROM "products" WHERE "products"."available" = ? AND (release_date > '2020-01-02 20:20:15.560645') LIMIT ?  [["available", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Relation []>

Quando usar scopes e quando usar métodos?

Apesar do resultado acima ter sido idêntico em ambos os casos, uma simples modificação no método poderia gerar problemas nas chamadas de outros métodos se realizadas em sequência.

class Product < ApplicationRecord
  def self.available()
    result = where(available: true)
    raise 'Error' if result.blank?
  end
end

Esse exemplo, por mais simples e óbvio, serve como referência para definirmos quando usar um ou outro: os scopes devem sempre retornar objetos do tipo ActiveRecord::Relation que permitem concatenar chamadas de outros scopes e até de métodos de consulta como first, count ou até mesmo mais uma condição via where.

Ao utilizar um método, você está indicando através do código a possibilidade de que seu retorno não seja necessariamente utilizado como parâmetro para uma próxima consulta ao banco de dados.

Foto de perfil do autor
João Almeida

Dev e instrutor na Campus Code