Ein Pointer-Feld ist eine typisierte Referenz auf ein einzelnes Objekt; ein Relation-Feld ist ein verwalteter Join, der viele Objekte mit vielen verknüpft. Beide beantworten die Frage, vor der jedes Schema steht – wie referenzieren sich Datensätze gegenseitig? –, aber bei unterschiedlichen Kardinalitäten und zu unterschiedlichen Kosten. Die Wahl zwischen ihnen ist die zentrale Entscheidung der Datenmodellierung in einem BaaS, und die gute Nachricht ist: Sie lässt sich auf eine Frage eindampfen – wie viele, auf jeder Seite?
Das Wichtigste in Kürze
| Frage | Antwort |
|---|---|
| Pointer | Eine typisierte Referenz, gespeichert im Objekt – ein Fremdschlüssel mit Klasse |
| Relation | Eine unbegrenzte Menge von Referenzen in einer plattformverwalteten Join-Tabelle |
| Eins-zu-viele | Pointer auf der Viele-Seite, immer |
| Viele-zu-viele | Relation – oder ein Array von Pointern, wenn die Liste klein und begrenzt ist |
| Die Werkzeuge zum Durchlaufen | include() löst Pointer auf; eine Relation-Abfrage holt die Join-Menge |
Die zwei Formen, in 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
} Das Gegenstück in der relationalen Datenbank macht die Zuordnung explizit – ein Pointer ist ein typisierter Fremdschlüssel; eine Relation ist eine Junction-Tabelle, die Sie nie anlegen müssen:
-- Pointer: eine Spalte auf der Kindzeile (eins-zu-viele)
CREATE TABLE comment (
id serial PRIMARY KEY,
post_id integer REFERENCES post(id), -- ← der "Pointer"
body text
);
-- Relation: eine Junction-Tabelle (viele-zu-viele) — ein BaaS baut sie für Sie
CREATE TABLE post_tags (
post_id integer REFERENCES post(id),
tag_id integer REFERENCES tag(id),
PRIMARY KEY (post_id, tag_id)
);
Wie sich beides zur Abfragezeit auflöst
Der Pointer-Pfad ist der günstige: include() weist den Server an, jede Referenz vor der Antwort durch das vollständige Objekt zu ersetzen – ein Roundtrip, beliebige Tiefe über die Punktnotation (include('author.company')) und das Standardmittel gegen das N+1-Muster, Kinder in einer Schleife zu holen. Das Filtern nutzt denselben Sprung: equalTo('author', pointer) findet die Kommentare eines Beitrags, und matchesQuery() filtert eine Klasse anhand von Bedingungen an einer anderen – der gesamte Werkzeugkasten aus Joins in Dokumentdatenbanken.
Der Relation-Pfad erkauft etwas anderes. Weil die Mitgliedschaft in der Join-Struktur lebt und nicht in einem der beiden Objekte, kann ein Nutzer zehntausend Gruppen angehören und eine Gruppe eine Million Nutzer fassen, ohne dass eines der Dokumente um ein einziges Byte wächst. Der Preis ist ein zusätzlicher Sprung: include() durchläuft keine Relationen – die Join-Menge bekommt ihre eigene Abfrage.
Dazwischen liegt das Array von Pointern: ein Listenfeld, das typisierte Referenzen hält. Es ist die Ökonomie des Pointers, angewandt auf eine kleine Menge – ein einziges include() holt alle Elemente –, aber das Array lebt im Objekt, sodass jedes Element das Objekt schwerer zu lesen, zu speichern und zu synchronisieren macht. Jenseits einiger hundert Einträge wird das Containerobjekt zum Engpass, und das ist das Signal, dass Sie eine Relation als Array modelliert haben.
Pointer vs. Array von Pointern vs. Relation
Welches Beziehungswerkzeug sollten Sie verwenden?
| Dimension | Pointer | Array von Pointern | Relation |
|---|---|---|---|
| Kardinalität | Ein Ziel | Wenige, begrenzt | Unbegrenzt, viele-zu-viele |
| Speicherort | Im Objekt | Im Objekt | Verborgene Join-Tabelle |
| Abruf mit dem Elternobjekt | include() | include() | Eigene Relation-Abfrage |
| Wachstum des Objekts | Keines | Pro Element | Keines, niemals |
| Reihenfolge | entfällt | Bleibt erhalten | Nicht garantiert |
| Typisches Beispiel | Comment → Post | Bestellung → Positionen | Users ↔ Groups |
Zwei Details lohnen Aufmerksamkeit. Arrays bewahren die Reihenfolge der Elemente – Relationen nicht –, weshalb eine sortierte Liste (Titel einer Playlist, Schritte eines Workflows) eine Array-Frage ist, unabhängig vom Druck auf die Größe. Und eins-zu-viele hat zwei Kodierungen: Der Pointer auf dem Kind skaliert unbegrenzt, das Array auf dem Elternobjekt liest sich bequemer; entschieden wird es von der Obergrenze der Kardinalität, nicht vom Geschmack.
Typische Anwendungsfälle
- Eigentum und Urheberschaft.
createdBy,author,owner– Eins-zu-eins- und Eins-zu-viele-Pointer; die Referenz, die am Ende jede Klasse trägt. - Kommentarstränge und Aktivitäts-Feeds. Pointer auf dem Kind (
comment.post), abgefragt über das Elternobjekt – das unbegrenzte Eins-zu-viele-Arbeitspferd. - Tagging und Kategorisierung. Posts ↔ Tags, Produkte ↔ Kollektionen: viele-zu-viele, beide Seiten unbegrenzt – Relationen.
- Follower und Gruppenmitgliedschaft. Der klassische soziale Graph – Relationen, denn die Follower-Liste eines beliebten Kontos darf nicht im Konto-Objekt liegen.
- Positionen und kleine geordnete Mengen. Begrenzt, mit dem Elternobjekt gelesen, Reihenfolge zählt – hier verdienen Arrays von Pointern ihre Bequemlichkeit.
Pointer oder Relation? Eine Entscheidungsmatrix
| Greifen Sie zum Pointer, wenn … | Greifen Sie zur Relation, wenn … |
|---|---|
| Jedes Objekt genau ein Ziel referenziert | Beide Seiten unbegrenzt wachsen können |
| Es eins-zu-viele ist (Pointer auf dem Kind) | Es wirklich viele-zu-viele ist |
include() es mit dem Elternobjekt holen soll | Die Menge für sich abgefragt wird, nicht mit dem Elternobjekt |
| Die Referenz an Filtern und Sortierungen teilnimmt | Prüfungen der Mitgliedschaft überwiegen (ist X in Gruppe Y?) |
| Eine begrenzte, geordnete Liste stattdessen ins Array passt | Ein Array-Feld das Objekt sichtbar aufbläht |
Die verdichtete Faustregel: Pointer zuerst, Array als Zweites, Relation zuletzt – eskalieren Sie nur, wenn die Kardinalität Sie dazu zwingt. Die meisten Schemas sind am Ende überwiegend pointerbasiert, mit einer Handvoll echter Relationen, die die sozialen oder taxonomischen Kanten tragen. Entwerfen Sie das Schema um die Abfragen herum, die Sie tatsächlich ausführen werden, und indizieren Sie dann die Pointer-Felder, die diese Abfragen durchlaufen.
Grenzen und Trade-offs
- Relationen kosten einen zusätzlichen Sprung. Kein
include()durch eine Relation – Mitglieder zu holen ist eine zweite Abfrage, und sie serverseitig zu zählen ist in größerem Maßstab der einzig vernünftige Weg. - Arrays blähen sich stillschweigend auf. Der Fehlerfall ist schleichend: Das Objekt, das 20 Pointer hielt, hält ein Jahr später 2.000, und jeder Lesevorgang zahlt dafür. Setzen Sie eine Obergrenze, wenn Sie sich für das Array entscheiden.
- Pointer brauchen Indizes wie jeder Filter. Kinder ohne Index über den Eltern-Pointer abzufragen ist ein Collection-Scan im Gewand einer bequemen API.
- Keine kaskadierenden Löschungen. Einen Beitrag zu löschen löscht weder seine Kommentare noch räumt es seine Relationen auf – der Umgang mit Waisen ist Ihre Aufgabe, typischerweise in einem serverseitigen Delete-Trigger.
- Referenzielle Integrität hat nur empfehlenden Charakter. Ein Pointer kann ein gelöschtes Objekt referenzieren; die Plattform hält Sie nicht davon ab. Behandeln Sie ins Leere zeigende Referenzen als einen Zustand, dem Ihr Code begegnen kann.
Pointer und Relationen mit Back4app
Back4app ist eine Open-Source-Plattform für Backend as a Service (BaaS), die eine verwaltete Datenbank, automatisch generierte REST- und GraphQL-APIs, Authentifizierung, Dateispeicher und Serverless-Funktionen mit Cloud Code kombiniert. Pointer und Relation sind vollwertige Spaltentypen in ihrem Schema: Legen Sie beides im Dashboard an, und die automatisch generierten APIs unterstützen sofort include(), Relation-Abfragen und klassenübergreifende Filter aus jedem SDK – die Code-Tabs oben laufen unverändert. Die Join-Tabellen hinter Relationen werden von der Plattform angelegt, benannt und gepflegt, und Cloud-Code-Trigger sind der natürliche Ort für die Logik zum kaskadierenden Löschen und Aufräumen von Waisen, die das Modell selbst Ihnen überlässt.
Häufige Fragen
Was ist der Unterschied zwischen einem Pointer und einer Relation in einem BaaS-Datenmodell?
Ein Pointer speichert eine einzelne Referenz im Objekt selbst – ein typisierter Fremdschlüssel –, sodass jedes Objekt auf genau ein Ziel zeigt. Eine Relation speichert eine unbegrenzte Menge von Referenzen in einer verborgenen Join-Struktur, die die Plattform verwaltet. Pointer modellieren eins-zu-eins und eins-zu-viele; Relationen modellieren viele-zu-viele, wo Mitgliedslisten ohne Grenze wachsen.
Wie modelliere ich eine Eins-zu-viele-Beziehung mit Pointern?
Setzen Sie den Pointer auf die Viele-Seite: Jeder Comment trägt einen Pointer auf seinen Post, genau wie ein Fremdschlüssel. Um die Kommentare eines Beitrags aufzulisten, fragen Sie die Klasse Comment dort ab, wo der Pointer diesem Beitrag entspricht. Jedes Objekt bleibt klein, Schreibvorgänge bleiben günstig, und das Muster funktioniert in jeder Größenordnung – die Zahl der Kinder bläht das Elternobjekt nie auf.
Wann sollte ich ein Array von Pointern statt einer Relation verwenden?
Wenn die Liste klein und begrenzt ist und üblicherweise zusammen mit dem Elternobjekt gelesen wird – die zehn Zutaten eines Rezepts, die Positionen einer Bestellung. Arrays reisen im Objekt mit, ein einziger include()-Aufruf holt also alles; aber jedes Element vergrößert das Objekt, und jenseits einiger hundert Einträge werden Lese- und Speichervorgänge langsamer. Unbegrenzte oder geteilte Listen gehören in Relationen.
Kann ich über einen Pointer hinweg abfragen, ohne beide Objekte getrennt zu holen?
Ja – genau das leistet include(). Fragen Sie Posts ab und nutzen Sie include('author'): Die Plattform löst jeden Pointer serverseitig auf und liefert vollständige Author-Objekte in einer Antwort; die Punktnotation reicht tiefer, wie in include('author.company'). Auch in die andere Richtung funktioniert das Filtern: matchesQuery() wählt Elternobjekte anhand von Bedingungen am referenzierten Objekt aus.
Wie funktionieren Relation-Felder unter der Haube?
Die Plattform pflegt pro Relation-Feld eine verborgene Join-Tabelle, die Paare von Objekt-IDs speichert – dieselbe Struktur, die ein relationales Schema Junction-Tabelle nennen würde, nur ohne dass Sie sie entwerfen oder benennen. Abfragen auf die Mitgliedschaft treffen diese Struktur direkt, sodass keine der beiden Seiten der Beziehung wächst, wie viele Verknüpfungen sich auch ansammeln.
Werden Pointer-Felder automatisch indiziert?
Gehen Sie nicht davon aus – behandeln Sie Pointer-Felder wie jeden anderen Abfragefilter und indizieren Sie diejenigen, die Ihre Abfragen durchlaufen. Ein Eins-zu-viele-Nachschlagen durchsucht die Kindklasse nach einem passenden Pointer, und ohne Index ist das ein vollständiger Collection-Scan. Die stehende Regel aus der Datenbankindizierung gilt unverändert: Indizieren Sie, was Sie abfragen, besonders Join-Pfade.
Was ist schneller, ein Pointer oder eine Relation?
In der Regel Pointer – sie werden über include() innerhalb derselben Abfrage aufgelöst, ohne eine Join-Struktur zu befragen. Relationen kosten eine zusätzliche Abfrage oder einen internen Join gegen die verborgene Tabelle; das ist der Preis für unbegrenzte Kardinalität. Die praktische Regel: Greifen Sie zum günstigsten Werkzeug, das die Kardinalität erlaubt – Pointer zuerst, Array als Zweites, Relation zuletzt.