Lección 3 de 311 min
Sequelize y Knex.js
Modelos y asociaciones con Sequelize, y consultas, migraciones y seeds con el query builder Knex.
Si entras en un proyecto de Node con unos años de historia, es muy probable que encuentres una de estas dos herramientas. Sequelize es un ORM clásico de JavaScript: modelos, asociaciones y métodos como save(). Knex.js es un query builder: no tiene modelos, te deja escribir el SQL con funciones y se encarga de las migraciones.
Representan dos filosofías distintas: trabajar con objetos que el ORM traduce a SQL, o seguir pensando en SQL con algo de ayuda. En esta lección verás cómo se usa cada una y, al final, cómo se comparan con TypeORM.
Sequelize: un ORM de JavaScript
Sequelize sigue el patrón Active Record que viste en la primera lección: cada modelo sabe buscarse, guardarse y borrarse. Funciona con PostgreSQL, MySQL, MariaDB, SQLite y SQL Server, entre otras.
Modelos y DataTypes
Definir cada tabla con sus columnas, tipos, validaciones y valores por defecto.
- Patrón:
- Active Record
- Añade solo:
- id, createdAt, updatedAt
Un modelo representa una tabla. Se define de dos formas equivalentes: con una clase que extiende Model y llama a Model.init(), o con sequelize.define(). En ambos casos describes cada columna con un tipo de DataTypes (STRING, INTEGER, BOOLEAN, DATE, JSONB…) y opciones como allowNull, defaultValue o unique.
Sequelize valida en tu aplicación antes de enviar nada a la base de datos: con validate puedes exigir un mínimo, un formato de email o una función propia. Además añade por su cuenta una clave primaria id y las columnas createdAt y updatedAt, que mantiene al día solo.
Úsalo cuando
Para cada tabla de un proyecto con Sequelize. Usa tableName para fijar el nombre exacto de la tabla.
Ten en cuenta
Si no indicas tableName, Sequelize pluraliza el nombre del modelo con reglas del inglés: Autor buscaría la tabla Autors. Y sequelize.sync() crea o altera tablas al arrancar, como synchronize en TypeORM: en producción, usa migraciones (con sequelize-cli).
import { Sequelize, DataTypes, Model } from "sequelize";
export const sequelize = new Sequelize(process.env.DATABASE_URL, { logging: false });
export class Receta extends Model {}
Receta.init(
{
nombre: { type: DataTypes.STRING(100), allowNull: false },
minutos: { type: DataTypes.INTEGER, validate: { min: 1 } },
vegana: { type: DataTypes.BOOLEAN, defaultValue: false },
},
{ sequelize, tableName: "recetas" },
);
// Lo mismo con define:
export const Autor = sequelize.define("Autor", { nombre: DataTypes.STRING }, { tableName: "autores" });Asociaciones e include
hasMany y belongsTo para relacionar modelos; include para cargarlos juntos.
- Métodos:
- hasMany, belongsTo, belongsToMany
Las relaciones se llaman asociaciones y se declaran por los dos lados: Autor.hasMany(Receta) y Receta.belongsTo(Autor). Con eso, la tabla recetas tiene una clave foránea hacia autores y cada instancia gana métodos nuevos, como autor.getRecetas() o autor.createReceta(). Para muchos a muchos existe belongsToMany, con una tabla intermedia.
Para cargar datos relacionados de forma anticipada, usas include en la consulta: Autor.findAll({ include: "recetas" }) trae los autores con sus recetas, normalmente con un JOIN. Es la forma de evitar el problema N+1 que viste en la primera lección.
Úsalo cuando
Siempre que dos modelos estén relacionados por una clave foránea. Da un alias con as a cada asociación: hace el código más legible y es obligatorio si asocias dos veces los mismos modelos.
Ten en cuenta
Llamar a autor.getRecetas() dentro de un bucle es exactamente el problema N+1: usa include. Y un where dentro de include convierte el JOIN en obligatorio (INNER JOIN) y descarta a los autores sin recetas que cumplan; añade required: false si quieres conservarlos.
Autor.hasMany(Receta, { foreignKey: "autorId", as: "recetas" });
Receta.belongsTo(Autor, { foreignKey: "autorId", as: "autor" });
// Carga anticipada: autores con sus recetas veganas
const autores = await Autor.findAll({
include: {
model: Receta,
as: "recetas",
where: { vegana: true },
required: false, // conserva a los autores sin recetas veganas
},
});
// Métodos que añade la asociación
const ana = await Autor.findByPk(1);
await ana.createReceta({ nombre: "Hummus", minutos: 10 });Consultas y transacciones
findAll con operadores, y transacciones para que varios cambios ocurran juntos o ninguno.
- Operadores:
- Op.lt, Op.in, Op.or…
Las consultas se hacen con métodos estáticos del modelo: findAll, findOne, findByPk, create, update, destroy. Las condiciones van en where, y para lo que no es una igualdad usas los operadores de Op: [Op.lt] (menor que), [Op.in], [Op.like], [Op.or]…
Una transacción agrupa varias operaciones para que se apliquen todas o ninguna, como al restar stock y crear un pedido. Con sequelize.transaction(async (t) => { … }), Sequelize hace commit si la función termina bien y rollback (deshace todo) si lanza un error.
Úsalo cuando
Transacciones siempre que una operación de negocio toque varias filas o tablas que deben quedar coherentes: pagos, pedidos, transferencias, altas con datos en varias tablas.
Ten en cuenta
Cada consulta dentro de la transacción debe recibir { transaction: t }. Si lo olvidas en una, esa consulta se ejecuta fuera de la transacción y no se deshará si algo falla después.
import { Op } from "sequelize";
const rapidas = await Receta.findAll({
where: { minutos: { [Op.lt]: 30 }, vegana: true },
order: [["nombre", "ASC"]],
limit: 10,
});
// Todo o nada: si algo lanza un error, se deshace todo
await sequelize.transaction(async (t) => {
const pedido = await Pedido.create({ total: 42 }, { transaction: t });
await Stock.decrement("unidades", {
by: 1,
where: { productoId: 7 },
transaction: t,
});
await Envio.create({ pedidoId: pedido.id }, { transaction: t });
});Knex.js: SQL con JavaScript
Knex no intenta ocultar el SQL: lo construye contigo. A cambio de no tener modelos ni relaciones, sabes en todo momento qué consulta se ejecuta, y obtienes migraciones y seeds para mantener el esquema.
El query builder de Knex
Consultas encadenadas que generan SQL con parámetros para varias bases de datos.
- Tipo:
- Query builder
- Devuelve:
- Objetos planos
Creas una instancia con el cliente (pg, mysql2, better-sqlite3…) y la conexión. Después, db("recetas") empieza una consulta sobre esa tabla, y encadenas .select(), .where(), .join(), .orderBy(), .insert(), .update() o .del(). La consulta se ejecuta cuando haces await.
Lo que devuelve son objetos planos, sin métodos: una fila es solo datos. Knex también tiene transacciones (db.transaction) y, para lo que no sabe expresar, db.raw() con parámetros.
Úsalo cuando
Cuando quieres controlar el SQL, con parámetros automáticos y la posibilidad de cambiar de base de datos (por ejemplo, SQLite en los tests y PostgreSQL en producción).
Ten en cuenta
Sin modelos no hay validación ni relaciones: tú escribes los JOIN y validas los datos antes de insertarlos. Y Knex no tipa los resultados por sí solo; si usas TypeScript, Kysely ofrece una experiencia parecida con tipos más precisos.
import knex from "knex";
export const db = knex({ client: "pg", connection: process.env.DATABASE_URL });
const recetas = await db("recetas")
.join("autores", "autores.id", "recetas.autor_id")
.select("recetas.nombre", "autores.nombre as autor")
.where("recetas.minutos", "<", 30)
.orderBy("recetas.nombre");
// [{ nombre: "Gazpacho", autor: "Ana" }, …] objetos planos
const [nueva] = await db("recetas")
.insert({ nombre: "Hummus", minutos: 10, autor_id: 1 })
.returning("*"); // PostgreSQL devuelve la fila creada
await db.transaction(async (trx) => {
await trx("stock").where({ producto_id: 7 }).decrement("unidades", 1);
await trx("pedidos").insert({ producto_id: 7, total: 42 });
});Migraciones y seeds
Cambios de esquema versionados y datos de ejemplo, con la CLI de Knex.
- Configuración:
- knexfile.js
- Datos de ejemplo:
- Seeds
Knex trae su propio sistema de migraciones: knex migrate:make crea un archivo con fecha y hora en el nombre, con una función up que aplica el cambio y otra down que lo deshace, escritas con knex.schema. migrate:latest aplica las pendientes y migrate:rollback deshace el último lote (el grupo de migraciones que se aplicó junto).
Los seeds («semillas») son scripts que rellenan la base de datos con datos de ejemplo: usuarios de prueba, un catálogo inicial. Se crean con knex seed:make y se ejecutan con knex seed:run. La configuración de cada entorno (desarrollo, pruebas, producción) vive en knexfile.js.
Úsalo cuando
Migraciones en todo proyecto con Knex que vaya a producción; seeds para que cualquier persona del equipo tenga una base de datos local con datos útiles en un comando.
Ten en cuenta
Los seeds suelen empezar borrando la tabla: no los ejecutes nunca contra producción. Y escribe siempre el down, aunque creas que no lo usarás: es tu salida de emergencia si un despliegue sale mal.
// npx knex migrate:make crear_recetas → crea este archivo
// npx knex migrate:latest → aplica las pendientes
// npx knex migrate:rollback → deshace el último lote
// npx knex seed:run → carga los datos de ejemplo
export async function up(knex) {
await knex.schema.createTable("recetas", (tabla) => {
tabla.increments("id");
tabla.string("nombre", 100).notNullable();
tabla.integer("minutos").unsigned();
tabla.integer("autor_id").references("id").inTable("autores").onDelete("CASCADE");
tabla.timestamps(true, true);
});
}
export async function down(knex) {
await knex.schema.dropTable("recetas");
}¿Sabías que…?
Knex es tan útil para las migraciones que algunos proyectos lo usan solo para eso, aunque consulten con otra herramienta. Del mismo modo, muchos ORM permiten bajar a SQL crudo cuando hace falta: las herramientas se combinan más de lo que parece.
Interactivo
TypeORM, Sequelize y Knex
Activa o desactiva opciones para compararlas cara a cara.
| Característica | TypeORM | Sequelize | Knex |
|---|---|---|---|
| Tipo | ORM | ORM | Query builder |
| Cómo defines el esquema | Clases con decoradores | Model.init o define con DataTypes | Migraciones con knex.schema; no hay modelos |
| Patrón | Data Mapper (y Active Record) | Active Record | Ninguno |
| Qué devuelve | Instancias de tus entidades | Instancias del modelo, con métodos | Objetos planos |
| Migraciones | Generadas desde las entidades | Con sequelize-cli, escritas a mano | Integradas: migrate y seeds |
| Encaja si… | Usas TypeScript o NestJS y quieres entidades como clases | Tienes un proyecto JavaScript y quieres un ORM veterano | Quieres SQL explícito, sin la capa de un ORM |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Al confirmar un pedido hay que crear el pedido, restar el stock y registrar el envío. Si falla cualquiera de los tres pasos, no debe quedar ninguno a medias.
Para recordar
- Sequelize es un ORM Active Record: modelos con
Model.initodefine, tipos deDataTypesy validación antes de guardar. - Las asociaciones (
hasMany,belongsTo) añaden métodos a los modelos;includelas carga de forma anticipada y evita el N+1. - En una transacción, todas las operaciones van juntas o ninguna; en Sequelize, cada una debe recibir
{ transaction: t }. - Knex es un query builder: SQL explícito con parámetros, resultados como objetos planos y sin modelos.
- Las migraciones y los seeds de Knex mantienen el esquema y los datos de ejemplo iguales para todo el equipo.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Declaras
Autor.hasMany(Receta)yReceta.belongsTo(Autor). ¿Dónde está la clave foránea?2.Dentro de
sequelize.transaction(async (t) => …)haces unStock.decrement(...)sin pasar{ transaction: t }, y después otra operación falla. ¿Qué ocurre con el stock?3.¿Qué obtienes al ejecutar una consulta de Knex como
await db("recetas").where({ id: 1 })?4.Aplicas tres migraciones nuevas con
knex migrate:latesty una da problemas. ¿Qué deshaceknex migrate:rollback?5.Defines
sequelize.define("Autor", …)sintableNamey la consulta falla porque la tabla no existe. ¿Por qué?