Models do Rails com Materialized Views

Rails + PostgreSQL FTW

Se você já trabalhou com projetos maiores usando Rails, provavelmente já se deparou com situações em que foi necessário ir além das abstrações que o ActiveRecord oferece para utilizar as funções mais complexas do seu RDBMS.

Para ilustrar a situação, considere o seguinte cenário: precisamos implementar um relatório em um sistema já existente de aluguel de bicicletas, que exiba os modelos ordenados pelo número de vezes que foram alugados e mostre o total em dinheiro que foi arrecadado com os aluguéis de cada um.

Desconsiderando a logística mais complexa, como disponibilidade e quantidade de bicicletas, a modelagem do sistema fica como na imagem a seguir:

Gerado com a gem rails-erd

Queremos uma classe que retorne as informações do relatório no formato desejado. A forma como isso será apresentado não importa nesse momento. Pode ser uma view html, ou um CSV, por exemplo. Inicialmente criamos os seguintes testes para garantir o funcionamento:

Primeira tentativa: Heavylifting no Ruby

O método mais ingênuo de se construir o array de hashes que queremos é inicializar todos os objetos e iterar para construir o array no Ruby.

Essa abordagem funciona, mas não é muito performática quando se precisa lidar com grandes quantidades de dados. O benchmark abaixo foi feito com uma base de 100.000 registros de aluguel no banco.

Repare que se leva 10 segundos para retornar o array e isso não inclui o tempo de tratar a requisição e renderizar as views, que existiria num cenário real. O problema se torna maior ainda se houverem várias requisições simultâneas para acessar esse relatório. Num cenário de listagem numa view, o problema poderia ser minimizado com paginação, mas isso não funcionaria para um download de uma tabela, por exemplo.

Tentativa 2: ActiveRecord + SQL

Nessa abordagem tentamos resolver a situação utilizando as abstrações do ActiveRecord, e, onde não for mais possível, SQL puro.

Essa classe também passa nos mesmos testes que definimos anteriormente. Abaixo um benchmark comparando as duas abordagens:

O relatório rodou em 0.13 segundos! 77 vezes mais rápido que a primeira abordagem. No entanto, se, no contexto em que o sistema for desenvolvido, o tempo para gerar o relatório for mais importante do que o quão atualizado ele está, podemos ter uma solução ainda mais elegante e performática.

Trabalhando com Materialized Views

Uma database view é basicamente um conjunto de resultados de queries que podem ser tratados como uma tabela. Toda vez que uma view é chamada, as queries são executadas e os resultados podem ser usados como uma tabela para outras operações. Views podem ser usadas, por exemplo, para abstrair uma série de tabelas que contém colunas de conteúdo sensível, garantindo para alguns usuários acesso somente à view, e não às tabelas abstraídas.

Uma materialized view é um tipo de view cujos resultados são salvos em disco e pode ser atualizada manualmente, mas não diretamente. É esse tipo que utilizaremos.

Criando uma view

Para criar uma view, basta gerar uma migration com o SQL desejado.

Existem alguns problemas com esse método. Um deles é que você vai escrever um SQL extenso sem o syntax highlighting do seu editor. Outro é que se for necessário atualizar essa view, você vai precisar copiar o método up dessa versão no método down da próxima, para que a migration seja reversível. Para resolver esses e outros problemas, o pessoal da Thoughtbot criou a gem scenic.

Utilizando a Scenic

Depois de fazer o setup usual da dependência adicionando-a ao Gemfile, você terá disponíveis alguns métodos e generators a mais. Para criar a migration e a view, rodamos o comando:

$ rails g scenic:view bikes_reports --materialized
create db/views
create db/views/bikes_reports_v01.sql
create db/migrate/20160801212120_create_bikes_reports.rb

Com isso temos dois arquivos novos: a migration, que utiliza a DSL do scenic para carregar o SQL; e o .sql, que é onde a definição da view vai ficar. Repare que o nome do arquivo sql tem a versão da view. Isso é utilizado pela scenic para atualizar ou reverter as views para determinadas versões. Para mais detalhes sobre o funcionamento, confira o repositório.

Nosso setup fica assim:

Agora podemos criar o model chamado BikesReport e tudo deve funcionar como se estivesse usando uma tabela real. Isso acontece porque o ActiveRecord não diferencia a view de uma tabela, já que o próprio Postgres as trata da mesma maneira.

Entretanto, é importante considerar alguns pontos:

  • Se chamarmos save em um dos objetos desse model teremos um erro em nível de banco de dados. Definindo readonly? ainda vai dar erro se tentar salvar, mas um erro específico, deixando claro que é de somente leitura;
  • Precisamos de uma forma cômoda de atualizar a view (você provavelmente vai querer fazer isso num worker)

O model vai ficar da seguinte forma:

E finalmente a abordagem utilizando a view que criamos:

Abaixo um benchmark comparando os 3 métodos (com a view previamente atualizada):

Como esperado, a abordagem utilizando views é ainda mais performática, sendo 11 vezes mais rápida que a abordagem AR + SQL e 924 vezes (!!!) mais rápida que a do ruby.

O projeto que usei como exemplo está disponível em https://github.com/jaimerson/rent_a_bike. Espero que o exercício tenha sido valioso e que essa metodologia não tão documentada possa te ajudar no futuro :)

Referências

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.