Archive for Setembro 18th, 2008
named_scope – combinando buscas
Não há dúvidas de que o named_scope é um dos recursos mais úteis dentre todas as novas funcionalidades do Rails 2.1. O que nem todo mundo sabe é que o named_scope serve para muito mais do que simplesmente criar finders mais “bonitinhos”. A opção mais utilizada dentro do named_scope continua sendo :conditions, mas com um pouco de criatividade podemos criar named_scopes que, quando combinados, nos permitem fazer buscas bastante complexas.
A princípal funcionalidade do named_scope é possibilitar que você concatene seus métodos e vá refinando sua busca através de uma cadeia de chamadas aos scopes. Funciona como se você estivesse sempre chamando o scope sobre a própria classe que define o model. Um exemplo:
class Camiseta < ActiveRecord::Base named_scope :vermelhas, :conditions => ["cor = 'vermelha'"] end
E podemos usar isso assim:
v = Camiseta.vermelhas
Podemos melhorar isso e criar um named_scope que retorna camisetas de uma cor específica
named_scope :da_cor, lambda { |cor| { :conditions => ["cor = ?", cor] } }
Porque agora precisamos do lambda? Porque os named_scopes são definidos no momento em que a classe é carregada pelo interpretador Ruby. Se você não usar um lambda, não será possível definir o valor de retorno do bloco em tempo de execução e o scope acabará sendo sempre o mesmo, estático. Agora podemos fazer assim:
azuis = Camiseta.da_cor('azul')
E se quisermos pesquisar todas as camisetas e ordenar os resultados pela ordem alfabética das cores? Podemos criar um named_scope para isso também
named_scope :ordenado_por_cor, :order => 'cor'
E podemos usar assim:
camisetas = Camiseta.ordenado_por_cor
Vamos avançar mais um pouco. Quero agora poder escolher o campo pelo qual os resultados devem ser ordenados.
named_scope :ordenado_por, lambda { |coluna| { :order => coluna } }
E podemos usar assim
camisetas = Camiseta.ordenado_por(:cor)
Se agora no model Camiseta eu tiver também uma coluna para o preço, podemos combinar nossos named_scopes para fazer uma busca por camisetas de uma cor específica, ordenadas por preço.
amarelas = Camisetas.da_cor('amarela').ordenado_por(:preco)
Praticamente todos os parâmetros que você pode usar em um ActiveRecord::Base.find você pode usar também nos named_scopes, sendo possível gerar diversas buscas combinadas como a descrita acima.
Factory Girl para models com has_many
A idéia de usar fixtures para os testes no Rails parece muito boa em um primeiro momento, porém quando começamos a querer testar models com relacionamentos muito complexos manter essas fixtures se transforma em um verdadeiro inferno. Para cada fixture você acaba tendo que abrir vários arquivos e verificar na raça os valores dos ids referenciados nas demais fixtures associadas. É muito fácil se perder. Pior ainda, é totalmente improdutivo.
Para testar controllers esse problema pode e deve ser solucionado através do uso de algum framework para mocking, como o Mocha ou os recursos nativos de mocking do RSpec. Assim diminui-se a dependência dos models nos testes dos controllers e os testes executam mais rápido. Mas para os models nem sempre “mockar” todo o comportamento é uma boa solução. Muitas vezes você vai querer testar tudo que ocorre entre seus objetos e o banco de dados, incluindo os relacionamentos anteriormente citados.
Para os testes dos models tenho substituído as velhas fixtures pela utilização do factory_girl.
O que o factory_girl faz é possibilitar a criação de fábricas de objetos. Com essas fábricas, você pode gerar objetos de forma simplificada durante seus testes, elimando complemente a necessidade de utilização das fixtures. E com diversas vantagens.
Bom, eu não vou ficar falando sobre como usar o factory_girl, porque isso é realmente MUITO simples. Caso você ainda não conheça o factory_girl, basta olhar a documentação no site do projeto que com certeza você vai se virar bem.
O que eu gostaria de falar aqui é sobre como criar associações quando temos declaração do tipo :has_many em um dos nossos models. Eu tive que fuçar um pouquinho na net para conseguir encontrar essa informação, então talvez seja útil para mais alguém.
O fato é que cada vez que fazemos algo como
Factory.define :post do |p|
p.title "Blabla"
p.author { |author| author.association(:user) }
end
estamos apenas usando um proxy que usa uma espécie de “lazy loading” para instanciar objetos a partir da fábrica de Users. Logo, se você possui um model User com uma declaração do tipo belongs_to :post, a coisa vai funcionar e pronto. Mas e quando tenho algo como
class Post < ActiveRecord::Base has_many :comments end
como podemos “fabricar” uma instância de Post já com uma lista de comentários?
Assim (supondo que já exista a factory para o model Comment):
Factory.define :post do |p|
p.title "Blabla"
p.author { |author| author.association(:user) }
p.comments do |c|
[c.association(:comment), c.association(:comment)]
end
end
Perceberam? Cada vez que chamo o método association e passo o parâmetro :comment estou fazendo com que o factory_girl use a factory criada para o model Comment e gere uma nova instância desta classe, a qual é retornada como um elemento dentro de um array. Está feita minha fábrica com um relacionamento do tipo um-para-muitos.


