Hamburger Icon
Explorando el NoSQLverso: Caso Práctico con MongoDB

Explorando el NoSQLverso: Caso Práctico con MongoDB

Las bases de datos NoSQL tienen unas particularidades especiales que las hacen muy ventajosas en situaciones concretas. Puedes ver consultar la Guía de Introducción a NoSQL para repasarlo.

MongoDB es una de las bases de datos NoSQL más usadas. Utiliza un modelo de documentos flexible, está diseñado para escalar horizontalmente y proporciona opciones para la replicación de datos y tolerancia a fallos aportando una alta disponibilidad. No obstante, el precio a pagar es el modelo de consistencia eventual que utiliza, que en ciertas situaciones puede llevar a problemas. También hay que tener en cuenta que en entornos con muchas escrituras intensivas puede experimentar problemas de rendimiento por la necesidad de bloquear documentos durante las operaciones.

Qué Haremos

Vamos a importar en MongoDB una serie de datos en CSV que podemos encontrar en este repositorio. Estos datos consisten en información ficticia de:

  • Clientes
  • Productos
  • Pedidos de venta
  • Líneas de pedido de venta
  • Trabajadores y su jerarquía
  • Tiendas

Una vez tengamos estos datos cargados, vamos a ver cómo consultarlos.

Para todo esto vamos a usar Node con mongoose, que nos facilitará mucho el trabajo con MongoDB.

Preparando Todo Lo Necesario

Primero de todo descarga los archivos CSV del repositorio enlazado en el apartado anterior. Asegúrate de tener instalado Node en tu equipo. Luego, crea un directorio para este proyecto. Dentro, en una carpeta "data" metes los CSV. Entra en la raíz del documento con tu consola de comandos e instala estos paquetes:

npm install csv-parser mongoose

Con esto, ya podemos empezar.

A Jugar Con MongoDB

Crear Los Esquemas

Crea un directorio "schemas" y dentro un archivo .js para cada uno de los esquemas que usaremos:

// customer.js

const mongoose = require("mongoose")

const dataSchema = new mongoose.Schema({
  customerNumber: String,
  customerName: String,
  contactLastName: String,
  contactFirstName: String,
  phone: String,
  addressLine1: String,
  addressLine2: String,
  city: String,
  state: String,
  postalCode: String,
  country: String,
  salesRepEmployeeNumber: Number,
  creditLimit: Number,
})

const Customer = mongoose.model("Customer", dataSchema)

module.exports = Customer
// employee.js

const mongoose = require("mongoose")

const dataSchema = new mongoose.Schema({
  employeeNumber: Number,
  lastName: String,
  firstName: String,
  extension: String,
  email: String,
  officeCode: String,
  reportsTo: Number,
  jobTitle: String,
})

const Employee = mongoose.model("Employee", dataSchema)

module.exports = Employee
// office.js

const mongoose = require("mongoose")

const dataSchema = new mongoose.Schema({
  officeCode: Number,
  city: String,
  phone: String,
  addressLine1: String,
  addressLine2: String,
  state: String,
  country: String,
  postalCode: String,
  territory: String,
})

const Office = mongoose.model("Office", dataSchema)

module.exports = Office
// order.js

const mongoose = require("mongoose")

const dataSchema = new mongoose.Schema({
  orderNumber: Number,
  orderDate: Date,
  requiredDate: Date,
  shippedDate: Date,
  status: String,
  comments: String,
})

const Order = mongoose.model("Order", dataSchema)

module.exports = Order
// orderdetail.js

const mongoose = require("mongoose")

const dataSchema = new mongoose.Schema({
  orderNumber: Number,
  productCode: String,
  quantityOrdered: Number,
  priceEach: Number,
  orderLineNumber: Number,
})

const OrderDetail = mongoose.model("OrderDetail", dataSchema)

module.exports = OrderDetail
// product.js

const mongoose = require("mongoose")

const dataSchema = new mongoose.Schema({
  productCode: String,
  productName: String,
  productLine: String,
  productScale: String,
  productVendor: String,
  productDescription: String,
  quantityInStock: Number,
  buyPrice: Number,
  MSRP: Number,
})

const Product = mongoose.model("Product", dataSchema)

module.exports = Product

Carga De Datos

Una vez definidos los esquemas, vamos a cargar toda la información en la base de datos. Para ello, necesitarás usar un servidor de MongoDB ya sea en tu equipo localmente, en un contenedor o, como he hecho yo, usando la capa grauita de MongoDB Atlas.

Para cargar los datos he usado este script:

const fs = require("fs")
const csv = require("csv-parser")
const mongoose = require("mongoose")

const Customer = require("./schemas/customer")
const Order = require("./schemas/order")
const OrderDetail = require("./schemas/orderdetail")
const Product = require("./schemas/product")
const Office = require("./schemas/office")
const Employee = require("./schemas/employee")

// función para cargar los datos de un archivo CSV
const loadCSVData = async (file, Model) => {
  fs.createReadStream(file)
    .pipe(csv())
    .on("data", async (row) => {
      // conversión de valores null
      Object.keys(row).forEach((key) => {
        if (row[key] === "null") {
          row[key] = null
        }
      })

      // creamos una instancia del modelo
      const newModelInstance = await new Model(row)

      // guardamos los datos en la base de datos
      try {
        await newModelInstance.save()
      } catch (err) {
        console.error("Error saving data:", err)
        console.log(row)
        process.exit(1)
      }
    })
    .on("end", async () => {
      console.log(`CSV file ${file} successfully processed`)
    })
}

// base de datos creada en capa gratuita de MongoDB Atlas
mongoose.connect("TU_URL_DE_CONEXION")

const db = mongoose.connection

db.on("error", console.error.bind(console, "connection error:"))

db.once("open", async () => {
  console.log("Database connected")

  // una vez conectados a la base de datos cargamos los datos
  // creamos esta estructura para llamar a la función loadCSVData en un bucle
  const models = [
    { model: Customer, file: "./data/customer.csv" },
    { model: Order, file: "./data/order.csv" },
    { model: OrderDetail, file: "./data/orderdetail.csv" },
    { model: Product, file: "./data/product.csv" },
    { model: Office, file: "./data/office.csv" },
    { model: Employee, file: "./data/employee.csv" },
  ]

  models.map((model) => {
    loadCSVData(model.file, model.model)
  })
})

Consulta De Datos

Y ahora que ya tenemos los datos cargados en MongoDB, ya podemos empezar a hacer consultas. Empecemos por consultar todos los clientes españoles que hay en la base de datos:

const mongoose = require("mongoose")

const Customer = require("./schemas/customer")
const Order = require("./schemas/order")
const OrderDetail = require("./schemas/orderdetail")
const Product = require("./schemas/product")
const Office = require("./schemas/office")
const Employee = require("./schemas/employee")

// función para recuperar todos los clientes de un país
const getAllCustomersByCountry = async (country) => {
  try {
    const customers = await Customer.find({ country: country })
    return customers
  } catch (error) {
    console.error("Error retrieving customers:", error)
    return null
  }
}

// base de datos creada en capa gratuita de MongoDB Atlas
mongoose.connect("TU_URL_DE_CONEXION")

const db = mongoose.connection

db.on("error", console.error.bind(console, "connection error:"))

db.once("open", async () => {
  console.log("Database connected")

  // obtener clientes de España
  const customers = await getAllCustomersByCountry("Spain")
  console.log(customers)
})

¿Fácil verdad? En esta consulta simplemente usamos la función find() para filtrar los clientes cuya propiedad country sea "Spain". Vamos a ver algo un poco más complejo. En los siguientes ejemplos me limitaré a poner solo la función para obtener los datos, que es la parte interesante.

Vale, ahora vamos a hacer alguna relación entre modelos. ¿Qué pasa si queremos obtener la información de un empleado y su oficina correspondiente? Veamos lo que podemos hacer:

// función para obtener la información de un empleado y su oficina correspondiente
const getEmployeeAndOffice = async (employeeNumber) => {
  try {
    const employee = await Employee.findOne({ employeeNumber: employeeNumber })
    const office = await Office.findOne({ officeCode: employee.officeCode })
    return { employee: employee, office: office }
  } catch (error) {
    console.error("Error retrieving employee and office:", error)
  }
}

En este caso, a partir de un id de empleado obtenemos sus datos y luego buscamos los datos de su oficina, usando la propiedad officeCode del empleado. Esto tendría que pulirse un poco añadiendo lógica de gestión de errores para cuando no se encuentre el empleado o la oficina, pero si ya has trabajado con bases de datos relacionales ya debes de ir pillando que esto es muy similar a nivel conceptual, al menos desde nuestro punto de vista.

Vamos a subir un nivel más. Vamos a hacer alguna consulta que conlleve agregar datos, como sería por ejemplo determinar el número total de productos en stock por categoría:

// Función para determinar el número total de productos en stock por categoría
const getStockByCategory = async () => {
  try {
    return Product.aggregate([
      {
        $group: {
          _id: "$productLine",
          totalStock: { $sum: "$quantityInStock" },
        },
      },
    ])
  } catch (error) {
    console.error("Error retrieving stock by category:", error)
  }
}

La clave aquí está en la función aggregate() del modelo, que nos permite agrupar los productos por su categoría y calcular la suma total de stock por cada una. Para que te hagas una idea, esta es la estructura que devolvería:

;[
  { _id: "Classic Cars", totalStock: 219183 },
  { _id: "Trucks and Buses", totalStock: 35851 },
  { _id: "Planes", totalStock: 62287 },
  { _id: "Trains", totalStock: 16696 },
  { _id: "Motorcycles", totalStock: 69401 },
  { _id: "Vintage Cars", totalStock: 124880 },
  { _id: "Ships", totalStock: 26833 },
]

En una base de datos relacional la consulta SQL sería un poco más compleja, en mi opinión mongoose aquí nos facilita mucho el trabajo a realizar, haciéndolo además de forma simple y clara.

Imagina ahora que quires encontrar el promedio de precios de compra por categoría. Harías algo así:

// Función para encontrar el promedio de precios de compra por línea de producto
const getAveragePriceByProductLine = async () => {
  try {
    return Product.aggregate([
      {
        $group: {
          _id: "$productLine",
          averagePrice: { $avg: "$buyPrice" },
        },
      },
    ])
  } catch (error) {
    console.error("Error retrieving average price by product line:", error)
  }
}

La estructura devuelta sería como esta:

;[
  { _id: "Trucks and Buses", averagePrice: 56.32909090909091 },
  { _id: "Ships", averagePrice: 47.007777777777775 },
  { _id: "Trains", averagePrice: 43.92333333333334 },
  { _id: "Motorcycles", averagePrice: 50.68538461538461 },
  { _id: "Vintage Cars", averagePrice: 46.06625 },
  { _id: "Planes", averagePrice: 49.62916666666666 },
  { _id: "Classic Cars", averagePrice: 64.44631578947369 },
]

Vamos a ver un último ejemplo, esta vez vamos a buscar todos los detalles del pedido para un número de pedido dado:

// Función para buscar todos los detalles del pedido para un número de pedido dado
const getOrderDetailsByOrderNumber = async (orderNumber) => {
  try {
    const order = await Order.findOne({ orderNumber: orderNumber })
    const lines = await OrderDetail.aggregate([
      // Filtramos los detalles del pedido por número de pedido
      {
        $match: {
          orderNumber: orderNumber,
        },
      },
      // Realizamos unión con la colección de productos para obtener el nombre del producto
      {
        $lookup: {
          from: "products", // Nombre de la colección de productos
          localField: "productCode", // Campo en la colección de detalles del pedido
          foreignField: "productCode", // Campo en la colección de productos
          as: "product", // Alias para los resultados de la unión
        },
      },
      // Proyectamos los campos deseados
      {
        $project: {
          _id: 0, // Excluimos el campo _id
          orderNumber: 1, // Incluimos el número de pedido
          productCode: 1, // Incluimos el código del producto
          quntityOrdered: 1, // Incluimos la cantidad pedida
          priceEach: 1, // Incluimos el precio de cada producto
          productName: { $arrayElemAt: ["$product.productName", 0] }, // Obtenemos el nombre del producto desde el resultado de la unión
        },
      },
    ])
    return { order: order, lines: lines }
  } catch (error) {
    console.error("Error retrieving order details:", error)
  }
}

Como puedes ver aquí la cosa ya se complica un poco, es necesario conocer un poco más a fondo sus funcionalidades, pero aún así la forma de hacer la consulta es bastante clara. La estructura devuelta sería algo así:

{
  order: {
    _id: new ObjectId('65d9d3b8f9491b349b71883a'),
    orderNumber: 10100,
    orderDate: 2003-01-06T00:00:00.000Z,
    requiredDate: 2003-01-13T00:00:00.000Z,
    shippedDate: 2003-01-10T00:00:00.000Z,
    status: 'Shipped',
    comments: null,
    __v: 0
  },
  lines: [
    {
      orderNumber: 10100,
      productCode: 'S18_4409',
      priceEach: 75.46,
      productName: '1932 Alfa Romeo 8C2300 Spider Sport'
    },
    {
      orderNumber: 10100,
      productCode: 'S18_2248',
      priceEach: 55.09,
      productName: '1911 Ford Town Car'
    },
    {
      orderNumber: 10100,
      productCode: 'S18_1749',
      priceEach: 136,
      productName: '1917 Grand Touring Sedan'
    },
    {
      orderNumber: 10100,
      productCode: 'S24_3969',
      priceEach: 35.29,
      productName: '1936 Mercedes Benz 500k Roadster'
    }
  ]
}

Conclusión

MongoDB destaca por su flexibilidad y escalabilidad horizontal, siendo una opción popular en el mundo de las bases de datos NoSQL. Su modelo de documentos permite almacenar datos de forma flexible y su capacidad para escalar horizontalmente facilita el manejo de grandes volúmenes de información.

Sin embargo, su modelo de consistencia eventual puede plantear desafíos en entornos donde se requiera alta consistencia en los datos. Además, en escenarios con muchas operaciones de escritura intensivas, MongoDB puede experimentar problemas de rendimiento debido a la necesidad de bloquear documentos durante estas operaciones.

En resumen, MongoDB es una poderosa herramienta que ofrece ventajas significativas, pero su implementación requiere una comprensión profunda de sus características y consideraciones para optimizar su rendimiento y garantizar la integridad de los datos.