No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Aprende todo un fin de semana sin pagar una suscripción 🔥

Aprende todo un fin de semana sin pagar una suscripción 🔥

Regístrate

Comienza en:

3D
20H
45M
43S

Resolviendo relaciones muchos a muchos

22/27
Recursos

Aportes 19

Preguntas 11

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

o inicia sesión.

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

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.

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"

Error de edicion:

Minuto 9:40
Se repite el contenido (:

Antes de agregar el producto a la order sería bueno validar que el producto exista, algo así:

async addItem(data) {
    const product = await productService.findOne(data.productId);

    if (!product) {
      throw boom.notFound('product not found');
    }

    return await models.OrderProduct.create(data);
  }

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.

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 😃

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:

En orders.router.js:

router.get('/', async (req, res) => {
  try {
    const orders = await service.find()
    res.json(orders)
  } catch (error) {
    res.status(404).json({
      message: error.message
    })
  }
})

En services/order.service.js:

  async find() {
    const orders = await models.Order.findAll({
      include: [
        {
          association: 'customer',
          include: ['user']
        },
        'items'
      ]
    })
    return orders
  }

Se agrega producto a la orden de compra, para ello de asignan nuevas validaciones en el schema de la orden.

order.schema.js:

const Joi = require('joi');

const id = Joi.number().integer();
const customerId = Joi.number().integer();
const orderId = Joi.number().integer();
const productId = Joi.number().integer();
const amount = Joi.number().integer().min(1);

const getOrderSchema = Joi.object({
  id: id.required(),
});

const createOrderSchema = Joi.object({
  customerId: customerId.required(),
});

const addItemSchema = Joi.object({
  orderId: orderId.required(),
  productId: productId.required(),
  amount: amount.required(),
});

module.exports = {
  getOrderSchema,
  createOrderSchema,
  addItemSchema,
};

También se crea el post en el router de la orden para agregar algo a la orden de compra deseada.

orders.router.js:

router.post(
  '/add-item',
  validatorHandler(addItemSchema, 'body'),
  async (req, res, next) => {
    try {
      const body = req.body;
      const newItem = await service.create(body);
      res.status(201).json(newItem);
    } catch (error) {
      next(error);
    }
  }
);

Posteriormente se crea el método en la clase OrderService.

order.service.js:

async addItem(data) {
    const newItem = await models.OrderProduct.create(data);
    return newItem;
  }

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) {
        return this.items.reduce((total, item) => {
          return total + (item.price * item.OrderProduct.amount);
        }, 0);
      }
      return 0;
    },
  },

Me costo mucho esta clase

Hecho!

Field: VIRTUAl + Reduce -> para calcular valores en tiempo de ejecucion. Y devolverlos en el endpoint.

çampo virtual

Para quienes quieran explorar un poco las relaciones n:m (muchos a muchos):
Sequelize nos permite usar las relaciones que definimos en las Associations para crear instancias de varios modelos relacionados de forma anidada. Ejemplo:
Digamos que tenemos dos modelos, cliente y producto definidos como en la clase, cada uno en su archivo cliente.model.js y producto.model.js

  • Un cliente puede tener muchos productos
  • Un mismo producto puede ser enlistado por muchos clientes
    = relación muchos a muchos
// Esquema del cliente
const ClienteSchema = {
    id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: DataTypes.INTEGER
    },
    nombre: {
        allowNull: false,
        type: DataTypes.STRING
    },
}

// Definimos el modelo
class Cliente extends Model {
    static associate(models) {
        // Asociaciones
        const relacionClienteProducto = this.belongsToMany(models.Cliente, {
            through: models.ClienteProducto,
            foreignKey: 'clienteId',
            otherKey: 'productoId'
        });
        return relacionClienteProducto
    }

    static config(sequelize) {
        return {
            sequelize,
            tableName: CLIENTE_TABLE,
            modelName: 'Cliente',
            timeStamps: false
        }
    }
}
export {
    CLIENTE_TABLE, ClienteSchema, Cliente
}
// la documentación dice que para una relación muchos a muchos se debe hacer la asociación belongsToMany en ambos modelos, en cliente.model y producto.model

Nótese cómo guardamos la asociación en una variable “relacionClienteProducto”, pues esta es la que vamos a usar para crear los clientes y productos de forma anidada, para ello debemos hacer llegar esta variable a nuestros servicios, que siguiendo la estructura de carpetas de la clase, podría ser así:

en el archivo db/models/index.js

// importamos los modelos
export function setupModels(sequelize) {
    // ... inicializamos los modelos como en la clase y luego:
    const relacionClienteProducto = Cliente.associate(sequelize.models)
};
// guardamos el return del associate de este modelo en una nueva variable para exponerla en

// libs/sequelize

const relacionClienteProducto= setupModels(sequelize);
sequelize.sync();
export { sequelize, relacionClienteProducto}

y usarla en los servicios:

async crearCliente(data) {
        const {
            nombre,
            Productos
        } = data;

        const clienteYProductos = await sequelize.models.Cliente.create({
                nombre,
                Productos: Productos
            },
            {
                include: [{
                    association: relacionClienteProducto
                }]
            });
            return clienteYProductos ;
        }
    }

Aquí suponemos que se creó un schema y modelo para Producto similar al de Cliente, y sequelize pluraliza los campos (no se si en español, pero en inglés lo hace), de modo que si el nombre que le dimos al modelo de los productos fue de “Product”, el campo para colocar el req.body sería “Products”.

De esta forma podemos crear un cliente y con una sola operación añadir cuantos productos queramos, dependiendo de cómo definamos sus modelos. Espero no haber sido muy enredado, aquí la doc:
https://sequelize.org/docs/v6/advanced-association-concepts/creating-with-associations/

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.

Para los que quieran guardar el total de los productos en la orden, deberán quitar el “VIRTUAL” por “FLOAT”, la función “get()” no va funcionar y tampoco si creas una función en el “defaultValue”, por lo que se tendría que realizar las acciones para guardarlo en el servicio de orden, este es la manera en la cuál lo hice y me funciono

  async addItem(data) {
    //crear el pedido
    const newItem = await models.OrderProduct.create(data);
    //obtener el pedido mediante data.orderId que es el objeto obtenido desde la petición
    let orderTotal = await this.findOne(data.orderId);
    //buscar el precio del producto que esta en la orden
    const productoOrdenado = await productService.findOne(data.productId);
    //multiplicar el precio por la cantidad
    const precioTotal = data.amount * productoOrdenado.dataValues.price;
    //sumar el valor al total
    orderTotal.dataValues.total += precioTotal;
    //realizar el cambio de total
    await models.Order.update(orderTotal.dataValues, {
      where: { id: data.orderId }
    });
    //al final se va ir sumando mientras más items agreguemos
    return newItem;
  }

“dataValue” viene a ser otro objeto que guarda los parámetros de la data.

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

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: []}
    }
  ]
});

I loved this class