En esta página
Relaciones entre entidades y migraciones
Tipos de relaciones en TypeORM
Las bases de datos relacionales se basan en la capacidad de conectar tablas entre sí. TypeORM ofrece cuatro tipos de relaciones que mapean directamente a los conceptos de SQL:
- OneToOne: Un registro de A se relaciona con exactamente un registro de B
- OneToMany / ManyToOne: Un registro de A puede relacionarse con muchos de B, pero cada B pertenece a un solo A
- ManyToMany: Muchos registros de A pueden relacionarse con muchos de B
OneToOne — Relación uno a uno
La relación más simple. Típicamente usada para dividir una tabla muy grande o para datos opcionales:
// perfil/entities/perfil.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Usuario } from '../../usuarios/entities/usuario.entity';
@Entity('perfiles')
export class Perfil {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true })
bio: string | null;
@Column({ nullable: true })
avatarUrl: string | null;
@OneToOne(() => Usuario, (usuario) => usuario.perfil, {
onDelete: 'CASCADE', // al borrar el usuario, se borra el perfil
})
@JoinColumn({ name: 'usuario_id' }) // la FK vive en perfiles.usuario_id
usuario: Usuario;
}
// En usuario.entity.ts
@OneToOne(() => Perfil, (perfil) => perfil.usuario, { eager: true })
perfil: Perfil | null;OneToMany y ManyToOne — La relación más común
Esta es la relación más frecuente en bases de datos relacionales. La clave foránea siempre vive en el lado "muchos" (ManyToOne):
// categoria.entity.ts — el lado "uno"
@Entity('categorias')
export class Categoria {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
nombre: string;
@OneToMany(() => Producto, (producto) => producto.categoria)
productos: Producto[];
}
// producto.entity.ts — el lado "muchos"
@Entity('productos')
export class Producto {
@PrimaryGeneratedColumn('uuid')
id: string;
// La FK real en la base de datos
@ManyToOne(() => Categoria, (categoria) => categoria.productos, {
nullable: false,
eager: false, // cargar manualmente cuando se necesite
onDelete: 'RESTRICT', // no borrar categoría si tiene productos
})
@JoinColumn({ name: 'categoria_id' })
categoria: Categoria;
// Columna de FK separada para queries sin JOIN
@Column({ name: 'categoria_id' })
categoriaId: number;
}ManyToMany — Tablas de unión
La relación ManyToMany requiere una tabla intermedia (junction table). TypeORM puede crearla automáticamente con @JoinTable:
// producto.entity.ts
@ManyToMany(() => Etiqueta, (etiqueta) => etiqueta.productos)
@JoinTable({
name: 'producto_etiquetas',
joinColumn: { name: 'producto_id' },
inverseJoinColumn: { name: 'etiqueta_id' },
})
etiquetas: Etiqueta[];Para tablas de unión con columnas adicionales (por ejemplo, fecha de asignación o cantidad), necesitas crear una entidad intermedia explícita:
@Entity('pedido_productos')
export class PedidoProducto {
@PrimaryColumn()
pedidoId: string;
@PrimaryColumn()
productoId: string;
@Column({ type: 'int' })
cantidad: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
precioUnitario: number;
@ManyToOne(() => Pedido, (p) => p.items, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'pedido_id' })
pedido: Pedido;
@ManyToOne(() => Producto)
@JoinColumn({ name: 'producto_id' })
producto: Producto;
}Opciones de cascade
Las cascadas controlan qué operaciones se propagan automáticamente a las entidades relacionadas:
@OneToMany(() => ItemPedido, (item) => item.pedido, {
cascade: true, // todas las operaciones (insert, update, remove)
cascade: ['insert'], // solo insert
cascade: ['insert', 'update'], // insert y update
})
items: ItemPedido[];Carga eager vs lazy
Eager loading (eager: true): La relación se carga automáticamente en cada consulta. Conveniente pero puede ser costoso.
Lazy loading (por defecto): La relación NO se carga a menos que se especifique en la consulta.
Para cargar relaciones manualmente, usa la opción relations en el find:
const pedido = await this.pedidoRepository.findOne({
where: { id },
relations: {
usuario: true,
items: {
producto: true, // relaciones anidadas
},
},
});Migraciones — La forma segura de evolucionar el esquema
Las migraciones son la forma correcta de gestionar cambios en el esquema de base de datos en producción. En lugar de usar synchronize: true, generas archivos de migración con los cambios SQL exactos.
Configurar el data source para el CLI
// data-source.ts (en la raíz del proyecto)
import { DataSource } from 'typeorm';
import { config } from 'dotenv';
config();
export default new DataSource({
type: 'postgres',
host: process.env['DB_HOST'] ?? 'localhost',
port: parseInt(process.env['DB_PORT'] ?? '5432', 10),
username: process.env['DB_USER'],
password: process.env['DB_PASS'],
database: process.env['DB_NAME'],
entities: ['src/**/*.entity.ts'],
migrations: ['src/migrations/*.ts'],
});Scripts de package.json
{
"scripts": {
"migration:generate": "typeorm-ts-node-commonjs migration:generate -d data-source.ts",
"migration:run": "typeorm-ts-node-commonjs migration:run -d data-source.ts",
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d data-source.ts",
"migration:create": "typeorm-ts-node-commonjs migration:create"
}
}Flujo de trabajo con migraciones
# 1. Modificas tu entidad (añades una columna, cambias un tipo, etc.)
# 2. Generas la migración automáticamente comparando entidades con la BD
npm run migration:generate src/migrations/AgregarColumnaCategoria
# 3. Revisas el archivo generado y verificas que el SQL es correcto
# 4. Ejecutas la migración
npm run migration:run
# 5. Si algo sale mal, reviertes la última migración
npm run migration:revertEjecutar migraciones al iniciar la aplicación
Para entornos de staging y producción, puedes ejecutar las migraciones automáticamente al arrancar:
// database/database.module.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class DatabaseMigrationsService implements OnModuleInit {
constructor(
@InjectDataSource()
private readonly dataSource: DataSource,
) {}
async onModuleInit(): Promise<void> {
if (process.env['RUN_MIGRATIONS'] === 'true') {
await this.dataSource.runMigrations();
console.log('Migraciones ejecutadas correctamente');
}
}
}Las relaciones y las migraciones son las herramientas que convierten TypeORM en una solución de persistencia completa y robusta. En la siguiente lección profundizaremos en el QueryBuilder para escribir consultas complejas y paginación eficiente.
Inicia sesión para guardar tu progreso