Continuando o trabalho em cima da biblioteca ArelOperators, há algumas novidades.

Para o pessoal que foi no encontro do Guru-SP, apresentei um pouco do trabalho. A idéia, conforme o post anterior sobre o assunto, é tornar o Arel mais transparente na hora de formar queries no ActiveRecord, aproveitando os recursos de Operator Overloading do Ruby.

A idéia é bem simples, na verdade, mas vem de uma dificuldade que eu acredito que muitos desenvolvedores têm: quando estamos escrevendo um código em qualquer linguagem, seria bom se pudéssemos apenas usar aquela linguagem para resolver nossos problemas. Não é conveniente usar duas, três linguagens no mesmo código-fonte (exceto, talvez, para o pessoal que usa Java, que tem que se entender com XML… ok, parei de zuar Java, juro!), e nem é recomendável de acordo com o fantástico livro “Clean Code”, do Robert Martin.

Então, por que SQL? Com uma linguagem expressiva como Ruby, e uma biblioteca fantástica como o Arel, não há mais motivos pra escrevermos “fragmentos de SQL” ou mesmo ficar fazendo “joins” e buscas estranhas manualmente. E isso trás algumas mudanças na cultura de buscas em banco de dados.

Para entender bem a mudança, basta pensar em como são estruturadas as buscas hoje. Em bancos de dados, quando se deseja retornar registros de mtas tabelas, faz-se “joins”, e usa-se o SELECT para retornar os dados das muitas tabelas dos Joins. Já quando se usa um mapeador objeto-relaciona, a coisa muda um pouco de figura: não se faz, normalmente, um “SELECT” com campos de várias tabelas, e sim “joins”, e pede-se para retornar todos os dados relacionados (eager loading), ou para retornar apenas a tabela onde se está pesquisando. Como exemplos, os códigos a seguir:

Person.where(:name => 'Mauricio') #Busca apenas os Mauricios da base
Person.where(:name => 'Mauricio').eager_load(:parents) #Busca os Mauricios, e também
#busca os pais dele (eager load). 

Até aqui, tudo bem. A coisa começa a ficar estranha quando se tenta buscar uma pessoa pelos dados de outras tabelas. Por exemplo, achar todas as pessoas que moram na rua “Jardim”:

Person.joins(:addresses).where('addresses.road' => "Jardim")

Aqui a coisa fica um pouco estranha. Isso porque, o ‘addresses.road’ é um campo de OUTRA tabela, e está sendo referenciado na tabela de pessoas. Além disso, o ‘addresses.road’ é, na verdade, um fragmento de SQL. Se errarmos o campo, por exemplo, escrevendo “address.road” (ao invés do plural), ganharemos um erro do banco de dados, não do Ruby ou do ActiveRecord. Mas esse não é o maior dos problemas. O maior dos problemas, ao meu ver, é o seguinte: normalmente, colocamos regras de busca em cada modelo (no Rails, scope ou named_scope), e essas regras servem para buscarmos corretamente determinadas coisas. Digamos, que no modelo Address, há regras para buscar os endereços mais rentáveis de um determinado produto: Aí, você quer buscar as pessoas que moram nesse endereço.

#ActiveRecord 2:
profitables = Address.profitables.all :include => :people
people = profitables.collect(&:people).flatten.uniq

O código acima tem um GRANDE problema: ele cria objetos DEMAIS (todos os endereços), quando na verdade, queremos apenas as pessoas. Além disso, ele pode retornar mais pessoas do que desejamos. Aí que entra o ActiveRecord 3:

profitables = Address.profitables #Nenhuma busca
people = Person.joins(:address) & profitables

Já melhorou bastante. Porém, ainda há coisas não resolvidas, por exemplo, fragmentos de SQL:

Person.where('name LIKE ?', 'M%)
Person.where('age > ?', 17)

Por que temos que agir desta forma, quando temos o Arel? Podemos escrever condições diretamente em Ruby. Já vi alguns plugins que permitiam escrever condições da seguinte forma:

Person.where(:age.gt => 17)

Mas isso exige MonkeyPatch do Symbol, e também não é muito expressivo. Frente a isso, eu extendi o ArelOperators, para poder usar condições mais “orientadas a objeto”. Dessa forma, hoje, é possível também usar condições em Ruby puro, tais como:

Person.where { (name =~ 'M%') & (age > 17) }
Person.where { |p| (p.name =~ 'M%') & (p.age > 17) }
#Esses dois códigos acima substituem o antigo:
Person.were('name LIKE ? AND age > ?', 'M%', 17)

Como tais condições traduzem-se para um Arel, é possível usá-las completamente, sem medo de não gerarem um SELECT válido. Além disso, não é mais necessário usar SQL, não se usam “black magic” tipo ParseTree, e não se tenta reinventar a roda (tudo é delegado para o Arel). E ainda, ganha-se os benefícios que eu citei no post anterior. How cool can it get?

O código, como sempre, está no Github. E a gem, chama-se “arel_operators”. Instale-a, extenda o ArelOperators em suas models, e bye bye SQL!


2 Comments

Victor Rodrigues · 2010-09-20 at 08:32

Muito legal, Mauricio!

Tem quem fale que tanto faz escrever expressões em string ou chamando métodos em Ruby, já que a linguagem não compila. Mas o ganho de legibilidade, cobertura de testes e praticidade mesmo é bem maior quando você pode chamar métodos, prefiro assim 😉

Dei uma olhada no teu código, compartilho da tua dúvida. Talvez o =~ passe a impressão de que um regex possa ser passado no right-side (embora caras de outras plataformas como Linq aceitem um Regex como expressão e convertam isto para SQL).

Boa sorte nessa gem, estou acompanhando!

guilhermesilveira · 2010-11-16 at 14:38

Muito boa!

O Anderson começou algo *muito* parecido e também tão legal quanto ao mesmo tempo e está aqui: http://github.com/caelum/relata

Abordagem muito parecida! Impressionante.

Abraço!

Comments are closed.