Pular para o conteúdo

Carga antecipada e o N+1

Duas consultas em vez de cento e uma — e a relação que atravessa um terceiro modelo.

O N+1 é o defeito de desempenho mais comum de qualquer ORM, e o mais fácil de não notar: ele funciona. A página abre, os dados estão certos, e a lentidão cresce com o tamanho da lista — o que em desenvolvimento, com dez linhas, não aparece.

O problema#

dataforge
// ERRADO — uma consulta para os clientes, e uma POR CLIENTE.
// Com cem clientes, sao cento e uma consultas.
//
//   clientes := Cliente.todos()
//   cycle c in clientes:
//       pedidos := Pedido.onde("cliente_id", c["id"]).buscar()
//
// CERTO — duas consultas, para qualquer quantidade:
//
//   clientes := Cliente.com(Cliente.todos(), "pedidos")

out "o numero de consultas nao pode crescer com o numero de linhas"
dataforge
adopt Arcane.Forge as Forge

db := Forge.memoria()
Forge.limpar_modelos()

Cliente := Forge.modelo("Cliente", {"id": "Serial", "nome": "Texto"})
Pedido := Forge.modelo("Pedido", {"id": "Serial", "cliente_id": "Inteiro", "total": "Decimal"})
cycle m in [Cliente, Pedido]:
    Forge.ligar(m, db)
    m.migrar()

Cliente.tem_muitos("pedidos", "Pedido", chave_externa := "cliente_id")
Pedido.pertence_a("cliente", "Cliente", chave_local := "cliente_id")

ana := Cliente.criar({"nome": "Ana"})
bob := Cliente.criar({"nome": "Bob"})
Pedido.criar({"cliente_id": ana["id"], "total": 99.9})
Pedido.criar({"cliente_id": ana["id"], "total": 45.0})
Pedido.criar({"cliente_id": bob["id"], "total": 12.0})

// DUAS consultas: uma para os clientes, outra para todos os
// pedidos deles.
cycle c in Cliente.com(Cliente.todos(), "pedidos"):
    out $"{c['nome']}: {len(c['pedidos'])} pedido(s)"

// E o outro lado, tambem em duas.
cycle p in Pedido.com(Pedido.todos(), "cliente"):
    out $"pedido {p['id']} e de {p['cliente']['nome']}"

Forge.fechar(db)

A relação que atravessa um terceiro#

O autor não tem comentários: ele tem posts, e os posts têm comentários. É a relação que não tem nome no dia a dia e aparece em todo sistema — e sem uma peça para ela, o caminho natural é carregar os posts, tirar os ids e consultar de novo. Quem escreve isso tende a fazer uma consulta por post: o N+1 com um passo a mais.

dataforge
adopt Arcane.Forge as Forge

db := Forge.memoria()
Forge.limpar_modelos()

Autor := Forge.modelo("Autor", {"id": "Serial", "nome": "Texto"})
Post := Forge.modelo("Post", {"id": "Serial", "autor_id": "Inteiro", "titulo": "Texto"})
Comentario := Forge.modelo("Comentario", {"id": "Serial", "post_id": "Inteiro", "texto": "Texto"})
cycle m in [Autor, Post, Comentario]:
    Forge.ligar(m, db)
    m.migrar()

// Autor -> (Post) -> Comentario
Autor.tem_muitos_atraves("comentarios", "Comentario", "Post")

ana := Autor.criar({"nome": "Ana"})
bob := Autor.criar({"nome": "Bob"})
p1 := Post.criar({"autor_id": ana["id"], "titulo": "Um"})
p2 := Post.criar({"autor_id": ana["id"], "titulo": "Dois"})
p3 := Post.criar({"autor_id": bob["id"], "titulo": "Tres"})

cycle t in ["otimo", "concordo", "hmm"]:
    Comentario.criar({"post_id": p1["id"], "texto": t})
Comentario.criar({"post_id": p2["id"], "texto": "no outro"})
Comentario.criar({"post_id": p3["id"], "texto": "do bob"})

// TRES consultas: autores, posts deles, comentarios desses posts.
cycle a in Autor.com(Autor.todos(), "comentarios"):
    out $"{a['nome']}: {len(a['comentarios'])} comentario(s)"

assert len(Autor.com(Autor.todos(), "comentarios")[0]["comentarios"]) is 4

Forge.fechar(db)

As cinco relações#

RelaçãoConsultasQuando
tem_um2um perfil por usuário
tem_muitos2os pedidos de um cliente
pertence_a2o cliente de um pedido
muitos_para_muitos2 (com tabela-ponte)as etiquetas de um post
tem_muitos_atraves3os comentários de um autor, pelos posts

Continue em Escopos e paginação e Em produção.