(NestJS-5)Understanding TypeORM Entities with Relationships in NestJS: An E-commerce Example
In this article, we will delve into the concept of entities and their relationships in NestJS using TypeORM. We will use an e-commerce application as our example to illustrate these concepts. Understanding how to properly define and manage these relationships is crucial for building robust and scalable applications.
We will cover the basics of entities, different types of relationships, and how to implement them in a NestJS application. This article is mainly focused on beginners and it will be lengthy due to the depth of the content.
In previous article we have been covered who t configure different loggers in NestJS Application. If you are interested to config logger please refer to this.
Table of Contents
- Prerequisites
- What are Entities
- Understanding Relationships
- Problem Statement: E-commerce Relationships
- Generate Modules and Entities
- Defining Entities
- Class Variables and Their Treatment
- Setting Up Entities in TypeOrm Module
- Execution and Output
- Tips and Best Practices
- Conclusion
Prerequisites
Before we start, make sure you have the following:
- NestJS Project Setup: A basic NestJS project. If you don’t have one, you can refer here.
- TypeORM and MySQL: Installed and configured in your project, refer here.
- Basic Knowledge of SQL and Database Relationships: Understanding of basic SQL and how relational databases work.
What are Entities
Entities are the building blocks of your database schema. Simply put, an entity object represents a database table, and we define entities to interact with the database. Entities are written as simple classes, and we can use some OOP programming concepts like abstraction and inheritance.
Advantages of using entities
- Provides a way to abstract database tables into TypeScript classes, making it easier to work with databases using an object-oriented approach.
- Ensures data integrity and consistency through different decorators.
- Allows you to define relationships between different tables (e.g., one-to-many, many-to-many).
- Simplifies database queries and operations.
TypeORM provides an @Entity
decorator to define an entity, and some decorators to define properties of data table columns like @PrimaryColumn
, @Column
, and so on.
Key Features of an Entities
- Entities: Represent database tables as classes.
- Decorators: Used to define mappings (
@Entity()
,@Column()
, etc.). - Relationships: Define relationships using decorators like
@OneToMany
,@ManyToOne
, etc. - OOP Features: Support inheritance and abstraction.
- Repositories: Used to manage entity instances and perform database operations.
Understanding Relationships
In relational databases, entities often relate to each other in various ways. TypeORM supports several types of relationships:
- One-to-One: Indicates that a single entity instance is related to a single instance of another entity.
- One-to-Many / Many-to-One: One-to-Many is where a single entity instance is related to multiple instances of another entity. Conversely, many-to-one is where multiple instances of one entity relate to a single instance of another entity.
- Many-to-Many: Indicates that multiple instances of an entity relate to multiple instances of another entity. This type of relationship requires a join table.
Relationships can be defined in two ways:
- Unidirectional Relationships: Only one entity knows about the relationship.
- Bidirectional Relationships: Both entities know about the relationship
Based on requirement, these relationships are defined.
Problem Statement: E-commerce Relationships
As stated in the title, we will work on an e-commerce example. In an e-commerce application, we typically have entities like User, Profile, Order, Product, and Category. The relationships among these entities might include:
- A User can have one Profile (One-to-One).
- A User can place many Orders, and many Orders can belong to a User (One-to-Many / Many-to-One).
- An Order can include many Products and a Product can be part of many Orders (Many-to-Many).
- A Product belongs to a Category and a Category can have many Products (Many-to-One / One-to-Many).
Generate Modules and Entities
As discussed in the problem statement, we now require the following modules in our application to configure entities and their relationships: User, Order, Product, and Category. If a module already exists, continue with the existing entities. If a module does not exist, create the module using the CRUD generator cli command as shown below, replace the module name with your desirable name.
$ nest g resource <module-name>
Select the REST API as the transport layer and enter Y or YES. It generates CRUD entry points, then click Enter.
Defining Entities
To define an entity in TypeORM, you use the @Entity()
decorator on a class. Each property in the class is decorated with column-specific decorators such as @PrimaryGeneratedColumn()
, @Column()
, @CreateDateColumn()
, and others.
Key Decorators
@Entity()
: Marks the class as a database entity.@PrimaryColumn()
: This decorator is used to mark a property as the primary key . Additional options can be passed to generated IDs automatically like UUID, rowid, soon .@Column()
: This decorator is used to mark a property as a column in the database. Additional options can be passed to customize the column (e.g.,select: false
to exclude the column from SELECT queries by default).@CreateDateColumn()
: This decorator is used to mark a property as a column that will store the creation timestamp. The value is automatically managed by TypeORM.@UpdateDateColumn()
: This decorator is used to mark a property as a column that will store the last update timestamp. Like@CreateDateColumn()
, the value is automatically managed by TypeORM.
User Entity and Profile Entity (One-to-One)
Create a profile.entity.ts
file in the user > entities
.
Note:- Try to use same relationships in all entities for now.
- Unidirectional relationships:
// user.entity.ts
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryColumn({ generated: "uuid" }) // generated specifies if this column will use auto increment (sequence, generated identity, rowid).
user_id: string;
@Column()
name: string;
@Column()
email: string;
/*
* Select indicates row selection in QueryBuilder
* Default value is "true".
*/
@Column({ select: false })
password: string;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
}
// profile.entity.ts
import { Entity, Column, PrimaryColumn, OneToOne, JoinColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Profile {
@PrimaryColumn({ generated: "uuid" })
profile_id: string;
@Column({ nullable: true })
dob: Date;
@Column({ type: 'longtext', nullable: true })
bio: string;
@Column({ nullable: true })
avatar_url: string;
@Column({ nullable: true })
address: string;
@Column()
phone_number: string;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@OneToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
}
In above example, the Profile
entity has a user
property annotated with @OneToOne()
and @JoinColumn()
. This setup indicates that the Profile
entity holds the foreign key reference to the User
entity.
- Bidirectional relationships:
// profile.entity.ts
import { Entity, Column, PrimaryColumn, OneToOne, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Profile {
@PrimaryColumn({ generated: "uuid" })
profile_id: string;
@Column({ nullable: true })
dob: Date;
@Column({ type: 'longtext', nullable: true })
bio: string;
@Column({ nullable: true })
avatar_url: string;
@Column({ nullable: true })
address: string;
@Column()
phone_number: string;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@OneToOne(() => User, (user) => user.profile, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: User;
}
// user.entity.ts
import { Entity, Column, PrimaryColumn, OneToOne, DeleteDateColumn, UpdateDateColumn, CreateDateColumn } from 'typeorm';
import { Profile } from './profile.entity';
@Entity()
export class User {
@PrimaryColumn({ generated: "uuid" })
user_id: string;
@Column()
name: string;
@Column()
email: string;
/*
* Select indicates row selection in QueryBuilder
* Default value is "true".
*/
@Column({ select: false })
password: string;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@OneToOne(() => Profile, profile => profile.user)
profile: Profile;
}
In this bidirectional setup, the User
entity also has a profile
property annotated with @OneToOne()
. This ensures that both entities are aware of the relationship.
User Entity and Order Entity (One-to-Many | Many-to-One)
- Unidirectional relationships:
// user.entity.ts
import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryColumn({ generated: "uuid" })
user_id: string;
@Column()
name: string;
@Column()
email: string;
/*
* Select indicates row selection in QueryBuilder
* Default value is "true".
*/
@Column({ select: false })
password: string;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
}
// order.entity.ts
import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm';
@Entity()
export class Order {
@PrimaryColumn({ generated: "uuid" })
order_id: string;
@Column()
total: number;
@Column()
ordered_on: Date
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
}
- Bidirectional relationships:
// user.entity.ts
import { Entity, Column, PrimaryColumn, DeleteDateColumn, UpdateDateColumn, CreateDateColumn, OneToMany } from 'typeorm';
import { Order } from 'src/order/entities/order.entity';
@Entity()
export class User {
@PrimaryColumn({ generated: "uuid" })
user_id: string;
@Column()
name: string;
@Column()
email: string;
/*
* Select indicates row selection in QueryBuilder
* Default value is "true".
*/
@Column({ select: false })
password: string;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@OneToMany(() => Order, order => order.user)
orders: Order[];
}
// order.entity.ts
import { User } from 'src/user/entities/user.entity';
import { Entity, Column, PrimaryColumn, ManyToOne, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, JoinColumn } from 'typeorm';
@Entity()
export class Order {
@PrimaryColumn({ generated: "uuid" })
order_id: string;
@Column()
total: number;
@Column()
ordered_on: Date
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@ManyToOne(() => User, user => user.orders)
@JoinColumn({ name: 'user_id' })
user: User;
}
@OneToMany
is always an inverse side of the relation, and it can’t exist without @ManyToOne
on the other side of the relation. In many-to-one / one-to-many relations, the owner side is always many-to-one. At owner side @JoinColumn
is must. It means that the class that uses @ManyToOne
will store the ID of the related object.
Order Entity and Product Entity (Many-to-Many)
- Unidirectional relationships:
// product.entity.ts
import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity()
export class Product {
@PrimaryColumn({ generated: "uuid" })
product_id: string;
@Column()
name: string;
@Column({ type: 'longtext' })
description: string
@Column()
price: number;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
}
// order.entity.ts
import { Entity, Column, PrimaryColumn, ManyToMany, JoinTable } from 'typeorm';
@Entity()
export class Order {
@PrimaryColumn({ generated: "uuid" })
order_id: string;
@Column()
total: number;
@Column()
ordered_on: Date
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@ManyToMany(() => Product)
@JoinTable()
products: Product[];
}
In this unidirectional setup, the Order
entity has a products
property annotated with @ManyToMany()
and @JoinTable()
. This setup indicates that the Order
entity holds the foreign key reference to the Product
entity.
After you run the application with the above code, the ORM will create an order_products_product
junction table, which holds product_id as the foreign key to order.
- Bidirectional relationships:
// order.entity.ts
import { Product } from 'src/product/entities/product.entity';
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, JoinColumn, ManyToMany, JoinTable } from 'typeorm';
@Entity()
export class Order {
@PrimaryColumn({ generated: "uuid" })
order_id: string;
@Column()
total: number;
@Column()
ordered_on: Date
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@ManyToMany(() => Product, product => product.orders)
@JoinTable({ name: "order_products" })
products: Product[];
}
// product.entity.ts
import { Order } from "src/order/entities/order.entity";
import { Column, CreateDateColumn, DeleteDateColumn, Entity, ManyToMany, PrimaryColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class Product {
@PrimaryColumn({ generated: "uuid" })
product_id: string;
@Column()
name: string;
@Column({ type: 'longtext' })
description: string
@Column()
price: number;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@ManyToMany(() => Order, order => order.products)
orders: Order[];
}
In this bidirectional setup, the Product
entity also has an orders
property annotated with @ManyToMany()
. This ensures that both entities are aware of the relationship and can reference each other.
@JoinTable
is required to specify that this is the owner side of the relationship. After you run the application with the above code, the ORM will create an order_products
junction table (because we defined the name of the table while using @JoinTable
), which holds both primary key mappings.
Product Entity and Category Entity (One-to-Many | Many-to-One)
- Unidirectional relationships:
// category.entity.ts
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
@Entity()
export class Category {
@PrimaryColumn({ generated: "uuid" })
category_id: string;
@Column()
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
}
// product.entity.ts
import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm';
@Entity()
export class Product {
@PrimaryColumn({ generated: "uuid" })
product_id: string;
@Column()
name: string;
@Column({ type: 'longtext' })
description: string
@Column()
price: number;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@ManyToOne(() => Category)
category: Category;
}
- Bidirectional relationships:
// category.entity.ts
import { Product } from 'src/product/entities/product.entity';
import { Entity, Column, PrimaryColumn, OneToMany, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
@Entity()
export class Category {
@PrimaryColumn({ generated: "uuid" })
category_id: string;
@Column()
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@OneToMany(() => Product, product => product.category)
products: Product[];
}
// product.entity.ts
import { Category } from "src/category/entities/category.entity";
import { Column, CreateDateColumn, DeleteDateColumn, Entity, ManyToOne, PrimaryColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class Product {
@PrimaryColumn({ generated: "uuid" })
product_id: string;
@Column()
name: string;
@Column({ type: 'longtext' })
description: string
@Column()
price: number;
@CreateDateColumn()
created_on: Date
@UpdateDateColumn()
updated_on: Date
@DeleteDateColumn({ nullable: true })
deleted_on: Date;
/* previous relationship if any */
@ManyToOne(() => Category, category => category.products)
@JoinColumn({ name: 'category_id' })
category: Category;
}
I have used bidirectional relationships for all entities. This approach provides more flexibility in select conditions but increases execution time.
We assume that a user can order multiple products in a single quantity. For the above setup, we can’t store the quantity of products ordered by a user. To address this, we need to store additional information, such as the quantity, either in a separate entity or by updating the relationship between Order
and Product
to many-to-one and adding a quantity
column.
Note: I have all entities with bidirectional relationships. Add additional columns to entities as necessary
Class Variables and Their Treatment
In the context of entities:
- Class Variables: These are properties defined within a class. They represent the attributes of the entity.
- Columns: When you use TypeORM decorators like
@Column
, these class variables are treated as columns in the database table. - Data Types: The TypeScript type of each class variable determines the SQL data type of the corresponding column. For example, a TypeScript
string
is mapped to a SQLVARCHAR
orTEXT
type.
Setting Up Entities in TypeOrm Module
Now we need to set the entities to be managed by TypeORM. For that, we need to set entities in the TypeOrmModule
in the imports of the module, as shown below:
// user.module.ts
/* previous imports */
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Profile } from './entities/profile.entity';
@Module({
imports: [TypeOrmModule.forFeature([User, Profile])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule { }
//product.module.ts
/* previous imports */
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from './entities/product.entity';
@Module({
imports:[TypeOrmModule.forFeature([Product])],
controllers: [ProductController],
providers: [ProductService],
})
export class ProductModule {}
//order.module.ts
/* previous imports */
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from './entities/order.entity';
@Module({
imports: [TypeOrmModule.forFeature([Order])],
controllers: [OrderController],
providers: [OrderService],
})
export class OrderModule { }
//category.module.ts
/* previous imports */
import { TypeOrmModule } from '@nestjs/typeorm';
import { Category } from './entities/category.entity';
@Module({
imports: [TypeOrmModule.forFeature([Category])],
controllers: [CategoryController],
providers: [CategoryService],
})
export class CategoryModule { }
Execution and Output
Before execution of application. If you are following NestJS series of my articles, we have to make some changes in previous implemented code i.e in product.service.ts
file.
/* imports */
@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product)
private readonly productRepo: Repository<Product>,
) { }
/* other methods */
async findOne(id: string): Promise<Product> {
return this.productRepo.findOneBy({ product_id: id }) // changed id to product_id
}
async remove(id: string): Promise<string> {
this.productRepo.delete({ product_id: id }); // changed id to product_id
return "Product deleted successfully";
}
}
Make sure that database is present and TypeOrm synchronize to true. Now start the application using below command in terminal.
$ nest start
For the first time it will take some time, because the TypeOrm should sync entities to database. The database may look like given below image:
Note: After execution make the TypeOrm synchronize to false and restart the application, this can avoid conflicts in database.
Tips and Best Practices
- Always use the appropriate relationship decorators (
@OneToOne
,@OneToMany
,@ManyToOne
,@ManyToMany
) to define relationships between entities. - Utilize
@JoinColumn
and@JoinTable
where necessary to specify the owning side of the relationship. - Keep your entity classes clean and focused on data structure and relationships.
- Ensure that your database schema aligns with your entity definitions.
- For code robustness and better readability, consider using a base entity class where all columns common to entities exist, and have other entities extend this base entity.
For more details about TypeORM, please refer to the TypeORM Documentation and TypeORM gitbook documentation.
Conclusion
Understanding and implementing entities and their relationships is crucial for building robust and scalable applications with NestJS. By leveraging TypeORM’s powerful features, you can easily manage complex relationships between your data models. With the examples and best practices provided in this article, you are now equipped to define and work with entities and their relationships in your NestJS applications.
In next article's we will look into CRUD operations.
GitHub Repository:
You can find the complete code for this implementation on GitHub: bhargavachary123
Contribution
If you like my Article, please, consider donating through Buy Me a Coffee: https://buymeacoffee.com/bhargavachary.
If you found this article useful, please consider leaving a clap (👏) and a comment.
Stay tuned for more articles covering advanced topics in NestJS development! Happy coding ⚔️