En esta página

Relaciones entre entidades y migraciones

14 min lectura TextoCap. 3 — Datos y persistencia

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:revert

Ejecutar 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.

Cuidado con eager: true en relaciones
La carga eager carga automáticamente la relación en cada consulta, sin necesidad de especificarlo. Esto es cómodo pero puede ser un problema de rendimiento si la relación contiene muchos registros o si se anida en varias capas. Úsala solo en relaciones pequeñas y de uso frecuente, como roles de usuario.
Guarda el ID de la relación como columna separada
Además de la relación `@ManyToOne`, declara la columna de clave foránea explícitamente con `@Column({ name: 'usuario_id' }) usuarioId: string`. Esto te permite filtrar por ID sin necesidad de hacer un JOIN, lo que mejora el rendimiento en consultas simples.