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.
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.
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;
}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.
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().
// 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.