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.