Tema
CRUD
CRUD (Create, Read, Update, Delete) es lo que hace cualquier aplicación: dar de alta, consultar, editar y borrar. En esta lección cargarás los datos de ejemplo de la tienda que usaremos el resto del curso y dominarás los métodos de MongoDB para cada operación: insertOne/insertMany, find/findOne con proyecciones, updateOne/updateMany con sus operadores, upsert, replaceOne y deleteOne/deleteMany.
Concepto
| CRUD | SQL | MongoDB |
|---|---|---|
| Create | INSERT | insertOne(), insertMany() |
| Read | SELECT | find(), findOne(), countDocuments() |
| Update | UPDATE | updateOne(), updateMany(), replaceOne(), findOneAndUpdate() |
| Delete | DELETE | deleteOne(), deleteMany(), findOneAndDelete() |
Todos se llaman sobre una colección: db.<coleccion>.<metodo>(...). Casi todos reciben un filtro (qué documentos) y, según el caso, una actualización u opciones.
Datos de ejemplo: la tienda
Guarda este script como semilla.js y ejecútalo con mongosh "TU_URI/tienda" semilla.js (o pégalo en mongosh tras use tienda). Usamos _id numéricos para que los resultados sean fáciles de leer.
js
// semilla.js — datos de ejemplo de la tienda (curso de MongoDB)
db = db.getSiblingDB("tienda");
db.productos.drop();
db.clientes.drop();
db.pedidos.drop();
db.productos.insertMany([
{ _id: 1, nombre: "Audífonos Bluetooth", categoria: "Electrónica", precio: 899, stock: 25,
etiquetas: ["audio", "inalambrico"], atributos: { color: "negro", bateriaHoras: 30 } },
{ _id: 2, nombre: "Teclado mecánico", categoria: "Electrónica", precio: 1250, stock: 10,
etiquetas: ["gamer", "oficina"], atributos: { color: "blanco", switches: "red" } },
{ _id: 3, nombre: "Monitor 24 pulgadas", categoria: "Electrónica", precio: 3499, stock: 5,
etiquetas: ["gamer", "oficina"], atributos: { pulgadas: 24, hz: 144 } },
{ _id: 4, nombre: "Libro Aprende SQL", categoria: "Libros", precio: 349, stock: 40,
etiquetas: ["programacion"], atributos: { autor: "Equipo Academia", paginas: 320 } },
{ _id: 5, nombre: "Cien años de soledad", categoria: "Libros", precio: 289, stock: 30,
etiquetas: ["novela", "clasico"], atributos: { autor: "Gabriel García Márquez", paginas: 471 } },
{ _id: 6, nombre: "Lámpara de escritorio", categoria: "Hogar", precio: 459, stock: 15,
etiquetas: ["oficina"], atributos: { color: "negro", watts: 9 } },
{ _id: 7, nombre: "Taza térmica", categoria: "Hogar", precio: 199, stock: 50,
etiquetas: ["cocina"], atributos: { capacidadMl: 450 } },
{ _id: 8, nombre: "Cable USB-C", precio: 99, stock: 100, etiquetas: [] } // sin categoría
]);
db.clientes.insertMany([
{ _id: 1, nombre: "Ana López", email: "ana@correo.com",
direccion: { ciudad: "Ciudad de México", cp: "06600" }, registro: new Date("2024-01-15") },
{ _id: 2, nombre: "Carlos Ruiz", email: "carlos@correo.com",
direccion: { ciudad: "Guadalajara", cp: "44100" }, registro: new Date("2024-02-03") },
{ _id: 3, nombre: "María Pérez", email: "maria@correo.com",
direccion: { ciudad: "Monterrey", cp: "64000" }, registro: new Date("2024-02-20") },
{ _id: 4, nombre: "Jorge Díaz", email: "jorge@correo.com",
direccion: { ciudad: "Ciudad de México", cp: "03100" }, registro: new Date("2024-03-11") },
{ _id: 5, nombre: "Lucía Gómez", email: "lucia@correo.com",
direccion: { ciudad: "Puebla", cp: "72000" }, registro: new Date("2024-04-02") } // sin pedidos
]);
db.pedidos.insertMany([
{ _id: 1, clienteId: 1, fecha: new Date("2024-05-02"), estado: "entregado",
items: [ { productoId: 1, nombre: "Audífonos Bluetooth", cantidad: 1, precio: 899 },
{ productoId: 4, nombre: "Libro Aprende SQL", cantidad: 2, precio: 349 } ],
total: 1597 },
{ _id: 2, clienteId: 2, fecha: new Date("2024-05-10"), estado: "entregado",
items: [ { productoId: 2, nombre: "Teclado mecánico", cantidad: 1, precio: 1250 },
{ productoId: 7, nombre: "Taza térmica", cantidad: 3, precio: 199 } ],
total: 1847 },
{ _id: 3, clienteId: 1, fecha: new Date("2024-06-01"), estado: "enviado",
items: [ { productoId: 3, nombre: "Monitor 24 pulgadas", cantidad: 1, precio: 3499 } ],
total: 3499 },
{ _id: 4, clienteId: 3, fecha: new Date("2024-06-15"), estado: "pendiente",
items: [ { productoId: 5, nombre: "Cien años de soledad", cantidad: 1, precio: 289 },
{ productoId: 6, nombre: "Lámpara de escritorio", cantidad: 1, precio: 459 },
{ productoId: 7, nombre: "Taza térmica", cantidad: 2, precio: 199 } ],
total: 1146 },
{ _id: 5, clienteId: 4, fecha: new Date("2024-06-20"), estado: "cancelado",
items: [ { productoId: 1, nombre: "Audífonos Bluetooth", cantidad: 1, precio: 899 } ],
total: 899 }
]);
print("Productos: " + db.productos.countDocuments()); // 8
print("Clientes: " + db.clientes.countDocuments()); // 5
print("Pedidos: " + db.pedidos.countDocuments()); // 5Fíjate en el diseño: cada pedido embebe sus items con una copia del nombre y precio del producto al momento de la compra (igual que precio_unitario en el curso de SQL), y guarda el total calculado para listar pedidos rápido.
Create: insertar
insertOne
js
db.productos.insertOne({
nombre: "Bocina portátil",
categoria: "Electrónica",
precio: 749,
stock: 12,
etiquetas: ["audio", "inalambrico"],
atributos: { resistenteAgua: true }
})js
{ acknowledged: true, insertedId: ObjectId("66a2b7f1c4e5d6a7b8c9d0e1") }Como no indicamos _id, se generó un ObjectId. (Puedes convivir con _id numéricos y ObjectId en la misma colección, aunque en un proyecto real conviene ser consistente.)
insertMany
js
db.productos.insertMany([
{ _id: 9, nombre: "Balón de fútbol", categoria: "Deportes", precio: 399, stock: 20, etiquetas: ["futbol"] },
{ _id: 10, nombre: "Tapete de yoga", categoria: "Deportes", precio: 349, stock: 18, etiquetas: ["yoga"] }
])js
{ acknowledged: true, insertedIds: { '0': 9, '1': 10 } }ordered: true por defecto
Si un documento de insertMany falla (por ejemplo, _id duplicado), por defecto se detiene ahí: los anteriores quedan insertados y los siguientes no. Con { ordered: false } intenta insertar todos y reporta los errores al final. No es una transacción: para "todo o nada" necesitas transacciones (Índices y modelado).
Read: consultar
find y findOne
js
db.productos.find() // todos
db.productos.find({ categoria: "Libros" }) // filtro por igualdad
db.productos.findOne({ _id: 3 }) // uno (o null)find devuelve un cursor (los resultados se traen por lotes); en mongosh se muestran de 20 en 20.
El filtro es un documento: cada campo es una condición y todas deben cumplirse (es un AND implícito):
js
db.productos.find({ categoria: "Electrónica", stock: 10 })
// [ { _id: 2, nombre: "Teclado mecánico", ... } ]
db.clientes.find({ "direccion.ciudad": "Ciudad de México" })
// Ana López y Jorge DíazProyección: elegir campos
El segundo argumento de find indica qué campos traer (1 incluir, 0 excluir):
js
db.productos.find({ categoria: "Hogar" }, { nombre: 1, precio: 1 })js
[
{ _id: 6, nombre: "Lámpara de escritorio", precio: 459 },
{ _id: 7, nombre: "Taza térmica", precio: 199 }
]js
// _id se incluye siempre, salvo que lo excluyas
db.productos.find({ categoria: "Hogar" }, { _id: 0, nombre: 1, precio: 1 })
// Excluir campos pesados
db.productos.find({}, { atributos: 0, etiquetas: 0 })
// Subcampos
db.clientes.find({}, { nombre: 1, "direccion.ciudad": 1, _id: 0 })No puedes mezclar inclusiones y exclusiones (salvo _id: 0).
sort, limit y skip
js
// Los 3 productos más caros
db.productos.find({}, { nombre: 1, precio: 1, _id: 0 }).sort({ precio: -1 }).limit(3)js
[
{ nombre: "Monitor 24 pulgadas", precio: 3499 },
{ nombre: "Teclado mecánico", precio: 1250 },
{ nombre: "Audífonos Bluetooth", precio: 899 }
]| Método | SQL | Uso |
|---|---|---|
.sort({ campo: 1 }) | ORDER BY campo ASC | -1 = descendente; varios campos = desempate |
.limit(n) | LIMIT n | Máximo de documentos |
.skip(n) | OFFSET n | Saltar documentos (paginación) |
js
// Página 2 de 3 en 3, ordenado por precio
db.productos.find().sort({ precio: 1 }).skip(3).limit(3)Contar
js
db.productos.countDocuments() // 8 (o más si insertaste)
db.productos.countDocuments({ categoria: "Libros" }) // 2
db.productos.estimatedDocumentCount() // rápido, sin filtro (usa metadatos)
db.pedidos.distinct("estado") // valores únicos de un campo
// [ "cancelado", "entregado", "enviado", "pendiente" ]Update: actualizar
js
db.coleccion.updateOne(filtro, actualizacion, opciones)
db.coleccion.updateMany(filtro, actualizacion, opciones)La actualización usa operadores. Sin ellos, estarías reemplazando el documento.
$set y $unset
js
// Cambiar o agregar campos
db.productos.updateOne(
{ _id: 1 },
{ $set: { precio: 849, "atributos.color": "azul", enOferta: true } }
)
// { acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
// Eliminar un campo
db.productos.updateOne({ _id: 1 }, { $unset: { enOferta: "" } })matchedCount = documentos que cumplieron el filtro; modifiedCount = los que realmente cambiaron (si ya tenían ese valor, no cuenta).
$inc y $mul
js
// Se vendieron 2 tazas: restar stock (¡atómico, sin leer antes!)
db.productos.updateOne({ _id: 7 }, { $inc: { stock: -2 } })
// Subir 10 % todos los productos de Electrónica
db.productos.updateMany({ categoria: "Electrónica" }, { $mul: { precio: 1.10 } })Atomicidad en un documento
Toda operación sobre un documento es atómica. $inc resuelve el problema de "dos usuarios compran a la vez" sin leer y escribir por separado. Para no vender sin stock, combina filtro y operador:
js
db.productos.updateOne(
{ _id: 3, stock: { $gte: 2 } }, // solo si hay al menos 2
{ $inc: { stock: -2 } }
)
// matchedCount: 0 → no había stock suficienteOperadores de arrays: $push, $addToSet, $pull, $pop
js
// Agregar una etiqueta (permite duplicados)
db.productos.updateOne({ _id: 6 }, { $push: { etiquetas: "led" } })
// Agregar solo si no existe
db.productos.updateOne({ _id: 6 }, { $addToSet: { etiquetas: "oficina" } }) // ya estaba: no cambia
// Agregar varios
db.productos.updateOne({ _id: 6 }, { $push: { etiquetas: { $each: ["estudio", "lectura"] } } })
// Quitar todas las coincidencias
db.productos.updateOne({ _id: 6 }, { $pull: { etiquetas: "estudio" } })
// Quitar el último (1) o el primero (-1)
db.productos.updateOne({ _id: 6 }, { $pop: { etiquetas: 1 } })Actualizar un elemento dentro de un array de subdocumentos:
js
// $ = el primer elemento que coincidió con el filtro
db.pedidos.updateOne(
{ _id: 4, "items.productoId": 7 },
{ $set: { "items.$.cantidad": 3 } }
)
// $[] = todos los elementos; $[x] con arrayFilters = los que cumplan una condición
db.pedidos.updateOne(
{ _id: 4 },
{ $mul: { "items.$[it].precio": 0.9 } },
{ arrayFilters: [ { "it.precio": { $gt: 300 } } ] }
)Otros operadores útiles: $rename (renombrar campo), $min/$max (actualiza solo si el valor nuevo es menor/mayor), $currentDate (pone la fecha actual).
js
db.productos.updateMany({}, { $currentDate: { actualizadoEn: true } })Upsert: actualizar o insertar
js
// Si existe el producto con ese nombre, actualiza el stock; si no, lo crea
db.productos.updateOne(
{ nombre: "Mochila escolar" },
{ $set: { precio: 650, categoria: "Hogar" }, $inc: { stock: 10 } },
{ upsert: true }
)
// { matchedCount: 0, modifiedCount: 0, upsertedCount: 1, upsertedId: ObjectId("...") }$setOnInsert asigna valores solo cuando se crea el documento:
js
db.visitas.updateOne(
{ pagina: "/productos/1" },
{ $inc: { conteo: 1 }, $setOnInsert: { primeraVisita: new Date() } },
{ upsert: true }
)replaceOne
Sustituye el documento completo (excepto _id):
js
db.productos.replaceOne(
{ _id: 10 },
{ nombre: "Tapete de yoga premium", categoria: "Deportes", precio: 499, stock: 8 }
)
// El campo "etiquetas" desaparece porque no está en el documento nuevoError común: olvidar el operador
js
db.productos.updateOne({ _id: 2 }, { precio: 1199 })
// MongoInvalidArgumentError: Update document requires atomic operatorsLos drivers modernos lo impiden, pero en versiones viejas (o con replaceOne) borrabas todos los demás campos. Para cambiar un campo usa siempre $set.
findOneAndUpdate
Actualiza y devuelve el documento (antes o después del cambio):
js
db.productos.findOneAndUpdate(
{ _id: 2, stock: { $gt: 0 } },
{ $inc: { stock: -1 } },
{ returnDocument: "after", projection: { nombre: 1, stock: 1 } }
)
// { _id: 2, nombre: "Teclado mecánico", stock: 9 }Muy útil para contadores (folios de factura, turnos) y colas de trabajo.
Delete: borrar
js
db.productos.deleteOne({ _id: 10 })
// { acknowledged: true, deletedCount: 1 }
db.pedidos.deleteMany({ estado: "cancelado" })
// { acknowledged: true, deletedCount: 1 }
const borrado = db.productos.findOneAndDelete({ _id: 9 }) // devuelve el documento borradodeleteMany({})
db.productos.deleteMany({}) borra todos los documentos de la colección (el filtro vacío coincide con todo), igual que un updateMany({}, ...) modifica todo. Antes de un deleteMany o updateMany importante, ejecuta un find o countDocuments con el mismo filtro y revisa el resultado.
Borrado lógico
Igual que en SQL, en muchos sistemas se prefiere marcar en lugar de borrar:
js
db.productos.updateOne({ _id: 8 }, { $set: { eliminadoEn: new Date() } })
db.productos.find({ eliminadoEn: { $exists: false } }) // solo activosResumen visual
txt
insertOne / insertMany ──► { documento }
find(filtro, proyección).sort().skip().limit() ──► cursor de documentos
updateOne / updateMany(filtro, { $set | $inc | $push | $pull | ... }, { upsert })
replaceOne(filtro, documentoCompleto)
deleteOne / deleteMany(filtro)
findOneAndUpdate / findOneAndDelete ──► devuelven el documentoEjercicios
Vuelve a ejecutar semilla.js antes de empezar.
- Muestra nombre y precio (sin
_id) de los productos de Electrónica, del más barato al más caro. - Agrega el cliente 6: Pedro Sánchez, pedro@correo.com, Mérida, CP 97000, registrado hoy.
- Agrega la etiqueta
"oferta"a todos los productos de la categoría Libros sin crear duplicados. - El pedido 4 se envió: cambia su estado a
"enviado"y agrega el campoenviadoEncon la fecha actual. - Descuenta 1 del stock del Monitor solo si hay existencias, y devuelve el documento actualizado.
- Asigna la categoría
"Electrónica"a los productos que no tienen categoría.
Ver solución
js
// 1
db.productos.find({ categoria: "Electrónica" }, { _id: 0, nombre: 1, precio: 1 }).sort({ precio: 1 })
// Audífonos 899, Teclado 1250, Monitor 3499
// 2
db.clientes.insertOne({
_id: 6,
nombre: "Pedro Sánchez",
email: "pedro@correo.com",
direccion: { ciudad: "Mérida", cp: "97000" },
registro: new Date()
})
// 3
db.productos.updateMany({ categoria: "Libros" }, { $addToSet: { etiquetas: "oferta" } })
// matchedCount: 2, modifiedCount: 2
// 4
db.pedidos.updateOne({ _id: 4 }, { $set: { estado: "enviado" }, $currentDate: { enviadoEn: true } })
// 5
db.productos.findOneAndUpdate(
{ _id: 3, stock: { $gt: 0 } },
{ $inc: { stock: -1 } },
{ returnDocument: "after" }
)
// stock: 4
// 6
db.productos.updateMany({ categoria: { $exists: false } }, { $set: { categoria: "Electrónica" } })
// matchedCount: 1 (Cable USB-C)Resumen
- Create:
insertOne/insertMany; si no das_id, se genera unObjectId. - Read:
find(filtro, proyección)consort,skip,limit;findOne,countDocuments,distinct. - Update: siempre con operadores (
$set,$unset,$inc,$push,$addToSet,$pull...);upsertcrea si no existe. - Las operaciones sobre un documento son atómicas: usa
$inccon filtro de stock para evitar sobreventas. - Delete:
deleteOne/deleteMany; cuidado con el filtro vacío{}.