Entendendo e Resolvendo o Problema N+1 com Ruby on Rails
Tags: ruby, rails, backend, activerecord
Published: 2023-05-24 03:05:01

Na jornada para se tornar um desenvolvedor Rails, é inevitável encontrar certos desafios que fazem você parar, pensar e até mesmo refatorar o código que você pensava estar pronto para produção. Um desses desafios é o fatídico problema **N+1**, uma armadilha comum de desempenho que tem o potencial de desacelerar a sua aplicação Rails.

Entender esse problema é essencial para qualquer desenvolvedor Rails, pois ele está ligado à forma como o [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html), o ORM padrão utilizado pelo Rails, manipula as associações entre os modelos. Quando mal gerenciado, este problema pode resultar em uma quantidade excessiva de consultas ao banco de dados, prejudicando a performance do seu aplicativo.

## Entendendo N+1

O **N+1** é um padrão comum de ineficiência de desempenho que ocorre ao trabalhar com bancos de dados relacionais, como PostgreSQL ou MySQL. Essa questão ocorre quando estamos recuperando objetos associados em uma relação de um para muitos (one-to-many) ou muitos para muitos (many-to-many). Por exemplo, suponha que você tenha modelos de `User` e `Post`, e cada `User` tem muitos `Posts`. Se você quisesse carregar todos os usuários e seus respectivos posts, você pode acabar criando um cenário de problema **N+1** sem perceber.

Imaginando que  que você queira mostrar todos os posts de cada usuário. Você pode ser tentado a fazer algo assim:

```
User.all.each do |user|
  puts user.posts
end
```
Mas vamos destrinchar com calma o que esse código esta fazendo:

1. Quando você executa `User.all.each`, o Rails realiza uma consulta SQL para buscar todos os usuários. Isso representa a primeira consulta SQL, ou o "**1**" no problema "**N+1**".

```
User Load (6.9ms)  SELECT "users".* FROM "users"
```
2. No momento em que você executa `user.posts.each` dentro do loop, o Rails tem que buscar os posts para o usuário atual. Isso representa uma consulta SQL adicional para cada usuário. Esta consulta é executada para cada um dos seus usuários e são as consultas "**N**" no problema "**N+1**".
```
Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
```
3. Então, supondo que na sua base de dados possuam 1000 usuários no total, seu aplicativo faz 1001 consultas ao banco de dados para este único pedaço de código. Para um número grande de usuários, isso se torna altamente ineficiente, pois cada consulta ao banco de dados consome tempo e recursos do servidor.

## Resolvendo esse problema

O problema **N+1** pode ser enfrentado de várias maneiras. Algumas delas incluem a utilização de gems como a '[bullet](https://github.com/flyerhzm/bullet)' ou a '[goldiloader](https://github.com/salsify/goldiloader)', que são projetadas para ajudar a identificar e corrigir isso. Além do mais, técnicas de otimização de consultas, como 'preload', 'eager_load' e 'joins', também podem ser usadas dependendo do caso específico. No entanto, uma das soluções mais comuns e simples é o uso do 'eager loading'. '[Eager loading](https://www.tutorialspoint.com/entity_framework/entity_framework_eager_loading.htm#:~:text=Eager%20loading%20is%20the%20process,query%20results%20from%20the%20database.)' é uma técnica onde nós carregamos todas as associações necessárias de uma só vez de maneira antecipada. Isso é especialmente útil para evitar consultas excessivas ao banco de dados. No Rails, isso é facilitado pelo método `includes`.

```
users = User.includes(:posts)
users.each do |user|
  puts user.posts
end
```
Nesse bloco de código, a chamada `User.includes(:posts)` instrui o [ActiveRecord](`https://guides.rubyonrails.org/active_record_basics.html`) a carregar todos os usuários e seus respectivos posts em duas consultas separadas. Primeiro, ele executa uma consulta para buscar todos os usuários. Em seguida, com base nos `IDs` dos usuários retornados, executa uma segunda consulta para buscar todos os posts associados a esses usuários.

Essas consultas são armazenadas na memória, de modo que quando você itera sobre os usuários e seus posts na sequência `user.posts`, nenhuma consulta adicional é necessária, pois os dados já estão disponíveis.

Isso reduz significativamente o total de consultas, de 1001 para apenas 2, neste caso, independentemente do número de usuários. Portanto, ao utilizar o '`includes`' para carregar antecipadamente as associações necessárias, resolvemos eficientemente o problema **N+1** e otimizamos a performance da nossa aplicação."