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
Introducción
Persistencia de datos en Node.js
Platzi Store: instalación y presentación del proyecto
Base de datos
Instalación de Docker
Configuración de Postgres en Docker
Explorando Postgres: interfaces gráficas vs. terminal
Integración de node-postgres
Manejando un Pool de conexiones
Variables de ambiente en Node.js
Sequelize
¿Qué es un ORM? Instalación y configuración de Sequelize ORM
Tu primer modelo en Sequelize
Crear, actualizar y eliminar
Cambiando la base de datos a MySQL
Migraciones
¿Qué son las migraciones? Migraciones en Sequelize ORM
Configurando y corriendo migraciones con npm scripts
Modificando una entidad
Relaciones
Relaciones uno a uno
Resolviendo las relaciones uno a uno
Relaciones uno a muchos
Resolviendo relaciones uno a muchos
Consultas
Órdenes de compra
Relaciones muchos a muchos
Resolviendo relaciones muchos a muchos
Paginación
Filtrando precios con operadores
Despliegue
Deploy en Heroku
Consideraciones al hacer migraciones
Próximos pasos
Toma con el Curso de Backend con Node.js: Autenticación con Passport.js y JWT
No tienes acceso a esta clase
¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera
No se trata de lo que quieres comprar, sino de quién quieres ser. Invierte en tu educación con el precio especial
Antes: $249
Paga en 4 cuotas sin intereses
Termina en:
Aportes 23
Preguntas 12
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"
Minuto 9:40
Se repite el contenido (:
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;
},
},
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);
}
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.
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
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
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:
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
}
Hecho!
😊 En este enlace/video: https://www.youtube.com/watch?v=VVySn87s8Eo podemos encontrar una breve explicación sobre cómo funciona el método reduce de un array. 😊 Para los que nunca lo habían visto, como yo. 😜
Si estas utilizando TypeScript puedes hacer la columna virtual total asi:
@Column(DataType.VIRTUAL)
get total(): number {
if (this.items.length > 0) {
return this.items.reduce((total, item) => {
return total + item.price * item.OrderProduct.amount;
}, 0);
}
return 0;
}
En mi código hice que la propiedad amount
se actualice si agrego el mismo producto a la misma orden, en vez de resetear el amount
.
async addItem(data: Partial<IOrderProduct>) {
const { orderId, productId, amount } = data;
const productExistsInOrder: any = await OrderProduct.findOne({
where: { order_id: orderId, product_id: productId },
});
if (productExistsInOrder) {
const newAmount = productExistsInOrder.amount + amount;
const updatedItem = await productExistsInOrder.update({
amount: newAmount,
});
if (!updatedItem)
throw boom.internal(
"An error occurred while adding items to the order. Please try again later."
);
return updatedItem;
} else {
const newItem = await OrderProduct.create(data);
if (!newItem)
throw boom.internal(
"An error occurred while adding items to the order. Please try again later."
);
return newItem;
}
}
ç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
// 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/
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.
I loved this class
¿Quieres ver más aportes, preguntas y respuestas de la comunidad?