Utilizando Arel para compor expressões SQL

O Rails nos dá uma excelente DSL (Domain Specific Language) para construir a maioria de nossas queries, mas algumas vezes nos deparamos com cenários em que precisamos de queries mais complexas que acabam sendo difíceis de estruturar e, especialmente, de entender com a interface do ActiveRecord. Além disso, os predicados que ele disponibiliza são restritos, de forma que conseguimos conectar condições apenas com “AND”s e fazer comparações apenas com “=” e “IN”.

Geralmente para contornarmos isso recorremos novamente ao SQL puro, o que não é muito interessante uma vez que o Ruby é uma linguagem orientada a objetos. E apenas por esse motivo já faz sentido representar nossas queries como objetos ao invés de strings. Construir um modelo de objeto apropriado para suas queries lhe dá os mesmos benefícios que construir um modelo de objetos para o seu sistema.

A Arel é uma biblioteca para a construção de queries com dois objetivos: 1) simplificar a geração de queries mais complexas; e 2) abstrair a geração de SQL específico para cada SGBD. Outra grande vantagem de utilizar a biblioteca é que ela monta suas queries com base nos conceitos matemáticos da álgebra relacional, de forma a otimizá-la e gerar SQL específico para as características de cada banco de dados.

A própria interface do ActiveRecord é construída em cima da Arel. Sempre que você chama Model.find_by, Model.where e Model.joins, o ActiveRecord está utilizando a Arel para construir a string da query. Para ficar claro, a Arel apenas gera o código SQL, em momento algum faz acessos ao banco e recupera dados.

O método que o Rails disponibiliza para acessarmos a interface Arel “por trás” do ActiveRecord é a arel_table. Por exemplo, chamando Model.arel_table temos um objeto do tipo Arel::Table que age como um hash contendo cada coluna da tabela. Cada coluna por sua vez é do tipo Arel::Node, que possui vários métodos para a construção de suas queries. Você pode encontrar uma lista com a maioria dos seus métodos no arquivo predications.rb.

Para exemplificar os benefícios da utilização da interface vamos pensar no seguinte exemplo:

class Product < ActiveRecord::Base
    def products
        where(“released_at <= :now and (discontinued_at is null or discontinued_at > :now) and stock >= :now”, now: Time.zone.now, stock: 2)
    end
end

Para listar os produtos disponíveis temos as seguintes restrições: 1) sua data de lançamento deve ser anterior à data atual; 2) o produto não deve ter data de descontinuação ou deve ser posterior à data atual; 3) seu estoque deve ser maior ou igual a 1. Este método quando executado geraria o seguinte SQL:

SELECT "products".* FROM "products" WHERE (released_at <= '2015-09-10 14:10:25.941753' and (discontinued_at is null or discontinued_at > '2015-09-10 14:10:25.941753') and stock >= 1)

Utilizando Arel nosso método ficaria:

def products
    where(
        arel_table[:released_at].lteq(Time.zone.now)
            .and(arel_table[:discontinued_at].eq(nil)
            .or(
                arel_table[:discontinued_at].gt(Time.zone.now))
                    .and(arel_table[:stock].gteq(2))
            )
    )
end

Quando reescrito utilizando Arel, é mais fácil de entender cada parte da consulta, enquanto no SQL puro temos que entender a query como um todo. Isso se torna mais óbvio conforme a query cresce e fazemos joins, unions, etc. Neste exemplo fica claro também que a Arel apenas monta a query a ser executada de forma que temos que passa-la para o método “where” para que seja executada e por sua vez gerando o seguinte SQL:

SELECT "products".* FROM "products" WHERE ("products"."released_at" <= '2015-09-10 14:11:48.800957' AND ("products"."discontinued_at" IS NULL OR "products"."discontinued_at" > '2015-09-10 14:11:48.801094') AND "products"."stock" >= 2)

Vemos que ele também já cuida de algumas questões como eliminar a ambiguidade das colunas que podemos vir a ter no primeiro código caso façamos junções com outras tabelas com colunas semelhantes.

Podemos ainda melhorar este método quebrando suas seções em outros métodos de forma a melhorar sua leitura e reutilização de código:

def products
    where(released.and(in_production.and(in_stock)))
end

private

    def products_table
        Product.arel_table
    end

    def released
        products_table [:released_at].lteq(Time.zone.now)
    end

    def in_production
        products_table [:discontinued_at].eq(nil).or(products_table [:discontinued_at].gt(Time.zone.now))
    end

    def in_stock
        products_table [:stock].gteq(1)
    end

O resultado é um código um pouco mais extenso mas atribuímos nomes relevantes a intenção de cada parte que compõe a nossa consulta de forma que o código fica mais legível para o próprio desenvolvedor e programadores que eventualmente necessitarem ler o código da sua aplicação.

Com consultas mais complexas, pode ser um grande problema compreender facilmente o que uma query está buscando, tão quanto debugar cada parte para encontrar um problema. Com a Arel se torna possível melhorar estes problemas além de facilitar a reutilização de partes da consulta com cláusulas OR, AND ou até mesmo no escopo de cláusulas JOIN ON.

Ainda não encontramos boas documentações oficiais sobre a Arel – e talvez nunca encontremos – mas um bom lugar para procurar é através do próprio código da biblioteca em seu repositório, seu RubyDoc e o Scuttle, uma ferramenta online muito interessante que te ajuda a converter suas queries para Arel, interessante para estudos e para ajudá-lo nos seus projetos.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *