Tema
Documentos y colecciones
En MongoDB todo gira alrededor del documento. En esta lección aprenderás cómo es un documento, qué es BSON y por qué no es exactamente JSON, cómo funciona el _id y el ObjectId, qué tipos de datos existen, cómo anidar objetos y arrays, el límite de tamaño y cómo poner orden con la validación de esquema.
Concepto
txt
Servidor MongoDB
└── Base de datos "tienda"
├── Colección "productos" ← como una tabla, pero sin columnas fijas
│ ├── Documento { _id: 1, nombre: "Audífonos", precio: 899, ... }
│ ├── Documento { _id: 2, nombre: "Teclado", precio: 1250, ... }
│ └── ...
├── Colección "clientes"
└── Colección "pedidos"- Documento: un conjunto de pares
campo: valor, como un objeto de JavaScript. Es la unidad básica. - Colección: un grupo de documentos (normalmente del mismo "tipo"). No exige que todos tengan los mismos campos.
- Base de datos: un grupo de colecciones.
Las bases y colecciones se crean solas la primera vez que insertas un documento. No hay CREATE TABLE.
Anatomía de un documento
js
{
_id: ObjectId("66a1f0c2e4b0a1b2c3d4e5f6"), // identificador único (obligatorio)
nombre: "Audífonos Bluetooth", // texto
precio: 899, // número
disponible: true, // booleano
etiquetas: ["audio", "inalambrico"], // array
atributos: { // documento anidado (subdocumento)
color: "negro",
bateriaHoras: 30
},
creadoEn: ISODate("2024-05-02T15:30:00Z") // fecha
}Reglas de los nombres de campo:
- Distinguen mayúsculas:
precioyPrecioson campos distintos. - No pueden empezar con
$(reservado para operadores) y el punto.se usa para navegar subdocumentos. - Convención habitual en el ecosistema JS: camelCase (
bateriaHoras,creadoEn).
BSON: el JSON binario
MongoDB no guarda texto JSON; guarda BSON (Binary JSON). Al usarlo desde JavaScript lo ves como objetos normales, pero BSON añade cosas que JSON no tiene:
| JSON | BSON | |
|---|---|---|
| Formato | Texto | Binario (más rápido de recorrer) |
| Números | Un solo tipo number | Int32, Int64 (Long), Double, Decimal128 |
| Fechas | No existen (se usan textos) | Tipo Date nativo |
| Identificadores | No | ObjectId |
| Datos binarios | No | BinData |
| Otros | — | Timestamp, RegExp, MinKey/MaxKey... |
Tipos de datos más usados
| Tipo | En mongosh | Uso |
|---|---|---|
| String | "texto" | Nombres, descripciones (UTF-8) |
| Int32 / Double | 42, 3.14 | Cantidades, medidas (en mongosh, 42 se guarda como Double por defecto; usa NumberInt(42) para Int32) |
| Int64 | NumberLong("9007199254740993") | Contadores enormes |
| Decimal128 | NumberDecimal("899.90") | Dinero exacto |
| Boolean | true, false | Banderas |
| Date | new Date("2024-05-02") o ISODate(...) | Fechas (se guardan en UTC) |
| ObjectId | ObjectId() | Identificadores |
| Array | [1, 2, 3] | Listas |
| Object | { ... } | Subdocumentos |
| Null | null | Valor nulo explícito |
Dinero y fechas
- Dinero: un
Doubletiene los mismos problemas de redondeo queFLOATen SQL (0.1 + 0.2 = 0.30000000000000004). Para precios exactos usaDecimal128o guarda centavos como enteros (89990= $899.90). Para simplificar, en este curso usaremos precios enteros en pesos. - Fechas: guárdalas como tipo
Date, nunca como texto"02/05/2024". ConDatepuedes ordenar, filtrar por rangos y agrupar por mes.
js
// Comprobar el tipo de un campo
db.productos.find({ precio: { $type: "double" } })
db.productos.find({ precio: { $type: "string" } }) // ¡detecta precios guardados como texto!El campo _id
Todo documento debe tener un _id único dentro de su colección. Es la clave primaria y siempre tiene un índice.
- Si no lo indicas, MongoDB (o el driver) genera un ObjectId.
- Puedes usar tu propio valor: un número, un texto (SKU, email) o un subdocumento.
- No se puede modificar una vez creado.
js
db.categorias.insertOne({ nombre: "Electrónica" }) // _id generado: ObjectId(...)
db.categorias.insertOne({ _id: "libros", nombre: "Libros" }) // _id propio (texto)
db.categorias.insertOne({ _id: "libros", nombre: "Otro" })
// MongoServerError: E11000 duplicate key error ... dup key: { _id: "libros" }¿Qué es un ObjectId?
Un valor de 12 bytes (24 caracteres hexadecimales) diseñado para generarse en cualquier lugar sin coordinarse con nadie y sin repetirse:
txt
66a1f0c2 e4b0a1b2c3 d4e5f6
└──┬───┘ └───┬────┘ └─┬──┘
4 bytes 5 bytes 3 bytes
segundos valor contador
desde 1970 aleatorio incremental
(timestamp) (proceso)Consecuencias útiles:
js
const id = ObjectId("66a1f0c2e4b0a1b2c3d4e5f6");
id.getTimestamp() // ISODate("2024-07-25T...") — ¡la fecha de creación viene incluida!
// Ordenar por _id ≈ ordenar por fecha de creación
db.pedidos.find().sort({ _id: -1 }).limit(10) // los 10 más recientes¿ObjectId o _id propio?
Usa ObjectId por defecto. Usa un _id propio cuando exista un identificador natural, único e inmutable (un código de país "MX", un slug fijo). En los ejemplos de este curso usamos números (_id: 1) solo para que los resultados sean fáciles de leer.
Documentos anidados y arrays
Aquí está la gran diferencia con SQL: un documento puede contener subdocumentos y arrays, incluso arrays de subdocumentos.
js
db.clientes.insertOne({
_id: 1,
nombre: "Ana López",
email: "ana@correo.com",
direccion: { // subdocumento (1:1)
calle: "Av. Reforma 100",
ciudad: "Ciudad de México",
cp: "06600"
},
telefonos: ["5511111111", "5522222222"], // array de valores
metodosPago: [ // array de subdocumentos (1:pocos)
{ tipo: "tarjeta", terminacion: "4242", principal: true },
{ tipo: "oxxo", principal: false }
],
registro: new Date("2024-01-15")
})Para llegar a campos anidados se usa la notación de punto (entre comillas):
js
db.clientes.find({ "direccion.ciudad": "Ciudad de México" })
db.clientes.find({ "metodosPago.tipo": "oxxo" }) // busca dentro del array
db.clientes.find({}, { nombre: 1, "direccion.cp": 1 }) // proyección de un subcampo
db.clientes.updateOne({ _id: 1 }, { $set: { "direccion.cp": "06700" } })Límite de 16 MB
Un documento BSON puede medir como máximo 16 MB. Parece muchísimo (un libro entero en texto cabe varias veces), pero se alcanza cuando un array crece sin límite:
js
// ANTI-PATRÓN: todos los comentarios dentro del artículo
{
_id: 1,
titulo: "Artículo viral",
comentarios: [ /* 200,000 comentarios... crece sin fin */ ]
}Antes de llegar a 16 MB tendrás problemas de rendimiento: cada lectura trae el documento completo. Regla: arrays acotados (decenas o cientos de elementos) sí; arrays sin límite van en otra colección. Profundizarás en Índices y modelado.
Para archivos grandes (videos, PDFs) existe GridFS, aunque normalmente es mejor guardar el archivo en un almacenamiento de objetos (S3, Cloudinary) y en Mongo solo la URL.
Equivalencias SQL ↔ MongoDB
| SQL | MongoDB |
|---|---|
CREATE DATABASE tienda | use tienda (se crea al insertar) |
CREATE TABLE productos (...) | Implícito al insertar, o db.createCollection("productos") |
INSERT INTO productos ... | db.productos.insertOne({...}) |
SELECT * FROM productos | db.productos.find() |
SELECT nombre, precio FROM productos | db.productos.find({}, { nombre: 1, precio: 1 }) |
WHERE precio > 500 | find({ precio: { $gt: 500 } }) |
ORDER BY precio DESC LIMIT 5 | .sort({ precio: -1 }).limit(5) |
UPDATE ... SET stock = 0 WHERE ... | updateMany({...}, { $set: { stock: 0 } }) |
DELETE FROM ... WHERE ... | deleteMany({...}) |
SELECT COUNT(*) ... | countDocuments({...}) |
GROUP BY categoria | aggregate([{ $group: { _id: "$categoria" } }]) |
JOIN | $lookup (o datos embebidos) |
ALTER TABLE ADD COLUMN | No hace falta (o updateMany con $set para rellenar) |
PRIMARY KEY | _id |
UNIQUE | Índice único |
CHECK, NOT NULL | Validación $jsonSchema |
Validación de esquema con $jsonSchema
La flexibilidad no significa aceptar cualquier cosa. MongoDB puede validar los documentos de una colección con un esquema JSON:
js
db.createCollection("productos", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["nombre", "precio", "stock"],
properties: {
nombre: {
bsonType: "string",
minLength: 2,
description: "nombre obligatorio de al menos 2 caracteres"
},
precio: {
bsonType: ["int", "double", "decimal"],
minimum: 0,
description: "precio numérico mayor o igual a 0"
},
stock: {
bsonType: "int",
minimum: 0
},
categoria: {
enum: ["Electrónica", "Libros", "Hogar", "Deportes"]
},
etiquetas: {
bsonType: "array",
items: { bsonType: "string" },
uniqueItems: true
},
atributos: { bsonType: "object" }
}
}
},
validationLevel: "strict", // valida todos los inserts y updates
validationAction: "error" // rechaza (con "warn" solo registra en el log)
})js
db.productos.insertOne({ nombre: "Taza", precio: "barata", stock: NumberInt(5) })
// MongoServerError: Document failed validation
// (con detalles: precio debe ser numérico)
db.productos.insertOne({ nombre: "Taza térmica", precio: 199, stock: NumberInt(50), categoria: "Hogar" })
// { acknowledged: true, insertedId: ObjectId("...") }Agregar validación a una colección existente:
js
db.runCommand({
collMod: "clientes",
validator: {
$jsonSchema: {
bsonType: "object",
required: ["nombre", "email"],
properties: {
email: { bsonType: "string", pattern: "^.+@.+\\..+$" }
}
}
},
validationLevel: "moderate" // no valida documentos existentes que ya incumplían
})| Opción | Valores |
|---|---|
validationLevel | strict (todo), moderate (solo documentos que ya eran válidos), off |
validationAction | error (rechaza), warn (permite y registra) |
Buena práctica
Define validación en la base y en la aplicación (por ejemplo, con Mongoose o Zod). La validación de la app da mensajes amigables; la de la base protege contra scripts, otras apps o errores. Empieza con los campos obligatorios y los tipos; es lo que más problemas evita.
Paso a paso: diseñar un documento
Para un producto de la tienda:
- ¿Qué campos tienen todos? →
nombre,precio,stock,categoria(campos de primer nivel, obligatorios). - ¿Qué varía según el producto? → talla, voltaje, autor... → subdocumento
atributos. - ¿Hay listas cortas? →
etiquetascomo array. - ¿Algo crece sin límite? → reseñas → otra colección
resenasconproductoId. - ¿Tipos correctos? → precio numérico, fechas
Date, stock entero.
Ejercicios
- Escribe un documento para un libro de la tienda con: nombre, precio, stock, categoría, autor, número de páginas, etiquetas y fecha de publicación.
- ¿Qué fecha de creación tiene el
ObjectId("6650a3c0f1d2e3a4b5c6d7e8")? (UsagetTimestamp()en mongosh.) - ¿Por qué es mala idea guardar todos los pedidos de un cliente dentro del documento del cliente?
- Crea la colección
resenascon validación:productoIdycalificacionobligatorios;calificacionentero entre 1 y 5;comentariotexto opcional de máximo 500 caracteres.
Ver solución
js
// 1
db.productos.insertOne({
nombre: "Cien años de soledad",
precio: 289,
stock: 30,
categoria: "Libros",
etiquetas: ["novela", "clasico"],
atributos: {
autor: "Gabriel García Márquez",
paginas: 471,
publicadoEn: new Date("1967-05-30")
}
})
// 2
ObjectId("6650a3c0f1d2e3a4b5c6d7e8").getTimestamp()
// ISODate("2024-05-24T...") (los primeros 8 hex = 0x6650a3c0 segundos desde 1970)- Porque el array de pedidos crece sin límite: el documento del cliente se vuelve cada vez más pesado (cada lectura del perfil trae todo el historial) y podría acercarse a los 16 MB. Además, consultar "todos los pedidos de junio" sería incómodo. Los pedidos van en su propia colección con un
clienteId.
js
// 4
db.createCollection("resenas", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["productoId", "calificacion"],
properties: {
productoId: { bsonType: ["int", "double", "objectId"] },
calificacion: { bsonType: "int", minimum: 1, maximum: 5 },
comentario: { bsonType: "string", maxLength: 500 }
}
}
}
})Resumen
- Un documento es un conjunto de pares campo-valor (como un objeto JS); una colección agrupa documentos.
- MongoDB guarda BSON: añade
Date,ObjectId,Decimal128y enteros de distintos tamaños. - Todo documento tiene un
_idúnico; elObjectIdincluye la fecha de creación. - Los documentos admiten subdocumentos y arrays; accede a ellos con notación de punto (
"direccion.ciudad"). - Límite de 16 MB por documento: evita arrays que crecen sin límite.
- Usa
$jsonSchemapara validar campos obligatorios, tipos y rangos.