Relations & Eager Loading

One-to-Many, Many-to-Many, Eager or Lazy

Relations & Eager Loading

Map relationships between entities and decide when to load them — without walking into an N+1 trap.

4 min read Level 3/5 #nestjs#typeorm#relations
What you'll learn
  • Define @OneToMany / @ManyToOne / @ManyToMany
  • Use the relations option in find
  • Understand N+1 and how to avoid it

Real schemas have relationships: a user has many posts, a post belongs to a user, a post has many tags. TypeORM expresses these with relation decorators that mirror the SQL.

One-to-Many / Many-to-One

The foreign key lives on the many side. That’s the side with @ManyToOne.

@Entity()
export class User {
  @PrimaryGeneratedColumn() id: number;

  @OneToMany(() => Post, (p) => p.user)
  posts: Post[];
}

@Entity()
export class Post {
  @PrimaryGeneratedColumn() id: number;
  @Column() title: string;

  @ManyToOne(() => User, (u) => u.posts)
  user: User;
}

The arrow function (() => Post) avoids circular import problems at class declaration time. The second argument tells TypeORM the inverse side.

Many-to-Many

When both sides can have multiple of the other, use @ManyToMany. One side owns the join table — that’s the side with @JoinTable().

@Entity()
export class Post {
  @ManyToMany(() => Tag, (t) => t.posts)
  @JoinTable()
  tags: Tag[];
}

@Entity()
export class Tag {
  @ManyToMany(() => Post, (p) => p.tags)
  posts: Post[];
}

Loading Relations

By default, relations are not loaded. You opt in per query:

await this.repo.find({
  relations: { posts: true },
});

// Nested relations work too
await this.repo.find({
  relations: { posts: { tags: true } },
});

You can also set eager: true on the relation decorator to always load it. Convenient for small, always-needed children — risky for large ones.

The N+1 Problem

The classic trap. You load 100 users, then loop and access user.posts — TypeORM fires 100 separate queries. The fix is to ask for the relation in one shot:

// Bad: N+1 queries
const users = await this.repo.find();
for (const u of users) console.log(u.posts.length); // each access = a query

// Good: one query
const users = await this.repo.find({ relations: { posts: true } });

For really wide trees, leftJoinAndSelect in a QueryBuilder gives you more control over column selection and conditions.

Migrations & Seeding →