Un champ Pointer est une référence typée vers un seul objet ; un champ Relation est une jointure gérée qui relie plusieurs objets à plusieurs autres. Les deux répondent à la question que se pose tout schéma — comment les enregistrements se référencent-ils entre eux ? — mais à des cardinalités et des coûts différents. Choisir entre les deux est la décision centrale de la modélisation de données sur un BaaS, et la bonne nouvelle, c’est que cette décision se résume à une question : combien, de chaque côté ?
Points clés
| Question | Réponse |
|---|---|
| Pointer | Une référence typée stockée dans l’objet — une clé étrangère avec une classe |
| Relation | Un ensemble illimité de références dans une table de jointure gérée par la plateforme |
| Un-à-plusieurs | Pointer du côté “plusieurs”, toujours |
| Plusieurs-à-plusieurs | Relation — ou un tableau de Pointers quand la liste est petite et bornée |
| Les outils de parcours | include() résout les Pointers ; une requête de Relation récupère l’ensemble joint |
Les deux formes, en code
// 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’équivalent en base de données relationnelle rend la correspondance explicite — un Pointer est une clé étrangère typée ; une Relation est une table de jointure que vous n’avez jamais à créer :
-- Pointer : une colonne sur la ligne enfant (un-à-plusieurs)
CREATE TABLE comment (
id serial PRIMARY KEY,
post_id integer REFERENCES post(id), -- ← le "pointer"
body text
);
-- Relation : une table de jointure (plusieurs-à-plusieurs) — un BaaS la construit pour vous
CREATE TABLE post_tags (
post_id integer REFERENCES post(id),
tag_id integer REFERENCES tag(id),
PRIMARY KEY (post_id, tag_id)
);
Comment chacun se résout au moment de la requête
Le chemin du Pointer est le moins coûteux : include() demande au serveur de remplacer chaque référence par l’objet complet avant de répondre — un seul aller-retour, une profondeur arbitraire via la notation pointée (include('author.company')), et le remède standard au pattern N+1 qui consiste à récupérer les enfants dans une boucle. Le filtrage traverse le même saut : equalTo('author', pointer) trouve les commentaires d’un post, et matchesQuery() filtre une classe selon des conditions portant sur une autre — toute la boîte à outils présentée dans requêtes relationnelles dans les bases documentaires.
Le chemin de la Relation achète autre chose. Comme l’appartenance vit dans la structure de jointure et non dans l’un ou l’autre objet, un utilisateur peut appartenir à dix mille groupes et un groupe contenir un million d’utilisateurs sans qu’aucun des deux documents ne grossisse d’un seul octet. Le coût, c’est un saut supplémentaire : include() ne traverse pas les Relations — l’ensemble joint a droit à sa propre requête.
Entre les deux se trouve le tableau de Pointers : un champ liste qui contient des références typées. C’est l’économie du Pointer appliquée à un petit ensemble — un seul include() récupère tous les éléments — mais le tableau vit dans l’objet, donc chaque élément alourdit la lecture, la sauvegarde et la synchronisation de l’objet. Au-delà de quelques centaines d’entrées, l’objet conteneur devient le goulot d’étranglement : le signe que vous avez modélisé une Relation sous forme de tableau.
Pointer vs. tableau de Pointers vs. Relation
Quel outil de relation choisir ?
| Dimension | Pointer | Tableau de Pointers | Relation |
|---|---|---|---|
| Cardinalité | Une cible | Peu, borné | Illimitée, plusieurs-à-plusieurs |
| Lieu de stockage | Dans l’objet | Dans l’objet | Table de jointure cachée |
| Récupération avec le parent | include() | include() | Requête de Relation séparée |
| Croissance de l’objet | Aucune | Par élément | Aucune, jamais |
| Ordre | n/a | Préservé | Non garanti |
| Exemple canonique | Comment → Post | Commande → lignes | Utilisateurs ↔ Groupes |
Deux détails méritent l’attention. Les tableaux préservent l’ordre des éléments — les Relations, non — donc une liste classée (pistes d’une playlist, étapes d’un workflow) relève du tableau, quelle que soit la pression sur la taille. Et le un-à-plusieurs a deux encodages : le Pointer sur l’enfant passe à l’échelle indéfiniment, le tableau sur le parent est plus pratique à lire ; c’est le plafond de cardinalité qui tranche, pas le goût.
Cas d’usage courants
- Propriété et paternité.
createdBy,author,owner— des Pointers un-à-un et un-à-plusieurs ; la référence que chaque classe finit par porter. - Fils de commentaires et flux d’activité. Pointer sur l’enfant (
comment.post), interrogé par parent — le cheval de trait du un-à-plusieurs illimité. - Tags et catégorisation. Posts ↔ tags, produits ↔ collections : du plusieurs-à-plusieurs, illimité des deux côtés — des Relations.
- Abonnés et appartenance à des groupes. Le graphe social classique — des Relations, car la liste d’abonnés d’un compte populaire ne doit pas vivre dans l’objet du compte.
- Lignes de commande et petits ensembles ordonnés. Bornés, lus avec le parent, l’ordre compte — c’est là que les tableaux de Pointers justifient leur commodité.
Devriez-vous utiliser un Pointer ou une Relation ? Matrice de décision
| Optez pour un Pointer quand… | Optez pour une Relation quand… |
|---|---|
| Chaque objet référence exactement une cible | Les deux côtés peuvent croître sans limite |
| C’est du un-à-plusieurs (Pointer sur l’enfant) | C’est réellement du plusieurs-à-plusieurs |
Vous voulez que include() le récupère avec le parent | L’ensemble est interrogé seul, pas avec le parent |
| La référence sert dans les filtres et les tris | Les vérifications d’appartenance dominent (X est-il dans le groupe Y ?) |
| Une liste bornée et ordonnée tiendrait plutôt dans un tableau | Un champ tableau alourdit visiblement l’objet |
L’heuristique condensée : Pointer d’abord, tableau ensuite, Relation en dernier — ne passez à l’étape suivante que lorsque la cardinalité vous y oblige. La plupart des schémas finissent massivement fondés sur des Pointers, avec une poignée de vraies Relations pour porter les arêtes sociales ou taxonomiques. Concevez le schéma autour des requêtes que vous exécuterez réellement, puis indexez les champs Pointer que ces requêtes parcourent.
Limites et trade-offs
- Les Relations coûtent un saut supplémentaire. Pas d’
include()à travers une Relation — récupérer les membres est une seconde requête, et les compter côté serveur est la seule façon raisonnable de faire à grande échelle. - Les tableaux gonflent en silence. La défaillance est progressive : l’objet qui contenait 20 Pointers en contient 2 000 un an plus tard, et chaque lecture en paie le prix. Fixez un plafond au moment où vous choisissez le tableau.
- Les Pointers ont besoin d’index comme n’importe quel filtre. Interroger des enfants par Pointer parent sans index, c’est un scan de collection déguisé en API pratique.
- Pas de suppression en cascade. Supprimer un post ne supprime ni ses commentaires ni ses Relations — la gestion des orphelins vous revient, généralement dans un trigger de suppression côté serveur.
- L’intégrité référentielle est indicative. Un Pointer peut référencer un objet supprimé ; la plateforme ne vous en empêchera pas. Traitez les références pendantes comme un état que votre code peut rencontrer.
Les Pointers et les Relations sur Back4app
Back4app est une plateforme open-source de Backend as a Service (BaaS) qui combine une base de données gérée, des API REST et GraphQL générées automatiquement, l’authentification, le stockage de fichiers et des fonctions serverless avec Cloud Code. Pointer et Relation sont des types de colonnes à part entière dans son schéma : créez l’un ou l’autre dans le dashboard, et les API générées automatiquement prennent immédiatement en charge include(), les requêtes de Relation et les filtres inter-classes depuis chaque SDK — les onglets de code ci-dessus fonctionnent sans modification. Les tables de jointure derrière les Relations sont créées, nommées et maintenues par la plateforme, et les triggers Cloud Code sont l’endroit naturel pour la logique de suppression en cascade et de nettoyage des orphelins que le modèle vous laisse.
Questions fréquentes
Quelle est la différence entre un Pointer et une Relation dans un modèle de données BaaS ?
Un Pointer stocke une seule référence dans l'objet lui-même — une clé étrangère typée —, si bien que chaque objet pointe vers exactement une cible. Une Relation stocke un ensemble illimité de références dans une structure de jointure cachée que la plateforme gère. Les Pointers modélisent le un-à-un et le un-à-plusieurs ; les Relations modélisent le plusieurs-à-plusieurs, où les listes d'appartenance grandissent sans limite.
Comment modéliser une relation un-à-plusieurs avec des Pointers ?
Placez le Pointer du côté "plusieurs" : chaque Comment porte un Pointer vers son Post, exactement comme une clé étrangère. Pour lister les commentaires d'un post, interrogez la classe Comment là où le Pointer est égal à ce post. Chaque objet reste léger, les écritures restent bon marché et le pattern fonctionne à n'importe quelle échelle — le nombre d'enfants ne fait jamais gonfler le parent.
Quand utiliser un tableau de Pointers plutôt qu'une Relation ?
Quand la liste est petite, bornée et généralement lue avec son parent — les dix ingrédients d'une recette, les lignes d'une commande. Les tableaux voyagent dans l'objet, donc un seul appel include() ramène tout ; mais chaque élément alourdit l'objet et, au-delà de quelques centaines d'entrées, lectures et sauvegardes ralentissent. Les listes illimitées ou partagées relèvent des Relations.
Peut-on interroger à travers un Pointer sans récupérer les deux objets séparément ?
Oui — c'est exactement le rôle de include(). Interrogez les Posts avec include('author') : la plateforme résout chaque Pointer côté serveur et renvoie les objets auteur complets dans une seule réponse ; la notation pointée va plus loin, comme dans include('author.company'). Le filtrage fonctionne aussi dans l'autre sens : matchesQuery() sélectionne les parents selon des conditions portant sur l'objet pointé.
Comment les champs Relation fonctionnent-ils en interne ?
La plateforme maintient une table de jointure cachée par champ Relation, qui stocke des paires d'ID d'objets — la structure qu'un schéma relationnel appellerait table de jointure, sans que vous ayez à la concevoir ni à la nommer. Les requêtes d'appartenance interrogent directement cette structure, si bien qu'aucun des deux côtés de la relation ne grossit, quel que soit le nombre de liens accumulés.
Les champs Pointer sont-ils indexés automatiquement ?
Ne le supposez pas — traitez les champs Pointer comme n'importe quel autre filtre de requête et indexez ceux que vos requêtes parcourent. Une recherche un-à-plusieurs parcourt la classe enfant à la recherche d'un Pointer correspondant, et sans index c'est un scan complet de la collection. La règle de base de l'indexation s'applique telle quelle : indexez ce que vous interrogez, surtout les chemins de jointure.
Lequel est le plus rapide, un Pointer ou une Relation ?
Les Pointers, en général — ils se résolvent dans la même requête via include(), sans structure de jointure à consulter. Les Relations coûtent une requête supplémentaire ou une jointure interne sur la table cachée ; c'est le prix d'une cardinalité illimitée. La règle pratique : prenez l'outil le moins coûteux que la cardinalité autorise — Pointer d'abord, tableau ensuite, Relation en dernier.