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.