Relations
NestJS CRUD Automator supports automatic loading and management of entity relations. Configure relation loading strategies to control how and when related entities are fetched.
Defining Relations
Define relations in entities using TypeORM decorators and @ApiPropertyDescribe
:
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from "typeorm";
import {
ApiPropertyDescribe,
EApiPropertyDescribeType
} from "@elsikora/nestjs-crud-automator";
import { UserEntity } from "../user/user.entity";
import { CommentEntity } from "../comment/comment.entity";
@Entity("posts")
export class PostEntity {
@PrimaryGeneratedColumn("uuid")
@ApiPropertyDescribe({
type: EApiPropertyDescribeType.UUID,
description: "Post ID",
})
id: string;
@Column()
title: string;
@ManyToOne(() => UserEntity, user => user.posts)
@ApiPropertyDescribe({
type: EApiPropertyDescribeType.RELATION,
description: "Post author",
relationMetadata: {
relationType: "many-to-one",
entity: UserEntity,
isEager: false,
},
})
author: UserEntity;
@Column()
authorId: string;
@OneToMany(() => CommentEntity, comment => comment.post)
@ApiPropertyDescribe({
type: EApiPropertyDescribeType.RELATION,
description: "Post comments",
isArray: true,
relationMetadata: {
relationType: "one-to-many",
entity: CommentEntity,
isEager: false,
},
})
comments: CommentEntity[];
}
Loading Relations
Automatic Loading
Configure automatic relation loading in controller routes:
import {
ApiController,
EApiRouteType,
EApiControllerLoadRelationsStrategy
} from "@elsikora/nestjs-crud-automator";
@Controller("posts")
@ApiController<PostEntity>({
entity: PostEntity,
routes: {
[EApiRouteType.GET]: {
request: {
relations: {
shouldLoadRelations: true,
relationsLoadStrategy: EApiControllerLoadRelationsStrategy.AUTO,
servicesLoadStrategy: EApiControllerLoadRelationsStrategy.AUTO,
relationsToLoad: ["author", "comments"],
},
},
response: {
relations: ["author", "comments"],
},
},
[EApiRouteType.GET_LIST]: {
request: {
relations: {
shouldLoadRelations: true,
relationsLoadStrategy: EApiControllerLoadRelationsStrategy.AUTO,
relationsToLoad: ["author"],
},
},
response: {
relations: ["author"],
},
},
},
})
export class PostController {
constructor(
public service: PostService,
public authorService: UserService,
public commentsService: CommentService,
) {}
}
Manual Loading
Specify exactly which relations to load:
routes: {
[EApiRouteType.GET]: {
request: {
relations: {
shouldLoadRelations: true,
relationsLoadStrategy: EApiControllerLoadRelationsStrategy.MANUAL,
relationsToLoad: ["author"],
},
},
},
}
Nested Relations
Load nested relations (relations of relations):
routes: {
[EApiRouteType.GET]: {
request: {
relations: {
shouldLoadRelations: true,
relationsToLoad: ["author", "author.profile", "comments", "comments.author"],
},
},
response: {
relations: ["author", "author.profile", "comments", "comments.author"],
},
},
}
Relation Loading Strategies
AUTO
Strategy
Automatically detects and loads configured relations:
relationsLoadStrategy: EApiControllerLoadRelationsStrategy.AUTO
MANUAL
Strategy
Only loads explicitly specified relations:
relationsLoadStrategy: EApiControllerLoadRelationsStrategy.MANUAL
Service Injection for Relations
Inject related services to enable relation loading:
@Controller("posts")
@ApiController<PostEntity>({
entity: PostEntity,
routes: {
[EApiRouteType.GET]: {
request: {
relations: {
shouldLoadRelations: true,
relationsToLoad: ["author", "comments"],
},
},
},
},
})
export class PostController {
constructor(
public service: PostService,
// Inject related services
public authorService: UserService,
public commentsService: CommentService,
) {}
}
The library uses these injected services to load relations efficiently.
Eager vs Lazy Loading
Eager Loading
Relations marked as eager are always loaded:
@ManyToOne(() => UserEntity, { eager: true })
@ApiPropertyDescribe({
type: EApiPropertyDescribeType.RELATION,
description: "Post author",
relationMetadata: {
relationType: "many-to-one",
entity: UserEntity,
isEager: true,
},
})
author: UserEntity;
Lazy Loading
Load relations only when needed:
@ManyToOne(() => UserEntity, { eager: false })
@ApiPropertyDescribe({
type: EApiPropertyDescribeType.RELATION,
description: "Post author",
relationMetadata: {
relationType: "many-to-one",
entity: UserEntity,
isEager: false,
},
})
author: UserEntity;
Filtering by Relations
Filter entities based on relation properties:
GET /posts?author.username[operator]=eq&author.username[value]=john_doe
// Programmatic filtering by relation
const posts = await postService.getList(
{
where: {
author: {
username: {
operator: EFilterOperation.EQ,
value: "john_doe",
},
},
},
limit: 10,
page: 1,
},
{ author: true }
);
Circular Relations
Handle circular relations carefully to avoid infinite loops:
@Entity("users")
export class UserEntity {
@OneToMany(() => PostEntity, post => post.author)
@ApiPropertyDescribe({
type: EApiPropertyDescribeType.RELATION,
description: "User posts",
isArray: true,
relationMetadata: {
relationType: "one-to-many",
entity: PostEntity,
isEager: false,
},
dto: {
response: {
isExposed: false, // Don't auto-load in user responses
},
},
})
posts: PostEntity[];
}
@Entity("posts")
export class PostEntity {
@ManyToOne(() => UserEntity, user => user.posts)
@ApiPropertyDescribe({
type: EApiPropertyDescribeType.RELATION,
description: "Post author",
relationMetadata: {
relationType: "many-to-one",
entity: UserEntity,
isEager: false,
},
})
author: UserEntity;
}
Performance Optimization
N+1 Query Problem
Avoid N+1 queries by loading relations efficiently:
// Bad: Causes N+1 queries
const posts = await postService.getList({ limit: 100 }, {});
for (const post of posts.items) {
// Each iteration causes a separate query
const author = await post.author;
}
// Good: Load relations upfront
const posts = await postService.getList(
{ limit: 100 },
{ author: true } // Load all authors in one query
);
Selective Loading
Only load relations when needed:
routes: {
[EApiRouteType.GET_LIST]: {
// Don't load relations for list view
request: {
relations: {
shouldLoadRelations: false,
},
},
},
[EApiRouteType.GET]: {
// Load relations for detail view
request: {
relations: {
shouldLoadRelations: true,
relationsToLoad: ["author", "comments"],
},
},
},
}
Query Builder for Complex Queries
Use TypeORM query builder for complex relation queries:
async getPostsWithDetails(): Promise<PostEntity[]> {
return this.repository
.createQueryBuilder("post")
.leftJoinAndSelect("post.author", "author")
.leftJoinAndSelect("post.comments", "comments")
.leftJoinAndSelect("comments.author", "commentAuthor")
.where("post.isPublished = :published", { published: true })
.orderBy("post.createdAt", "DESC")
.getMany();
}
Many-to-Many Relations
Handle many-to-many relations:
@ManyToMany(() => TagEntity, tag => tag.posts)
@JoinTable()
@ApiPropertyDescribe({
type: EApiPropertyDescribeType.RELATION,
description: "Post tags",
isArray: true,
relationMetadata: {
relationType: "many-to-many",
entity: TagEntity,
isEager: false,
},
})
tags: TagEntity[];
Creating with Relations
Create entities with relations:
// Create post with existing author
await postService.create({
body: {
title: "New Post",
content: "Post content",
authorId: "existing-user-uuid",
},
});
// Create nested relations
await userService.create({
body: {
username: "john_doe",
email: "john@example.com",
posts: [
{
title: "First Post",
content: "Content",
},
],
},
});
Next Steps
- Filtering and Sorting - Filter by relation properties
- Pagination - Paginate with relations
- Core Concepts - Entities - Entity relation configuration