Um campo pointer é uma referência tipada a um único objeto; um campo relation é um join gerenciado que liga muitos objetos a muitos. Os dois respondem à pergunta que todo schema enfrenta — como os registros referenciam uns aos outros? — mas em cardinalidades diferentes e a custos diferentes. Escolher entre eles é a decisão central da modelagem de dados em um BaaS, e a boa notícia é que a decisão se comprime em uma pergunta: quantos, de cada lado?
Principais pontos
| Pergunta | Resposta |
|---|---|
| Pointer | Uma referência tipada guardada dentro do objeto — chave estrangeira com classe |
| Relation | Um conjunto ilimitado de referências em uma tabela de junção gerenciada pela plataforma |
| Um-para-muitos | Pointer no lado “muitos”, sempre |
| Muitos-para-muitos | Relation — ou um array de pointers quando a lista é pequena e limitada |
| As ferramentas de travessia | include() resolve pointers; uma query de relation busca o conjunto da junção |
As duas formas, em código
// JavaScript / Node.js — Back4app JS SDK
// Pointer: one query, one hop — include() resolves the reference server-side
const posts = new Parse.Query('Post');
posts.equalTo('status', 'published');
posts.include('author'); // pointer → full Author object
const page = await posts.find();
const name = page[0].get('author').get('displayName');
// Relation: the unbounded join set gets its own query
const tags = await page[0].relation('tags').query().find();
// tags is a plain array of Tag objects — the join table stays invisible // Flutter / Dart — Back4app Flutter SDK
// Pointer: one query, one hop — includeObject resolves the reference server-side
final posts = QueryBuilder<ParseObject>(ParseObject('Post'))
..whereEqualTo('status', 'published')
..includeObject(['author']); // pointer → full Author object
final response = await posts.query();
final post = response.results!.first as ParseObject;
final author = post.get<ParseObject>('author');
// Relation: the unbounded join set gets its own query
final relation = post.getRelation('tags');
final tags = await relation.getQuery().query();
// tags.results is a plain list of Tag objects — the join table stays invisible // iOS / Swift — Back4app Swift SDK
// Pointer: one query, one hop — include() resolves the reference server-side
let posts = Post.query("status" == "published")
.include("author") // pointer → full Author object
posts.find { result in
if case .success(let page) = result {
print(page.first?.author?.displayName ?? "")
}
}
// Relation: the unbounded join set gets its own query
let tags = Tag.query(related(key: "tags", object: try post.toPointer()))
tags.find { result in
if case .success(let tagList) = result { render(tagList) }
} // Android / Kotlin — Back4app Android SDK
// Pointer: one query, one hop — include() resolves the reference server-side
val posts = ParseQuery.getQuery<ParseObject>("Post")
posts.whereEqualTo("status", "published")
posts.include("author") // pointer → full Author object
posts.findInBackground { page, e ->
val author = page?.firstOrNull()?.getParseObject("author")
println(author?.getString("displayName"))
}
// Relation: the unbounded join set gets its own query
val relation = post.getRelation<ParseObject>("tags")
relation.query.findInBackground { tags, e ->
if (e == null) render(tags) // the join table stays invisible
} O equivalente em banco relacional deixa o mapeamento explícito — um pointer é uma chave estrangeira tipada; uma relation é uma tabela de junção que você nunca precisa criar:
-- Pointer: uma coluna na linha filha (um-para-muitos)
CREATE TABLE comment (
id serial PRIMARY KEY,
post_id integer REFERENCES post(id), -- ← o "pointer"
body text
);
-- Relation: uma tabela de junção (muitos-para-muitos) — um BaaS cria isso para você
CREATE TABLE post_tags (
post_id integer REFERENCES post(id),
tag_id integer REFERENCES tag(id),
PRIMARY KEY (post_id, tag_id)
);
Como cada um resolve na hora da query
O caminho do pointer é o barato: include() manda o servidor trocar cada referência pelo objeto completo antes de responder — um round trip só, profundidade arbitrária via notação de ponto (include('author.company')), e a cura padrão para o padrão N+1 de buscar filhos dentro de um loop. O filtro funciona no mesmo salto: equalTo('author', pointer) acha os comentários de um post, e matchesQuery() filtra uma classe por condições em outra — o kit completo está em consultas relacionais em bancos de documentos.
O caminho da relation compra outra coisa. Como a participação vive na estrutura de junção, e não em nenhum dos dois objetos, um usuário pode pertencer a dez mil grupos e um grupo pode conter um milhão de usuários sem que nenhum documento cresça um byte sequer. O custo é um salto extra: include() não atravessa relations — o conjunto da junção ganha a própria query.
Entre os dois fica o array de pointers: um campo de lista guardando referências tipadas. É a economia do pointer aplicada a um conjunto pequeno — um include() traz todos os elementos — mas o array vive dentro do objeto, então cada elemento deixa o objeto mais pesado para ler, salvar e sincronizar. Passando de algumas centenas de entradas, o objeto contêiner vira o gargalo, e esse é o sinal de que você modelou uma relation como array.
Pointer vs. array de pointers vs. relation
Qual ferramenta de relacionamento você deve usar?
| Dimensão | Pointer | Array de pointers | Relation |
|---|---|---|---|
| Cardinalidade | Um alvo | Poucos, limitados | Ilimitada, muitos-para-muitos |
| Onde é guardado | No objeto | No objeto | Tabela de junção escondida |
| Buscar junto com o pai | include() | include() | Query de relation separada |
| Crescimento do objeto | Nenhum | Por elemento | Nenhum, nunca |
| Ordenação | não se aplica | Preservada | Sem garantia |
| Exemplo canônico | Comment → Post | Pedido → itens de linha | Users ↔ Groups |
Dois detalhes merecem atenção. Arrays preservam a ordem dos elementos — relations não —, então uma lista ordenada (faixas de uma playlist, etapas de um workflow) é uma questão de array independentemente da pressão de tamanho. E o um-para-muitos tem duas codificações: pointer-no-filho escala indefinidamente, array-no-pai é mais conveniente de ler; quem decide é o teto de cardinalidade, não o gosto.
Casos de uso comuns
- Posse e autoria.
createdBy,author,owner— pointers um-para-um e um-para-muitos; a referência que toda classe acaba carregando. - Threads de comentários e feeds de atividade. Pointer no filho (
comment.post), consultado pelo pai — o cavalo de batalha do um-para-muitos ilimitado. - Tags e categorização. Posts ↔ tags, produtos ↔ coleções: muitos-para-muitos, os dois lados ilimitados — relations.
- Seguidores e participação em grupos. O grafo social clássico — relations, porque a lista de seguidores de uma conta popular não pode morar dentro do objeto da conta.
- Itens de linha e conjuntos pequenos e ordenados. Limitados, lidos junto com o pai, com ordem importando — é aqui que arrays de pointers justificam a conveniência.
Você deveria usar um pointer ou uma relation? Matriz de decisão
| Vá de pointer quando… | Vá de relation quando… |
|---|---|
| Cada objeto referencia exatamente um alvo | Os dois lados podem crescer sem limite |
| É um-para-muitos (pointer no filho) | É genuinamente muitos-para-muitos |
Você quer que o include() traga junto com o pai | O conjunto é consultado sozinho, não junto com o pai |
| A referência participa de filtros e ordenações | Checagens de participação dominam (X está no grupo Y?) |
| Uma lista limitada e ordenada cabe num array | Um campo array está visivelmente inchando o objeto |
A heurística comprimida: pointer primeiro, array segundo, relation por último — só escale quando a cardinalidade obrigar. A maioria dos schemas termina esmagadoramente baseada em pointers, com um punhado de relations de verdade carregando as arestas sociais ou de taxonomia. Desenhe o schema em torno das queries que você vai realmente rodar e depois indexe os campos pointer que essas queries atravessam.
Limitações e trade-offs
- Relations custam um salto extra. Não existe
include()através de uma relation — buscar os membros é uma segunda query, e contá-los no servidor é a única forma sã em escala. - Arrays incham em silêncio. O modo de falha é gradual: o objeto que guardava 20 pointers guarda 2.000 um ano depois, e toda leitura paga a conta. Defina um teto quando escolher o array.
- Pointers precisam de índice como qualquer filtro. Consultar filhos pelo pointer do pai sem índice é um scan de coleção vestido de API conveniente.
- Não existe delete em cascata. Apagar um post não apaga os comentários dele nem limpa suas relations — cuidar dos órfãos é tarefa sua, tipicamente em um gatilho de delete no servidor.
- A integridade referencial é apenas indicativa. Um pointer pode referenciar um objeto já apagado; a plataforma não vai impedir. Trate referências penduradas como um estado que seu código pode encontrar.
Pointers e relations no Back4app
O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. Pointer e relation são tipos de coluna de primeira classe no schema dele: crie qualquer um dos dois no dashboard e as APIs geradas automaticamente já suportam na hora include(), queries de relation e filtros entre classes a partir de qualquer SDK — as abas de código acima rodam sem alteração. As tabelas de junção por trás das relations são criadas, nomeadas e mantidas pela plataforma, e os gatilhos de Cloud Code são a casa natural para a lógica de delete em cascata e limpeza de órfãos que o modelo em si deixa por sua conta.
Perguntas frequentes
Qual a diferença entre pointer e relation em um modelo de dados de BaaS?
O pointer guarda uma única referência dentro do próprio objeto — uma chave estrangeira com tipo — então cada objeto aponta para exatamente um alvo. A relation guarda um conjunto ilimitado de referências em uma estrutura de junção que a plataforma mantém escondida. Pointers modelam um-para-um e um-para-muitos; relations modelam muitos-para-muitos, onde as listas de participação crescem sem teto.
Como modelar um relacionamento um-para-muitos com pointers?
Coloque o pointer no lado "muitos": cada Comment carrega um pointer para o seu Post, exatamente como uma chave estrangeira faria. Para listar os comentários de um post, consulte a classe Comment onde o pointer é igual àquele post. Cada objeto continua pequeno, as escritas continuam baratas e o padrão funciona em qualquer escala — a quantidade de filhos nunca incha o pai.
Quando usar um array de pointers em vez de uma relation?
Quando a lista é pequena, limitada e quase sempre lida junto com o pai — os dez ingredientes de uma receita, os itens de um pedido. Arrays viajam dentro do objeto, então um único include() traz tudo; mas cada elemento engorda o objeto, e passando de algumas centenas de entradas as leituras e os saves ficam lentos. Listas ilimitadas ou compartilhadas pertencem a relations.
Dá para consultar através de um pointer sem buscar os dois objetos separadamente?
Sim — é exatamente o que o include() faz. Consulte Posts com include('author') e a plataforma resolve cada pointer no servidor, devolvendo os objetos de autor completos em uma resposta só; a notação de ponto vai mais fundo, como em include('author.company'). O filtro funciona na direção oposta também: matchesQuery() seleciona os pais por condições no objeto apontado.
Como os campos relation funcionam por baixo dos panos?
A plataforma mantém uma tabela de junção escondida para cada campo relation, guardando pares de IDs de objeto — a mesma estrutura que um schema relacional chamaria de tabela de junção, sem que você precise projetá-la ou nomeá-la. As consultas de participação batem direto nessa estrutura, então nenhum dos dois lados do relacionamento cresce de tamanho, não importa quantos vínculos se acumulem.
Campos pointer são indexados automaticamente?
Não parta desse pressuposto — trate campos pointer como qualquer outro filtro de query e indexe aqueles que suas consultas atravessam. Um lookup um-para-muitos varre a classe filha procurando um pointer correspondente, e sem índice isso é um full scan da coleção. A regra de sempre da indexação vale igual aqui: indexe o que você consulta, principalmente os caminhos de join.
O que é mais rápido, pointer ou relation?
Pointers, em geral — eles resolvem dentro da mesma query via include(), sem estrutura de junção para consultar. Relations custam uma query extra ou um join interno contra a tabela escondida; esse é o preço da cardinalidade ilimitada. A regra prática: use a ferramenta mais barata que a cardinalidade permitir — pointer primeiro, array segundo, relation por último.