¿Cómo gestionar la relación muchos a muchos en una orden de compra?
Gestionar relaciones muchos a muchos en las bases de datos puede volverse complejo, pero es esencial para aplicaciones comerciales como los sistemas de gestión de órdenes de compra. Uniendo productos y órdenes, se requiere de un esquema que facilite esta interacción, llevando registro del producto, cantidad e identificación de la orden respectiva. Este blog te guiará paso a paso para implementar esta relación usando una técnica sencilla.
¿Qué debe incluir el esquema para agregar productos a una orden?
Para el adecuado funcionamiento de nuestro sistema de órdenes de compra, es esencial estructurar un esquema que gestione la asociación entre productos y la orden. Este esquema debe incluir:
Identificación de la Orden (order ID): Necesario para vincular el producto a la orden correcta.
Identificación del Producto (product ID): Para especificar qué producto se está incluyendo.
Cantidad (amount): La cantidad de unidades del producto en la orden; debe ser un número positivo.
Este esquema será la base para un sistema robusto que maneja las relaciones de muchos a muchos dentro de la base de datos.
¿Cómo agregar un producto a la orden de compra?
Añadir productos a la orden de compra sigue un flujo bien definido, que comienza con la creación de una función encargada de gestionar estos ítems. Tu aplicación debe permitir enviar esta información a través del cuerpo del request hacia un endpoint dedicado, como podría ser /orders/add-item.
Aquí un código de ejemplo en Node.js con este propósito:
functionaddItemToOrder(data){const newItem ={orderId: data.orderId,productId: data.productId,quantity: data.amount};// Persist the data to the databasereturncreateNewOrderProduct(newItem);}
Este bloque asegura que cada vez que se soliciate agregar un producto, se crea un nuevo registro en la tabla intermediaria correspondiente.
¿Cómo calcular el total de una orden de compra?
El cálculo del total de una orden de compra puede parecer complejo inicialmente; sin embargo, siguiendo ciertos pasos y con la ayuda de funciones como reduce, se vuelve un proceso automático y efectivo.
¿Qué son los campos virtuales y cómo te ayudan?
SQLite proporciona la posibilidad de generar campos virtuales, es decir, campos que no existen en la tabla física, pero se calculan sobre la marcha. Esto se logra configurando una propiedad virtual con un getter personalizado. En este caso, nos ayudará a calcular el total de la orden:
functioncalculateOrderTotal(){returnthis.items.reduce((total, item)=>{const itemTotal = item['order-product'].price* item['order-product'].amount;return total + itemTotal;},0);}
La función reduce suma multiplicando el precio por la cantidad para cada ítem, ofreciendo así un total preciso para toda la orden.
Recomendaciones para el uso de cálculos en el servidor
Volumen de datos: Es importante considerar el volumen de ítems involucrados en una orden al optar por este tipo de cálculos en el servidor. Generalmente, esta técnica es adecuada para órdenes pequeñas o medianas (menos de 50 ítems).
Escalabilidad: En el caso de que los datos sean muy grandes, es preferible optar por consultas SQL que aprovechan mejor el motor de base de datos para estos cálculos, garantizando un rendimiento superior y una carga menor en el servidor Node.js.
¿Por qué es esencial manejar adecuadamente las relaciones?
El manejo efectivo de relaciones dentro de la base de datos no sólo permite un flujo de información correcto sino también una experiencia de usuario más robusta y eficiente. Al seguir estos pasos, no sólo lograrás mantener una estructura clara y detallada en tus tablas, sino también optimizar recursos en la concatenación de datos relevantes para mostrar en tus aplicaciones. Explotando estas funcionalidades, generas un eco que hace crecer el potencial de tu aplicación y la estructura subyacente, potenciando tus habilidades en la gestión de datos a gran escala. Inspirado por estas prácticas, sigue aprendiendo y expandiendo el horizonte de tus aplicaciones.
Ha sido genial este curso hasta aqui, bastante largo, pero uff me siento capaz de crear un CRUD con entero con bdd relaciones gracias a este curso 😄
Tape datos porque sin querer puse los reales en algunos campos JAJAJAJA
En las relaciones anidadas, en los métodos findByPk puedes usar la opción include, para seleccionar los campos que quieres, así te evitas que la consulta SQL retorno algún valor sensible
con confianza conpañero, que estamos entre amigos, jaja es broma ten cuidado que en un entorno real puede ser motivo de despido xd jaja
Para una aplicación más profesional o "seria" yo recomendaría realizar el calculo de la orden de compra y ese calculo guardarlo en la base de datos, así para cuando el cliente requiera una aclaración, una factura o un estado de cuenta se tenga toda la información almacenada. Incluso para una auditoria es mejor tener toda la historia de los datos dentro de la base de datos. Si se calcula un dato al vuelo como en esta clase es probable que en algún punto de la historia este dato se pierda y puede ocasionar un dolor de cabeza tremendo cuando se requiera cualquier tipo de aclaración.
Excelente la aclaración pero por otro lado esta técnica de los campos virtuales podría llegar a ser útil
Ah sí, claro que son útiles. Lo que yo comento o recomiendo es que hay que revisar y analizar en que campos se puede hacer y en que campos no es tan recomendable.
Profe, una corrección en la url del método agregar items a un orden rompe el principio "Sustantivos sí, verbos no" del siguiente post. Creo que es importante mantener la coherencia en el contenido que se publica.
La ruta en mi opinión debería ser algo como:
Method:"post"Action:"api/v1/orders/:id/products"
Y el atributo productId ya no tendría que ir en el body de la petición.
¿Ya no tendría que ir en el body de la petición el productId o el orderId? :think
Error de edicion:
Minuto 9:40
Se repite el contenido (:
pasa muy seguido en este curso jajaja
Se agrega producto a la orden de compra, para ello de asignan nuevas validaciones en el schema de la orden.
La respuesta de la API arroja el precio del producto y la cantidad de items, se puede calcular el total de esa orden de compra. Para ello hay una propiedad de sequelize en donde se puede generar un total y datos calculados.
El atributo no va a existir como campo en la tabla y se debe especificar que el campo es de tipo virtual (DataTypes.VIRTUAL), esto es recomendable únicamente para campos pequeños, pero cuando son campos grandes no es recomendable, lo mejor sería hacer una query porque será más rápida ya que va a calcular directamente desde la base de datos.
Con el método get() se va a especificar cómo se calcula ese campo. Algo que se debe tener en cuenta es que en this.items.length, el nombre de items debe ser el mismo con el que se haya nombrado la asociación (as: 'items')
total:{type:DataTypes.VIRTUAL,get(){if(this.items.length>0){returnthis.items.reduce((total, item)=>{return total +(item.price* item.OrderProduct.amount);},0);}return0;},},
Gracias Platzinauta, tus apuntes son increibles!... solo una pequeña corrección. En order.router.js hay que cambiar el servicio de create a addItem.
router.post('/add-item',validatorHandler(addItemSchema,'body'),async(req, res, next)=>{try{const body = req.body;const newItem =await service.addItem(body); res.status(201).json(newItem);}catch(error){next(error);}});
Antes de agregar el producto a la order sería bueno validar que el producto exista, algo así:
asyncaddItem(data){const product =await productService.findOne(data.productId);if(!product){throw boom.notFound('product not found');}returnawait models.OrderProduct.create(data);}
Hola, también se puede hacer capturando el error, y así te evitas hacer dos consultas a la base de datos.
En el middleware de errorHandler puse esto:
El error 23503 se produce cuando por ejemplo quiero agregar un id de un producto que no existe.
De producirse este error te devuelve esto:
{"statusCode":400,"message":"Key (product_id)=(12) is not present in table \"products\".","error":"SequelizeForeignKeyConstraintError"}
Desde mi experiencia considero que se están haciendo alguno usos erróneos de las migraciones. Por ejemplo, el campo virtual no funciona si se corren las migraciones desde 0. Se debe especificar en el Schema pero no en las migraciones. Por ejemplo en RoR las migraciones se crean para modificar una entidad del sistema, normalmente no se agregan varios create a una sola migración por cuestiones de prácticas.
Es enserio lo del campo virtual, por si al alguien le pasa ya sabe por qué es :)
si vas a mover tu proyecto o volver a crear la base de datos, te recomiendo remover el total, ya que al hacer migrate sequelize te dice que el tipo VIRTUAL no existe. y despues de creado todo se lo vuelve a poner al modelo.
Además se cae la app cuando creas una orden. para "solucionarlo" coloque un try-catch
<code>total:{type:DataTypes.VIRTUAL,get(){try{if(this.items.length>0){returnthis.items.reduce((total, item)=>{return total +(item.Order.precio* item.OrderProduct.cantidad);},0);}}catch(err){}}}
Hola a todos, espero tengan un excelente día.
Tengo un problema y es que en el total me devuelve 0 por algun motivo pero no se por que, ya intente de todo :(
total:{type:DataTypes.VIRTUAL,get(){if(this.items.lenght>0){returnthis.items.reduce((total, item)=>{return total +(item.price* item.OrderProduct.amount)},0)}return0;}}
Olvidenlo, mi dislexia para escribir lenght en vez de length me trolleo 2 horas :)
jajaja me pasó lo mismo y gracias tu pregunta lo pude corregir, gracias jajaa
Ojito con crear la tabla con el campo total de tipo virtual, eh! que te bota todo. Lo agregan después de crear la tabla no más.
Me costo mucho esta clase
Animo si esta complicada pero tampoco es ciencia de cohetes tal vez si haces un ejemplo usando un tema que te guste como peliculas o musica desde 0 lo entiendes mejor
Field: VIRTUAl + Reduce -> para calcular valores en tiempo de ejecucion. Y devolverlos en el endpoint.
Esta configuración en Sequelize establece una relación "muchos a muchos" entre dos modelos: Order y Product. Aquí hay una explicación de los parámetros:
this.belongsToMany(models.Product): Indica que el modelo actual (Order) tiene una relación "muchos a muchos" con el modelo especificado (Product).
as: 'items': Define el nombre de la asociación en el modelo actual. En este caso, la relación se denomina "items".
through: models.OrderProduct: Especifica el modelo intermedio que se utiliza para gestionar la relación muchos a muchos. En este caso, parece ser OrderProduct.
foreignKey: 'orderId': Indica la clave externa en la tabla intermedia (OrderProduct) que se refiere al modelo actual (Order). En este caso, parece ser la clave externa que conecta la orden con el producto.
otherKey: 'productId': Especifica la clave externa en la tabla intermedia (OrderProduct) que se refiere al modelo relacionado (Product). En este caso, parece ser la clave externa que conecta el producto con la orden.
Si tu como yo, llegaste hasta acá utilizando de motor de bd a mysql y no hiciste el cambio a postgres y te marca este error al correr las migraciones : Cannot add foreign key constraint, cambia de inmediato a postgres, por alguna razón todo funciona de maravilla allá, así que es algo del motor en especifico
será que mysql no soporta esta funcionalidad de los campos virtuales ?
Para el caso que necesiten obtener la relación sin traer datos de la tabla de unión como en mi caso necesitaba saber todos los cursos que tenía un perfil asociado, pero sin traer el objeto que profileCourse por defecto pueden utilizar:
through:{attributes:[]}
const data =await models.Profile.findByPk(id,{include:[{model: models.User,as:'user',attributes:['id','email']},{model: models.Course,as:'courses',through:{attributes:[]}}]});
Hola, les comparto un código adicional que hice para tener un endpoint que me devuelva todas las órdenes para corroborar los datos de prueba:
No fue hasta que vi tu aporte que entendí por qué el GET de '/orders' me tiraba error
Buen aporte, gracias!
Hecho!
Tengo una duda a la mejor un poco tonta, pero tengo entendido que estas relaciones de muchos a mucho del ejemplo que vemos en el curso es la lógica de como funcionaria un carrito de compras??, es decir el modelo orders hace referencia al carrito y la relación con productos son los productos que tiene el carrito el cual después el cliente puede comprarlos??
Esta order hace referencia a los productos dentro del carrito asociado con el costumer no como tal al carrito
Hola a todos, ¿cómo puede funcionar la migración de order-product si no existe un OrderProductSchema? Mis nombres de tablas son diferentes al curso, pero me salta un error:
ERROR: Cannot read properties of undefined (reading 'schema')
Alguien sabe? Gracias!
¿cual sería la manera indicada de resolver los totales? ¿en el backend o en el frontend? ¿o son validas las dos?
En mi opinión usaría ambas formas, en el frontend simplemente como feedback para el usuario. Y en el backend sí sería importante hacer el cálculo y validar que todo esté correcto.
También si es un cálculo que necesita mucho tiempo de ejecución lo delegaría al backend y evitaría que fuese en el frontend para proveerle la mejor experiencia al usuario.