Mastering NestJS: Unlocking the Power of Relationships with TypeORM and SQL Databases
Introduction
Welcome to "Mastering NestJS: Unlocking the Power of Relationships with TypeORM and SQL Databases." In this blog post, we'll look at how NestJS, in conjunction with TypeORM and SQL databases, can help you build complicated data structures and handle interactions between them. By the end of this tutorial, you'll be able to construct APIs that handle complex data connections with ease, taking your NestJS knowledge to the next level.
Whether you're a seasoned NestJS developer hoping to expand your knowledge or a beginner eager to master the ins and outs of data relationships, this comprehensive investigation of data relationships will provide you with the experience needed to construct cutting-edge apps. So, let's get started and learn how to design strong, interconnected systems with NestJS, TypeORM, and SQL databases.
I created a GitHub repo for this series accessible at the following address
What are relations
Relations are connections formed between two or more tables. Relations are formed using common fields from each table, which frequently include primary and foreign keys.
There are three kinds of relationships:
- One-to-one: Every row in the primary table has exactly one row in the foreign table. To define this type of
relationship, use the
@OneToOne()
decorator. - One-to-many/Many-to-one: Every row in the primary table is connected to one or more rows in the foreign table. To
define this type of relationship, we use the
@OneToMany()
and@ManyToOne()
decorators. - Many-to-many: Every row in the primary table has many related rows in the foreign table, and every record in the
foreign table has many related rows in the primary table. Use the
@ManyToMany()
decorator to define this type of relation.
We will now go over each of these terms in detail
One-To-Many
One-to-one is a relation where A contains only one instance of B, and B contains only one instance of A. Let's take for example User and Profile entities. User can have only a single profile, and a single profile is owned by only a single user.
Since we were going to implement authentication and authorization in the next post, we should start creating related resources. So let's create profiles and users Rest API resources.
nest g resource profiles
nest g resource users
See the following output for creating profiles resource
Let's explore user.entity.ts
import { UserRole } from 'src/enums/user.role';
import { Profile } from 'src/profiles/entities/profile.entity';
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.CUSTOMER })
role: string;
}
The UserRole enum is available in the source code. You should take a look at it if you want to see it is implemented
Now let's explore the profile.entity.ts
content
import { User } from 'src/users/entities/user.entity';
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('profiles')
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true })
full_address?: string;
@Column({ nullable: true })
photo?: string;
@OneToOne(() => User, (user) => user.profile) // specify inverse side as a second parameter
@JoinColumn()
user: User;
}
Above, we used the @OneToOne()
decorator. Its argument is a function that returns the class of the entity with which
we wish to establish a relationship.
The @JoinColumn()
decorator specifies that the relationship is owned by the Profile entity. It signifies that the rows
of the Profile table contain the userId column, which can store a user's id. We only use it on one side of the
relationship.
Bidirectional relation
Our relationship is currently unidirectional. Meaning that just one side of the relationship knows anything about the other. With TypeORM, relations can be uni-directional and bidirectional. Uni-directional is relations with a relation decorator only on one side. Bidirectional is relations with decorators on both sides of a relation.
We just created a uni-directional relation. Let's make it bidirectional:
import { UserRole } from 'src/enums/user.role';
import { Profile } from 'src/profiles/entities/profile.entity';
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.CUSTOMER })
role: string;
@OneToOne(() => Profile, (profile) => profile.user)
profile: Profile;
}
It's worth noting that the inverse relationship is a somewhat abstract idea that does not add any new columns to the database.
We just made our relations bidirectional. Note, inverse relation does not have a @JoinColumn
. `@JoinColumn must only
be on one side of the relation - on the table that will own the foreign key
How to save and retrieve one-to-one relation
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { Profile } from 'src/profiles/entities/profile.entity';
import { UpdateProfileDto } from 'src/profiles/dto/update-profile.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
@InjectRepository(Profile) private profileRepository: Repository<Profile>,
) {
}
async create(createUSerDto: CreateUserDto) {
const user = await this.userRepository.save(createUSerDto); // saving the user
// creating the the profile object
const profile = new Profile();
profile.full_address = createUSerDto.full_address;
profile.photo = createUSerDto.photo;
profile.user = user;
await this.profileRepository.save(profile); // linking the profile to user
return this.findOne(user.id); // return the user with the profile
}
async findOne(id: number) {
const user = await this.userRepository.findOne({
where: { id },
relations: { profile: true }, // by doing this, we're implementing the eager loading to automatically load the profile object
});
if (!user) throw new HttpException('user not found', HttpStatus.NOT_FOUND);
return user;
}
}
Many-To-One / One-To-Many Relations
Many-to-one / one-to-many is a relation where A contains multiple instances of B, but B contains only one instance of A. Let's take for example User and Order entities. A user can have multiple orders, but each order is owned by only one single user.
Let's quickly set up orders REST API resource.
nest g resource orders
import { Order } from 'src/orders/entities/order.entity';
@Entity('users')
export class User {
// we add the following to user entity
@OneToMany(() => Order, (order) => order.user)
orders: Order[];
}
import { OrderStatus } from 'src/enums/order.status';
import { User } from 'src/users/entities/user.entity';
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'enum', enum: OrderStatus, default: OrderStatus.PLACED })
status: string; // the order status
@Column()
amount: number;
@ManyToOne(() => User, (user) => user.orders)
user: User;
@CreateDateColumn()
created_datetime?: Date;
}
We added @OneToMany
to orders property and set Order as the target relation type. In a @ManyToOne
/ @OneToMany
relation, you can omit @JoinColumn
. @ManyToOne
cannot exist without @OneToMany
. @ManyToOne
is required if you
want to use @OneToMany
. However, if you only care about the @ManyToOne
relationship, you can define it without
having @OneToMany
on the associated entity. Wherever @ManyToOne
is configured, its linked entity will have a
"relation id" and a foreign key.
How to save and retrieve one-to-many/many-to-one relation
Let me show the full order service that implements an order crud
import { Injectable } from '@nestjs/common';
import { CreateOrderDto } from './dto/create-order.dto';
import { UpdateOrderDto } from './dto/update-order.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Order } from './entities/order.entity';
import { Repository } from 'typeorm';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class OrdersService {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
private readonly userService: UsersService,
) {
}
async create(userId: number, createOrderDto: CreateOrderDto) {
const user = await this.userService.findOne(userId);
const order = new Order();
order.amount = createOrderDto.amount;
order.user = user;
return await this.orderRepository.save(order);
}
async findAll(userId: number): Promise<Order[]> {
const user = await this.userService.findOne(userId);
return await this.orderRepository.find({
where: { user },
});
}
async findOne(id: number): Promise<Order | null> {
return await this.orderRepository.findOneBy({ id });
}
}
Many-To-Many relations
Many-to-many is a relationship in which entity A contains multiple instances of entity B, and vice versa. For example, let's consider the entities 'Product' and 'Order.' An order can include multiple products, and each product can belong to multiple orders.
Let's explore the profile.entity.ts
content
import { Product } from 'src/products/entities/product.entity';
import { ManyToMany, JoinTable } from 'typeorm';
@Entity('orders')
export class Order {
// ..... other properties
// our new property
@ManyToMany(() => Product)
products: Product[];
}
@JoinTable()
is required for@ManyToMany
relations. You must put@JoinTable
on one (owning) side of relation.
Saving many-to-many relations
Let's update the order service file and add some new lines to create method
export class OrdersService {
// ....previous stuffs
async create(userId: number, createOrderDto: CreateOrderDto) {
// ..... previous stuffs
const products = [];
for (const product of createOrderDto.products) {
try {
const dbProduct = await this.productService.findOne(product);
products.push(dbProduct);
} catch (error) {
// We'll update this later
console.log('failed to find product with id ' + product);
}
}
order.products = products;
// .... previous stuffs
}
}
Loading Many-to-Many relations
To load orders with products inside you, must specify the relation in FindOptions:
export class OrdersService {
// ....previous stuffs
async findOne(id: number): Promise<Order | null> {
return await this.orderRepository.findOne({
relations: {
products: true,
},
where: { id },
});
}
}
Summary
We delve deeply into the world of data relationships with NestJS, TypeORM, and SQL databases in this extensive blog post. This lesson will help you regardless of whether you're an experienced NestJS developer trying to broaden your expertise or a novice keen to learn the nuances of data connections. Stay tuned for the upcoming post, where we'll delve into data validation and error handling, equipping you with even more tools to become a NestJS expert. Join us on this exciting adventure, and let's master the art of data relationships in NestJS together!