Skip to content

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: precio y Precio son 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:

JSONBSON
FormatoTextoBinario (más rápido de recorrer)
NúmerosUn solo tipo numberInt32, Int64 (Long), Double, Decimal128
FechasNo existen (se usan textos)Tipo Date nativo
IdentificadoresNoObjectId
Datos binariosNoBinData
Otros—Timestamp, RegExp, MinKey/MaxKey...

Tipos de datos más usados ​

TipoEn mongoshUso
String"texto"Nombres, descripciones (UTF-8)
Int32 / Double42, 3.14Cantidades, medidas (en mongosh, 42 se guarda como Double por defecto; usa NumberInt(42) para Int32)
Int64NumberLong("9007199254740993")Contadores enormes
Decimal128NumberDecimal("899.90")Dinero exacto
Booleantrue, falseBanderas
Datenew Date("2024-05-02") o ISODate(...)Fechas (se guardan en UTC)
ObjectIdObjectId()Identificadores
Array[1, 2, 3]Listas
Object{ ... }Subdocumentos
NullnullValor nulo explícito

Dinero y fechas

  • Dinero: un Double tiene los mismos problemas de redondeo que FLOAT en SQL (0.1 + 0.2 = 0.30000000000000004). Para precios exactos usa Decimal128 o 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". Con Date puedes 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 ​

SQLMongoDB
CREATE DATABASE tiendause tienda (se crea al insertar)
CREATE TABLE productos (...)Implícito al insertar, o db.createCollection("productos")
INSERT INTO productos ...db.productos.insertOne({...})
SELECT * FROM productosdb.productos.find()
SELECT nombre, precio FROM productosdb.productos.find({}, { nombre: 1, precio: 1 })
WHERE precio > 500find({ 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 categoriaaggregate([{ $group: { _id: "$categoria" } }])
JOIN$lookup (o datos embebidos)
ALTER TABLE ADD COLUMNNo hace falta (o updateMany con $set para rellenar)
PRIMARY KEY_id
UNIQUEÍndice único
CHECK, NOT NULLValidació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ónValores
validationLevelstrict (todo), moderate (solo documentos que ya eran válidos), off
validationActionerror (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:

  1. ¿Qué campos tienen todos? → nombre, precio, stock, categoria (campos de primer nivel, obligatorios).
  2. ¿Qué varía según el producto? → talla, voltaje, autor... → subdocumento atributos.
  3. ¿Hay listas cortas? → etiquetas como array.
  4. ¿Algo crece sin límite? → reseñas → otra colección resenas con productoId.
  5. ¿Tipos correctos? → precio numérico, fechas Date, stock entero.

Ejercicios ​

  1. 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.
  2. ¿Qué fecha de creación tiene el ObjectId("6650a3c0f1d2e3a4b5c6d7e8")? (Usa getTimestamp() en mongosh.)
  3. ¿Por qué es mala idea guardar todos los pedidos de un cliente dentro del documento del cliente?
  4. Crea la colección resenas con validación: productoId y calificacion obligatorios; calificacion entero entre 1 y 5; comentario texto 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)
  1. 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, Decimal128 y enteros de distintos tamaños.
  • Todo documento tiene un _id único; el ObjectId incluye 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 $jsonSchema para validar campos obligatorios, tipos y rangos.

Academia Web — aprende, copia y construye.