Un campo Pointer è un riferimento tipizzato a un singolo oggetto; un campo Relation è un join gestito che collega molti oggetti a molti altri. Entrambi rispondono alla domanda che ogni schema affronta – come si referenziano i record fra loro? – ma a cardinalità e costi diversi. Scegliere fra i due è la decisione centrale della modellazione dei dati su un BaaS, e la buona notizia è che la decisione si comprime in una sola domanda: quanti, da ciascun lato?
Punti chiave
| Domanda | Risposta |
|---|---|
| Pointer | Un riferimento tipizzato memorizzato *nell’*oggetto – una chiave esterna con una classe |
| Relation | Un insieme illimitato di riferimenti in una tabella di join gestita dalla piattaforma |
| Uno-a-molti | Pointer sul lato “molti”, sempre |
| Molti-a-molti | Relation – oppure un array di Pointer quando la lista è piccola e limitata |
| Gli strumenti di attraversamento | include() risolve i Pointer; una query sulla Relation recupera l’insieme del join |
Le due forme, in codice
// 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
} L’equivalente in un database relazionale rende esplicita la corrispondenza – un Pointer è una chiave esterna tipizzata; una Relation è una tabella di collegamento che non serve mai creare:
-- Pointer: una colonna sulla riga figlia (uno-a-molti)
CREATE TABLE comment (
id serial PRIMARY KEY,
post_id integer REFERENCES post(id), -- ← il "Pointer"
body text
);
-- Relation: una tabella di collegamento (molti-a-molti) – un BaaS la costruisce da sé
CREATE TABLE post_tags (
post_id integer REFERENCES post(id),
tag_id integer REFERENCES tag(id),
PRIMARY KEY (post_id, tag_id)
);
Come si risolve ciascuno al momento della query
Il percorso del Pointer è quello economico: include() dice al server di sostituire ogni riferimento con l’oggetto completo prima di rispondere – un solo round trip, profondità arbitraria con la notazione a punti (include('author.company')), e la cura standard per il pattern N+1 che recupera i figli in un ciclo. Il filtro funziona attraverso lo stesso salto: equalTo('author', pointer) trova i commenti di un post, e matchesQuery() filtra una classe in base a condizioni su un’altra – l’intera cassetta degli attrezzi è coperta in query relazionali nei database documentali.
Il percorso della Relation offre qualcosa di diverso. Poiché l’appartenenza vive nella struttura di join e non in uno dei due oggetti, un utente può appartenere a diecimila gruppi e un gruppo può contenere un milione di utenti senza che nessuno dei due documenti cresca di un solo byte. Il costo è un salto in più: include() non attraversa le Relation – l’insieme del join richiede una query a parte.
Fra i due sta l’array di Pointer: un campo lista che contiene riferimenti tipizzati. È l’economia del Pointer applicata a un piccolo insieme – un solo include() recupera tutti gli elementi – ma l’array vive dentro l’oggetto, quindi ogni elemento rende l’oggetto più pesante da leggere, salvare e sincronizzare. Oltre qualche centinaio di voci l’oggetto contenitore diventa il collo di bottiglia, ed è il segnale che una Relation è stata modellata come array.
Pointer vs. array di Pointer vs. Relation
Quale strumento di relazione conviene usare?
| Dimensione | Pointer | Array di Pointer | Relation |
|---|---|---|---|
| Cardinalità | Un bersaglio | Pochi, limitati | Illimitata, molti-a-molti |
| Dove si memorizza | Nell’oggetto | Nell’oggetto | Tabella di join nascosta |
| Recupero con il genitore | include() | include() | Query separata sulla Relation |
| Crescita dell’oggetto | Nessuna | Per elemento | Nessuna, mai |
| Ordinamento | n/d | Preservato | Non garantito |
| Esempio canonico | Comment → Post | Ordine → righe d’ordine | Utenti ↔ Gruppi |
Due dettagli meritano attenzione. Gli array preservano l’ordine degli elementi – le Relation no – quindi una lista ordinata (le tracce di una playlist, i passi di un workflow) è una questione da array a prescindere dalla pressione sulla dimensione. E l’uno-a-molti ha due codifiche: il Pointer sul figlio scala indefinitamente, l’array sul genitore si legge più comodamente; decide il tetto di cardinalità, non il gusto.
Casi d’uso comuni
- Proprietà e paternità.
createdBy,author,owner– Pointer uno-a-uno e uno-a-molti; il riferimento che ogni classe finisce per portare. - Thread di commenti e feed di attività. Pointer sul figlio (
comment.post), interrogato per genitore – il cavallo di battaglia dell’uno-a-molti illimitato. - Tag e categorizzazione. Post ↔ tag, prodotti ↔ collezioni: molti-a-molti, entrambi i lati illimitati – Relation.
- Follower e appartenenza ai gruppi. Il classico grafo sociale – Relation, perché la lista dei follower di un account popolare non deve vivere dentro l’oggetto account.
- Righe d’ordine e piccoli insiemi ordinati. Limitati, letti con il genitore, l’ordine conta – qui la comodità degli array di Pointer è pienamente giustificata.
Pointer o Relation? Matrice decisionale
| Scegliere un Pointer quando… | Scegliere una Relation quando… |
|---|---|
| Ogni oggetto referenzia esattamente un bersaglio | Entrambi i lati possono crescere senza limite |
| È un uno-a-molti (Pointer sul figlio) | È un molti-a-molti autentico |
Serve che include() lo recuperi con il genitore | L’insieme si interroga da solo, non con il genitore |
| Il riferimento partecipa a filtri e ordinamenti | Dominano i controlli di appartenenza (X è nel gruppo Y?) |
| Una lista limitata e ordinata sta in un array | Un campo array sta visibilmente gonfiando l’oggetto |
L’euristica compressa: prima il Pointer, poi l’array, per ultima la Relation – si sale di livello solo quando la cardinalità lo impone. La maggior parte degli schemi finisce per essere in larghissima parte basata su Pointer, con una manciata di vere Relation a reggere gli archi sociali o tassonomici. Lo schema va progettato attorno alle query che verranno eseguite davvero, indicizzando poi i campi Pointer che quelle query attraversano.
Limiti e trade-off
- Le Relation costano un salto in più. Nessun
include()attraverso una Relation – recuperare i membri è una seconda query, e contarli lato server è l’unico modo sano su larga scala. - Gli array si gonfiano in silenzio. Il modo di fallimento è graduale: l’oggetto che conteneva 20 Pointer ne contiene 2.000 un anno dopo, e ogni lettura paga. Un tetto va fissato nel momento in cui si sceglie l’array.
- I Pointer hanno bisogno di indici come qualsiasi filtro. Interrogare i figli per Pointer al genitore senza indice è una scansione della collection travestita da API comoda.
- Nessuna cancellazione a cascata. Eliminare un post non elimina i suoi commenti né ripulisce le sue Relation – la gestione degli orfani resta in mano a chi sviluppa, di norma in un trigger di cancellazione lato server.
- L’integrità referenziale non viene applicata. Un Pointer può referenziare un oggetto eliminato; la piattaforma non lo impedisce. I riferimenti pendenti vanno trattati come uno stato che il codice può incontrare.
Pointer e Relation su Back4app
Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. Pointer e Relation sono tipi di colonna di prima classe nel suo schema: basta creare l’uno o l’altra nella dashboard, e le API generate automaticamente supportano subito include(), query sulle Relation e filtri fra classi da ogni SDK – le schede di codice qui sopra girano senza modifiche. Le tabelle di join dietro le Relation vengono create, nominate e mantenute dalla piattaforma, e i trigger di Cloud Code sono la sede naturale della logica di cancellazione a cascata e di pulizia degli orfani che il modello in sé lascia a chi sviluppa.
Domande frequenti
Qual è la differenza fra un Pointer e una Relation in un modello di dati BaaS?
Un Pointer memorizza un solo riferimento dentro l'oggetto stesso – una chiave esterna con un tipo – così ogni oggetto punta esattamente a un bersaglio. Una Relation memorizza un insieme illimitato di riferimenti in una struttura di join nascosta che la piattaforma gestisce. I Pointer modellano l'uno-a-uno e l'uno-a-molti; le Relation modellano il molti-a-molti, dove le liste di appartenenza crescono senza limite.
Come si modella una relazione uno-a-molti con i Pointer?
Mettendo il Pointer sul lato "molti": ogni Comment porta un Pointer al suo Post, esattamente come una chiave esterna. Per elencare i commenti di un post, si interroga la classe Comment dove il Pointer è uguale a quel post. Ogni oggetto resta piccolo, le scritture restano economiche e il pattern funziona a qualsiasi scala – il numero di figli non gonfia mai il genitore.
Quando usare un array di Pointer invece di una Relation?
Quando la lista è piccola, limitata e di solito letta insieme al genitore – i dieci ingredienti di una ricetta, le righe di un ordine. Gli array viaggiano dentro l'oggetto, quindi una sola chiamata include() recupera tutto; ma ogni elemento fa crescere l'oggetto, e oltre qualche centinaio di voci letture e salvataggi rallentano. Le liste illimitate o condivise appartengono alle Relation.
Si può interrogare attraverso un Pointer senza recuperare i due oggetti separatamente?
Sì – è proprio ciò che fa include(). Interrogando Post con include('author'), la piattaforma risolve ogni Pointer lato server e restituisce gli oggetti autore completi in una sola risposta; la notazione a punti arriva più in profondità, come in include('author.company'). Il filtro funziona anche nell'altra direzione: matchesQuery() seleziona i genitori in base a condizioni sull'oggetto puntato.
Come funzionano i campi Relation sotto il cofano?
La piattaforma mantiene una tabella di join nascosta per ogni campo Relation, in cui memorizza coppie di ID di oggetti – la stessa struttura che uno schema relazionale chiamerebbe tabella di collegamento, senza che nessuno debba progettarla o darle un nome. Le query di appartenenza colpiscono direttamente quella struttura, così nessuno dei due lati della relazione cresce di dimensione per quanti collegamenti si accumulino.
I campi Pointer sono indicizzati automaticamente?
Meglio non darlo per scontato – i campi Pointer vanno trattati come qualsiasi altro filtro di query, indicizzando quelli che le query attraversano. Una ricerca uno-a-molti scansiona la classe figlia alla ricerca di un Pointer corrispondente, e senza indice è una scansione completa della collection. La regola fissa dell'indicizzazione dei database vale invariata: indicizzare ciò che si interroga, soprattutto i percorsi di join.
Qual è più veloce, un Pointer o una Relation?
In genere i Pointer – si risolvono dentro la stessa query tramite include(), senza nessuna struttura di join da consultare. Le Relation costano una query in più o un join interno sulla tabella nascosta; è il prezzo della cardinalità illimitata. La regola pratica: scegliere lo strumento più economico che la cardinalità consente – prima il Pointer, poi l'array, per ultima la Relation.