Aprenda / Ruby on Rails

IntermediárioAula 13 de 2016 min0% da trilha

Serializers avançados

Associação aninhada, atributo condicional por contexto e N+1 escondido dentro de um serializer — o que a lição básica deixou de fora.

A lição de serializers cobriu atributos explícitos e um atributo calculado. Faltam três coisas que aparecem o tempo todo num serializer de produção: aninhar uma associação inteira dentro da resposta, mudar o que sai dependendo de quem pediu, e evitar que essa mesma associação aninhada dispare uma query por registro.

Associação aninhada — serializando o relacionamento inteiro

app/serializers/pedido_serializer.rb + item_serializer.rb
class ItemSerializer
  include JSONAPI::Serializer

  attributes :quantidade
  belongs_to :produto, serializer: ProdutoSerializer
end

class PedidoSerializer
  include JSONAPI::Serializer

  attributes :status, :total
  belongs_to :usuario
  has_many :itens, serializer: ItemSerializer
end

serializer: ItemSerializer diz explicitamente qual serializer usar pra cada item da lista — sem isso, o gem não saberia formatar itens como objetos estruturados. O resultado é uma árvore completa: pedido, com seus itens, com o produto de cada item, tudo no mesmo JSON.

Serializer condicional por contexto

Às vezes o mesmo recurso precisa aparecer diferente dependendo de quem está pedindo — um campo que só administrador vê, por exemplo. Duplicar o serializer inteiro (ProdutoSerializer e AdminProdutoSerializer) só pra um campo a mais é desperdício; a opção if: resolve isso no mesmo serializer.

app/serializers/produto_serializer.rb
class ProdutoSerializer
  include JSONAPI::Serializer

  attributes :nome, :preco

  attribute :custo_interno, if: Proc.new { |_produto, params| params[:usuario_atual]&.admin? }
end
controller passando o contexto pro serializer
def show
  render json: ProdutoSerializer.new(
    @produto,
    params: { usuario_atual: current_usuario }
  ).serializable_hash
end

O Proc recebe o registro e os params passados na hora de instanciar o serializer — se usuario_atual não for admin, custo_interno simplesmente não entra na resposta. Mesma classe, mesmo model, JSON diferente dependendo de quem chamou.

N+1 escondido dentro de um serializer

O perigo de has_many :itens, serializer: ItemSerializer, que por sua vez tem belongs_to :produto, é que serializar uma lista de pedidos assim dispara uma query de itens por pedido, e uma query de produto por item — sem ninguém perceber, porque o N+1 não está no controller, está dentro da árvore de associações do serializer.

dispara N+1 ao listar
def index
  render json: PedidoSerializer.new(Pedido.all).serializable_hash
  # pra cada pedido: 1 query de itens + 1 query de produto por item
end
corrigido — carregado antecipadamente na consulta
def index
  pedidos = Pedido.includes(itens: :produto)
  render json: PedidoSerializer.new(pedidos).serializable_hash
end

includes(itens: :produto) carrega pedidos, itens e produtos em poucas consultas otimizadas, em vez de uma nova consulta pra cada registro percorrido. O serializer em si não muda nada — ele não sabe (nem precisa saber) se os dados vieram eager loaded; o ajuste sempre acontece na query, no controller.

Exercício

Um UsuarioSerializer tem attributes :nome. Adicione um atributo condicional email, visível só quando usuario_atual (passado via params) for o próprio usuário sendo serializado ou for admin.

Quiz rápido

No serializer com attribute :custo_interno, if: Proc.new { |_produto, params| params[:usuario_atual]&.admin? }, quando esse atributo aparece na resposta?
Por que Pedido.includes(itens: :produto) evita N+1 ao serializar uma lista de pedidos com itens aninhados?