Database Integration with TypeORM
Connecting to a database (e.g., PostgreSQL, MySQL) using TypeORM. Defining entities, repositories, and performing CRUD operations.
Advanced TypeORM Features in NestJS
Introduction
This document explores more advanced features of TypeORM within the NestJS framework. It assumes you have a basic understanding of NestJS and TypeORM, including entities, repositories, and basic CRUD operations. We'll delve into topics such as migrations, transactions, eager/lazy loading, and custom queries.
Migrations
Migrations provide a way to evolve your database schema over time. They allow you to make changes to your database structure in a controlled and versioned manner. TypeORM provides a CLI to generate and run migrations.
First, ensure TypeORM CLI is installed globally or as a dev dependency: npm install -g typeorm
or npm install --save-dev typeorm
To generate a migration, use the following command:
typeorm migration:generate -n CreateUsersTable -d src
Replace CreateUsersTable
with a descriptive name for your migration, and src
with the path to your data source config. The migration file will contain up
(for applying the migration) and down
(for reverting the migration) methods.
Example up
method:
export class CreateUsersTable1678886400000 {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment',
},
{
name: 'username',
type: 'varchar',
},
{
name: 'email',
type: 'varchar',
isUnique: true,
},
],
}),
true,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users');
}
}
To run migrations: typeorm migration:run -d src
To revert the last migration:
typeorm migration:revert -d src
Remember to configure your TypeORM data source to point to the migrations directory.
Transactions
Transactions allow you to group multiple database operations into a single atomic unit. If any operation within the transaction fails, all changes are rolled back, ensuring data consistency.
Example using the EntityManager
:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { InjectConnection } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectConnection()
private readonly connection: Connection,
) {}
async createUserWithProfile(userData: any, profileData: any): Promise<User> {
return this.connection.transaction(async transactionalEntityManager => {
const user = this.userRepository.create(userData);
const savedUser = await transactionalEntityManager.save(user);
// Simulate an error to demonstrate rollback. Remove this in production code.
if(profileData.shouldFail){
throw new Error("Simulated Error")
}
const profile = { ...profileData, userId: savedUser.id }; // Assuming a one-to-one relationship
await transactionalEntityManager.save('UserProfile', profile); // Assuming you have a UserProfile entity
return savedUser;
});
}
}
In this example, the createUserWithProfile
function uses connection.transaction
to create a transaction. All database operations (saving the user and profile) are executed within this transaction. If any operation throws an error, the transaction is rolled back, and no changes are persisted.
Eager and Lazy Loading
Eager and lazy loading determine when related entities are loaded.
- Eager Loading: Related entities are loaded automatically when the parent entity is fetched. Use
{ eager: true }
in the relation definition. Can lead to performance issues if not used carefully. - Lazy Loading: Related entities are only loaded when explicitly accessed. Requires enabling lazy loading: See TypeORM documentation for how to enable lazy loading. Generally the preferred approach for most use cases.
Example:
// User.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Post } from './post.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
// Eager loading (use with caution)
@OneToMany(() => Post, (post) => post.user, { eager: false }) // Change to true to test eager loading
posts: Post[];
}
With eager: false
(Lazy loading, the recommended default), when you fetch a User, the Posts are *not* automatically loaded. You'd have to explicitly fetch them via a relation query or using a join in your query builder. With eager: true
, the Posts *would* be loaded automatically.
Custom Queries
While TypeORM provides a powerful query builder, you may sometimes need to write custom SQL queries for performance reasons or to leverage database-specific features.
Example using query
method on the EntityManager
:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { InjectConnection } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectConnection()
private readonly connection: Connection,
) {}
async findUsersWithCustomQuery(): Promise<User[]> {
return this.connection.query(`SELECT * FROM users WHERE username LIKE '%test%'`);
}
}
This example executes a raw SQL query to find users whose username contains "test". Be extremely careful with raw SQL to avoid SQL injection vulnerabilities. Always sanitize user input before including it in a query.