Un campo pointer es una referencia tipada a un solo objeto; un campo relation es un join gestionado que enlaza muchos objetos con muchos. Ambos responden la pregunta que todo esquema enfrenta — ¿cómo se referencian los registros entre sí? — pero a cardinalidades distintas y costos distintos. Elegir entre ellos es la decisión central del modelado de datos en un BaaS, y la buena noticia es que la decisión se comprime en una sola pregunta: ¿cuántos, de cada lado?
Puntos clave
| Pregunta | Respuesta |
|---|---|
| Pointer | Una referencia tipada guardada dentro del objeto — una clave foránea con clase |
| Relation | Un conjunto ilimitado de referencias en una tabla de unión gestionada por la plataforma |
| Uno-a-muchos | Pointer en el lado “muchos”, siempre |
| Muchos-a-muchos | Relation — o un array de pointers cuando la lista es pequeña y acotada |
| Las herramientas de recorrido | include() resuelve pointers; una query de relation trae el conjunto de la unión |
Las dos formas, en 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
} El equivalente en base de datos relacional hace explícito el mapeo — un pointer es una clave foránea tipada; una relation es una tabla de unión que nunca tienes que crear:
-- Pointer: una columna en la fila hija (uno-a-muchos)
CREATE TABLE comment (
id serial PRIMARY KEY,
post_id integer REFERENCES post(id), -- ← el "pointer"
body text
);
-- Relation: una tabla de unión (muchos-a-muchos) — un BaaS la crea por ti
CREATE TABLE post_tags (
post_id integer REFERENCES post(id),
tag_id integer REFERENCES tag(id),
PRIMARY KEY (post_id, tag_id)
);
Cómo se resuelve cada uno al momento de la query
El camino del pointer es el barato: include() le dice al servidor que cambie cada referencia por el objeto completo antes de responder — un solo viaje de ida y vuelta, profundidad arbitraria vía notación de punto (include('author.company')) y la cura estándar para el patrón N+1 de traer hijos dentro de un loop. El filtrado funciona en el mismo salto: equalTo('author', pointer) encuentra los comentarios de un post, y matchesQuery() filtra una clase por condiciones sobre otra — el kit completo está en consultas relacionales en bases de documentos.
El camino de la relation compra otra cosa. Como la membresía vive en la estructura de unión y no en ninguno de los dos objetos, un usuario puede pertenecer a diez mil grupos y un grupo puede contener un millón de usuarios sin que ningún documento crezca un solo byte. El costo es un salto extra: include() no atraviesa relations — el conjunto de la unión recibe su propia query.
Entre los dos está el array de pointers: un campo de lista que guarda referencias tipadas. Es la economía del pointer aplicada a un conjunto pequeño — un solo include() trae todos los elementos — pero el array vive dentro del objeto, así que cada elemento vuelve el objeto más pesado de leer, guardar y sincronizar. Pasadas unas cuantas centenas de entradas, el objeto contenedor se convierte en el cuello de botella, y esa es la señal de que modelaste una relation como array.
Pointer vs. array de pointers vs. relation
¿Qué herramienta de relación deberías usar?
| Dimensión | Pointer | Array de pointers | Relation |
|---|---|---|---|
| Cardinalidad | Un destino | Pocos, acotados | Ilimitada, muchos-a-muchos |
| Dónde se guarda | En el objeto | En el objeto | Tabla de unión oculta |
| Traer con el padre | include() | include() | Query de relation separada |
| Crecimiento del objeto | Ninguno | Por elemento | Ninguno, jamás |
| Orden | no aplica | Preservado | Sin garantía |
| Ejemplo canónico | Comment → Post | Pedido → líneas de pedido | Users ↔ Groups |
Dos detalles merecen atención. Los arrays preservan el orden de los elementos — las relations no —, así que una lista ordenada (las pistas de una playlist, los pasos de un workflow) es una cuestión de array sin importar la presión de tamaño. Y el uno-a-muchos tiene dos codificaciones: pointer-en-el-hijo escala indefinidamente, array-en-el-padre se lee con más comodidad; quien decide es el techo de cardinalidad, no el gusto.
Casos de uso comunes
- Propiedad y autoría.
createdBy,author,owner— pointers uno-a-uno y uno-a-muchos; la referencia que toda clase termina cargando. - Hilos de comentarios y feeds de actividad. Pointer en el hijo (
comment.post), consultado por el padre — el caballo de batalla del uno-a-muchos ilimitado. - Etiquetado y categorización. Posts ↔ tags, productos ↔ colecciones: muchos-a-muchos, ambos lados ilimitados — relations.
- Seguidores y membresía en grupos. El grafo social clásico — relations, porque la lista de seguidores de una cuenta popular no puede vivir dentro del objeto de la cuenta.
- Líneas de pedido y conjuntos pequeños y ordenados. Acotados, leídos junto con el padre, con el orden importando — aquí los arrays de pointers se ganan su comodidad.
¿Deberías usar un pointer o una relation? Matriz de decisión
| Ve por un pointer cuando… | Ve por una relation cuando… |
|---|---|
| Cada objeto referencia exactamente un destino | Ambos lados pueden crecer sin límite |
| Es uno-a-muchos (pointer en el hijo) | Es genuinamente muchos-a-muchos |
Quieres que include() lo traiga con el padre | El conjunto se consulta solo, no con el padre |
| La referencia participa en filtros y ordenamientos | Dominan las verificaciones de membresía (¿está X en el grupo Y?) |
| Una lista acotada y ordenada cabe en un array | Un campo array está inflando visiblemente el objeto |
La heurística comprimida: pointer primero, array segundo, relation al final — escala solo cuando la cardinalidad te obligue. La mayoría de los esquemas terminan abrumadoramente basados en pointers, con un puñado de relations de verdad cargando las aristas sociales o de taxonomía. Diseña el esquema alrededor de las queries que realmente vas a ejecutar, y luego indexa los campos pointer que esas queries atraviesan.
Limitaciones y trade-offs
- Las relations cuestan un salto extra. No hay
include()a través de una relation — traer los miembros es una segunda query, y contarlos del lado del servidor es la única forma sana a escala. - Los arrays se inflan en silencio. El modo de falla es gradual: el objeto que guardaba 20 pointers guarda 2.000 un año después, y cada lectura paga la cuenta. Pon un techo cuando elijas el array.
- Los pointers necesitan índices como cualquier filtro. Consultar hijos por el pointer del padre sin índice es un escaneo de colección disfrazado de API conveniente.
- No hay borrados en cascada. Borrar un post no borra sus comentarios ni limpia sus relations — el manejo de huérfanos es tu trabajo, típicamente en un trigger de borrado del lado del servidor.
- La integridad referencial es solo indicativa. Un pointer puede referenciar un objeto ya borrado; la plataforma no te lo va a impedir. Trata las referencias colgantes como un estado que tu código puede encontrarse.
Pointers y relations en Back4app
Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. Pointer y relation son tipos de columna de primera clase en su esquema: crea cualquiera de los dos en el dashboard y las APIs generadas automáticamente soportan de inmediato include(), queries de relation y filtros entre clases desde cualquier SDK — las pestañas de código de arriba corren sin cambios. Las tablas de unión detrás de las relations las crea, nombra y mantiene la plataforma, y los triggers de Cloud Code son el hogar natural para la lógica de borrado en cascada y limpieza de huérfanos que el modelo en sí te deja a ti.
Preguntas frecuentes
¿Cuál es la diferencia entre un pointer y una relation en un modelo de datos BaaS?
Un pointer guarda una sola referencia dentro del propio objeto — una clave foránea con tipo — de modo que cada objeto apunta a exactamente un destino. Una relation guarda un conjunto ilimitado de referencias en una estructura de unión oculta que la plataforma gestiona. Los pointers modelan uno-a-uno y uno-a-muchos; las relations modelan muchos-a-muchos, donde las listas de membresía crecen sin límite.
¿Cómo modelo una relación uno-a-muchos con pointers?
Pon el pointer en el lado "muchos": cada Comment lleva un pointer a su Post, exactamente como una clave foránea. Para listar los comentarios de un post, consulta la clase Comment donde el pointer sea igual a ese post. Cada objeto se mantiene pequeño, las escrituras siguen baratas y el patrón funciona a cualquier escala — la cantidad de hijos nunca infla al padre.
¿Cuándo debería usar un array de pointers en lugar de una relation?
Cuando la lista es pequeña, acotada y casi siempre se lee junto con su padre — los diez ingredientes de una receta, las líneas de un pedido. Los arrays viajan dentro del objeto, así que una sola llamada a include() lo trae todo; pero cada elemento agranda el objeto, y pasadas unas cuantas centenas de entradas las lecturas y los guardados se vuelven lentos. Las listas ilimitadas o compartidas pertenecen a las relations.
¿Puedo consultar a través de un pointer sin traer los dos objetos por separado?
Sí — para eso existe include(). Consulta Posts con include('author') y la plataforma resuelve cada pointer del lado del servidor, devolviendo los objetos de autor completos en una sola respuesta; la notación de punto llega más profundo, como en include('author.company'). El filtrado también funciona en la otra dirección: matchesQuery() selecciona padres por condiciones sobre el objeto apuntado.
¿Cómo funcionan los campos relation por debajo?
La plataforma mantiene una tabla de unión oculta por cada campo relation, guardando pares de IDs de objeto — la misma estructura que un esquema relacional llamaría tabla intermedia, sin que tú la diseñes ni la nombres. Las consultas de membresía golpean esa estructura directamente, así que ninguno de los dos lados de la relación crece de tamaño sin importar cuántos vínculos se acumulen.
¿Los campos pointer se indexan automáticamente?
No lo des por hecho — trata los campos pointer como cualquier otro filtro de consulta e indexa los que tus queries atraviesan. Un lookup uno-a-muchos recorre la clase hija buscando un pointer coincidente, y sin índice eso es un escaneo completo de la colección. La regla de siempre de la indexación aplica sin cambios: indexa lo que consultas, sobre todo los caminos de join.
¿Qué es más rápido, un pointer o una relation?
Los pointers, en general — se resuelven dentro de la misma query vía include(), sin estructura de unión que consultar. Las relations cuestan una query extra o un join interno contra la tabla oculta; ese es el precio de la cardinalidad ilimitada. La regla práctica: usa la herramienta más barata que la cardinalidad permita — pointer primero, array segundo, relation al final.