Voltar para Artigos
Banco de Dados5 min de leitura

Domínio Relacional no TypeORM: Eager Loading, Cascades e JoinTables

Como modelar relacionamentos complexos corretamente: OneToMany vs ManyToOne, o perigo do eager: true, como customizar a JoinTable em relacionamentos ManyToMany e a diferença entre cascade: insert e cascade: update.

12 de agosto de 2026

O verdadeiro poder dos bancos de dados SQL está nas relações (Foreign Keys). Mas mapear a teoria relacional para o paradigma de Orientação a Objetos no Node.js tem suas armadilhas. Entender exatamente de qual lado declarar as chaves e quando delegar o carregamento dos relacionamentos salva a aplicação do temido gargalo de N+1 queries.

1. O Lado Dono da Chave (ManyToOne / OneToMany)

Em um relacionamento 'Um para Muitos' (ex: Um Usuário tem Muitos Pedidos), a tabela do 'Muitos' sempre hospeda a chave estrangeira (o Pedido tem um user_id). Por isso, no TypeORM, o lado que usa @ManyToOne obrigatoriamente usa o @JoinColumn. A simetria é importante: Order declara @ManyToOne, User declara @OneToMany apontando de volta para order.user. Sem o lado @OneToMany, o TypeORM não consegue navegar a relação inversa.

Order.ts (O Lado 'Muitos')
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './User';

@Entity('orders')
export class Order {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  userId: string; // Fica visível para queries diretas e inserts rápidos

  // O relacionamento espelho
  @ManyToOne(() => User, user => user.orders)
  @JoinColumn({ name: 'userId' }) // Define qual coluna guarda o ID no banco
  user: User;
}
User.ts (O Lado 'Um')
import { Entity, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Order } from './Order';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  // Aqui NÃO VAI JoinColumn. O TypeORM apenas sabe que a relação
  // reversa se chama 'user' dentro da classe Order.
  @OneToMany(() => Order, order => order.user)
  orders: Array<Order>;
}

Fuja do `eager: true`: Se você colocar @OneToMany(..., { eager: true }) no User, toda vez que fizer um .findOneBy({ id }), o TypeORM secretamente fará um LEFT JOIN para puxar os pedidos. Se você tiver mil pedidos, o banco trará uma carga massiva de dados silenciosamente. Use relações explícitas via relations: ['orders'] no seu Repository.

2. Tabelas Pivô Nativas (ManyToMany)

Em um relacionamento Muitos-para-Muitos (ex: Produto pode estar em muitas Categorias e uma Categoria pode ter muitos Produtos), o banco de dados usa uma tabela pivot (ou join table) para representar as associações. O TypeORM cria e gerencia essa tabela automaticamente — mas você pode customizá-la se precisar de colunas extras na associação (ex: data de adição, prioridade).

Relações Muito para Muitos exigem uma tabela intermediária no SQL. Se um Produto pertence a várias Categorias e vice-versa, o TypeORM abstrai essa tabela pivô com o @JoinTable. Apenas um lado do relacionamento (você escolhe qual) recebe essa anotação.

Product.ts
import { Entity, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { Category } from './Category';

@Entity('products')
export class Product {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToMany(() => Category)
  @JoinTable({
    name: 'products_categories', // Nome da tabela pivô no banco
    joinColumn: {
      name: 'product_id', // Como se chama o ID do produto lá
      referencedColumnName: 'id'
    },
    inverseJoinColumn: {
      name: 'category_id', // Como se chama o ID da categoria lá
      referencedColumnName: 'id'
    }
  })
  categories: Array<Category>;
}

3. O Poder (e Risco) dos Cascades

Se configurarmos { cascade: ['insert'] }, o TypeORM permite criar a árvore inteira de dados em um único .save().

Exemplo de Cascade
// Se o relacionamento categorias estiver com cascade: ['insert']
// Você pode salvar o Produto e criar Categorias novas na mesma transação
const product = productRepo.create({
  name: 'Notebook',
  categories: [
    { name: 'Eletrônicos' }, // Não tem ID, será um INSERT
    { id: 'uuid-existente' } // Tem ID, apenas vinculará na tabela pivô
  ]
});

await productRepo.save(product);

Conclusão

O TypeORM tira todo o peso de escrever queries complexas de junção, mas essa abstração cobra um preço: se você errar a direção do JoinColumn ou abusar do Eager Loading, a performance do sistema sofrerá sem que você veja os comandos SQL rodando de fundo. Mantenha os mapeamentos explícitos e faça queries focadas.