Nestjs Phần 3: Database Integration, TypeORM

7 min read

Tích hợp cơ sở dữ liệu trong Nestjs Typeorm cơ bản

Database Integration

Một phần quan trọng của một dịch vụ/API backend là Hệ thống Quản lý Cơ sở dữ liệu của nó.

Note: Bài viết này là về Nestjs, không phải về Cơ sở dữ liệu (SQL/NoSQL) nên tôi giả định rằng, người đọc sẽ có kiến thức cơ bản về Hệ thống Quản lý Cơ sở dữ liệu.

Vì Nestjs chỉ là một sự trừu tượng hóa trên các API/Gói máy chủ Nodejs điển hình, nó hỗ trợ tất cả các loại cơ sở dữ liệu phổ biến và hầu hết các ORM của chúng. Nó hỗ trợ các trình điều khiển và ORM cơ sở dữ liệu sau đây:

  • TypeORM (SQL/NoSQL)
  • MikroORM (SQL/NoSQL)
  • Knex
  • Prisma
  • Mongoose (NoSQL)

Tôi sử dụng TypeORM ở đây vì theo quan điểm cá nhân của tôi, nó phù hợp nhất với mô hình API của Nestjs do mẫu trang trí của nó. Tôi cũng sử dụng PostgreSQL làm Cơ sở dữ liệu. Bạn có thể sử dụng các cơ sở dữ liệu và ORM khác nếu bạn muốn. Mẫu cấu hình của tất cả chúng đều phần lớn giống nhau. Ngoài ra, luôn có một gói chính thức/được cộng đồng phát triển và tài liệu cho lựa chọn của bạn. Chỉ cần tìm kiếm trên Google.

Để bắt đầu, đầu tiên cài đặt:

# for npm users*
$ npm i @nestjs/typeorm typeorm psql
# for yarn user
$ yarn add @nestjs/typeorm typeorm psql

Bây giờ tạo các tệp sau trong thư mục gốc của dự án:

.env (để lưu trữ thông tin đăng nhập và bí mật của cơ sở dữ liệu)

config.ts (để nhập các biến môi trường)

ormconfig.ts (cấu hình kết nối cơ sở dữ liệu)

#### .env #####
POSTGRES_PASSWORD=simplepassword
POSTGRES_DB=hello
NODE_ENV=development
DATABASE_USERNAME=postgres # you can put your username of your OS
DATABASE_HOST=localhost # use `postgres` if using PostgreSQL Docker Container
DATABASE_PORT=5432
PORT=4000

Bây giờ nhập các biến môi trường này và xuất lại cho dự án

///// config.ts //////
export const NODE_ENV = process.env.NODE_ENV;
// all the env vars
export const DATABASE_HOST = process.env.DATABASE_HOST;
export const DATABASE_PORT = process.env.DATABASE_PORT
    ? parseInt(process.env.DATABASE_PORT)
    : undefined;
export const DATABASE_NAME = process.env.POSTGRES_DB;
export const DATABASE_PASSWORD = process.env.POSTGRES_PASSWORD;
export const DATABASE_USERNAME = process.env.DATABASE_USERNAME;
export const PORT = process.env.PORT ?? 4000;

Tạo kết nối Cơ sở dữ liệu:

///// ormconfig.ts /////
import {
    DATABASE_HOST,
    DATABASE_NAME,
    DATABASE_PASSWORD,
    DATABASE_PORT,
    DATABASE_USERNAME,
    NODE_ENV,
} from "./config";
import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions";

// db configuration for the orm
const ormconfig: PostgresConnectionOptions = {
    type: "postgres", // name of db you'll be using
    username: DATABASE_USERNAME,
    database: DATABASE_NAME,
    host: DATABASE_HOST,
    port: DATABASE_PORT,
    password: DATABASE_PASSWORD,
    uuidExtension: "uuid-ossp", // for using `uuid` as the type for Primary-Column `id` column
    synchronize: NODE_ENV !== "production",
};

export = ormconfig;

Bây giờ tạo một module có tên là database, nơi tất cả các cấu hình/tệp liên quan đến cơ sở dữ liệu sẽ được lưu trữ. Lệnh sau sẽ tạo nó:

npx nest g module database

Trong database.module.ts, đăng ký kết nối cơ sở dữ liệu của cấu hình bằng cách sử dụng TypeORM:

///// database.module.ts //////

import { Module } from '@nestjs/common';
import ormconfig from "../../ormconfig";
import { TypeOrmModule } from "@nestjs/typeorm";

@Module({
  imports: [
       // registers Database config
        TypeOrmModule.forRoot({
            ...ormconfig, //db config
            entities: [], // put the constructor of all classes that are an Entity
        }),
    ],
})
export class DatabaseModule {}

Hy vọng sau khi khởi động lại ứng dụng của bạn, API/dịch vụ của bạn sẽ kết nối với Cơ sở dữ liệu.

Mẹo!: Bạn có thể truyền một Mảng các cấu hình ORM trong hàm TypeOrmModule.forRoot() để kết nối với nhiều cơ sở dữ liệu. Điều này cho phép sử dụng bất kỳ loại cơ sở dữ liệu nào cùng nhau.

TypeORM

Hướng dẫn này không phải về TypeORM nhưng tôi sẽ đưa ra một giải thích nhỏ về nó để bắt đầu. Bạn có thể tìm thông tin chi tiết hơn về nó trong tài liệu của họ.

API của TypeORM cho SQL/NoSQL có sự khác biệt. Ở đây, tôi sẽ chỉ hiển thị phần SQL. Nếu bạn đang sử dụng NoSQL DB như MongoDB với TypeORM thì bạn có thể tìm hiểu thêm ở đây.

Nếu bạn đã đọc phần 2, bạn có thể biết, ở đó tôi đã sử dụng một thuộc tính lớp như một cơ sở dữ liệu tạm thời trong bộ nhớ. Bây giờ chúng ta sẽ phản ánh phần đó để sử dụng cơ sở dữ liệu PostgreSQL mới với TypeOrm.

Đầu tiên, tạo src/database/entities/hello-record.entity.ts, sau đó tạo một lược đồ TypeORM:

///// hello-record.entity.ts /////

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('hello-record')
export class HelloRecord {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column('varchar', { length: 16 })
  from!: string;

  @Column('text')
  msg!: string;
}

Bạn có thể khai báo một lớp là một thực thể TypeORM bằng cách sử dụng bộ trang trí @Entity(). Bạn có thể đặt tên cho thực thể (điều bạn nên làm) hoặc để typeorm tạo một tên từ displayName của lớp.

Để tạo một cột chính, sử dụng @PrimaryGeneratedColumn. Bạn có thể sử dụng rowId, tăng dần hoặc uuid làm kiểu cột chính của bạn. Hãy nhớ bật các phần mở rộng UUID của cơ sở dữ liệu PostgreSQL hoặc bất kỳ cơ sở dữ liệu SQL nào để sử dụng uuid.

Để tạo một cột, sử dụng bộ trang trí @Column. Bạn có thể chỉ định loại của cột hoặc bất kỳ điều gì khác của cột đó. Tôi đã sử dụng varchar có độ dài 16 ký tự cho cột “from” vì nó sẽ là địa chỉ IP của người dùng đã đăng một thông báo hello. Ngoài ra, tôi đã sử dụng loại text cho cột “msg” vì chúng ta không muốn hạn chế ai đó chỉ có thể nhập tối đa 240 ký tự giống như một số trang mạng xã hội. Điều đó là không nhân văn🤐

Bây giờ để cho TypeORM biết HelloRecord tồn tại, chúng ta phải đặt nó trong mảng entities của hàm Typeorm.forRoot() trong database.module.ts. Bạn phải đặt tất cả các thực thể mà bạn sẽ sử dụng trong ứng dụng trong mảng đó. BTW, nếu bạn đang sử dụng nhiều kết nối cơ sở dữ liệu, hãy đặt các thực thể, được tạo ra đặc biệt cho cơ sở dữ liệu cụ thể, trong mảng thực thể của đối tượng cấu hình cụ thể của cơ sở dữ liệu cụ thể. Cùng một thực thể sẽ không hoạt động cho nhiều cơ sở dữ liệu

///// database.module.ts //////

// .... (other imported stuffs)
import { HelloRecord } from './entities/hello-record.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...ormconfig,
      // put all the entities related to the database in here
      entities: [
        HelloRecord,
      ],
    }),
  ],
})
export class DatabaseModule {}

Bây giờ chúng ta đã tạo ra thực thể, hãy sử dụng nó trong HelloService của chúng ta. Nhưng chúng ta phải nhập nó vào HelloModule để cho Nest biết rằng nó thuộc về HelloModule.

////// hello.module.ts //////

// .... (other imported stuff)
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    forwardRef(() => HiModule),
    // labelling the entity as `HelloModule`'s Repository
    TypeOrmModule.forFeature([HelloRecord]),
  ],
  providers: [HelloService, ByeService],
  controllers: [HelloController],
  exports: [HelloService],
})
export class HelloModule {}

TypeOrmModule.forFeature sẽ cung cấp quyền truy cập vào thực thể HelloRecord trong tất cả các providers/controllers của HelloModule. BTW, bạn không thể gán nhãn cho cùng một thực thể trong các module khác nhau nhiều lần. Nếu bạn muốn truy cập vào thực thể đó trong các module khác, chỉ cần nhập provider đang sử dụng thực thể đó.

Bây giờ hãy refactor HelloService để sử dụng Thực thể mới để lưu, sửa đổi và đọc các thông báo hello:

////// hello.service.ts ///////

import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { HiService } from 'src/hi/hi.service';
import { ByeService } from './bye.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { HelloRecord } from '../database/entities/hello-record.entity';

@Injectable()
export class HelloService {
  constructor(
    @Inject(forwardRef(() => HiService))
    private hiService: HiService,
    @Inject(forwardRef(() => ByeService))
    private byeService: ByeService,
    @InjectRepository(HelloRecord)
    private helloRecordRepo: Repository<HelloRecord>,
  ) {}

    async findById(id: string) {
    return await this.helloRecordRepo.findOneOrFail({ id });
  }

  async create(msg: string, ip: string) {
    const newMsg = this.helloRecordRepo.create({ msg, from: ip });
    return await newMsg.save();
  }

  async deleteById(id: string) {
    return await this.helloRecordRepo.delete({ id });
  }

  getHello(arg: string) {
    return `hello for ${arg}`;
  }

  // a method that uses `hiService`
  hiServiceUsingMethod() {
    return this.hiService.getHi('hello');
  }

  byeServiceUsingMethod() {
    return this.byeService.getBye('hello');
  }

TypeORM cung cấp tất cả các phương thức cần thiết để tạo-xóa-sửa đổi dữ liệu. Dưới đây là một số trong số những phương thức đó thường được sử dụng:

  • EntityName.findOne (Tìm bằng tiêu chí & trả về bản ghi khớp đầu tiên dưới dạng Promise)
  • EntityName.findOneOrFail (Tương tự như findOne nhưng ném ra một lỗi nếu không tìm thấy bản ghi nào. Luôn cố gắng sử dụng nó thay vì findOne vì nó hỗ trợ xử lý lỗi)
  • EntityName.find (tìm tất cả các bản ghi khớp với tiêu chí & trả về dưới dạng Promise)
  • EntityName.save (lưu bất kỳ đối tượng nào được truyền cho nó khớp với lược đồ của thực thể đó. Cũng có thể được sử dụng để sửa đổi/cập nhật một bản ghi)
  • EntityName.create (tạo một bản ghi mềm mới sẽ được truyền làm tham số cho EntityName.save)
  • EntityName.delete (xóa tất cả các bản ghi khớp với các tiêu chí được truyền)
  • EntityName.createQueryBuilder (API truy vấn thay thế sử dụng chuỗi để thao tác các giao dịch SQL thay vì sử dụng phương pháp hướng đối tượng. Nó giống như một phương pháp hàm. Nó tuân theo mô hình builder phổ biến và hỗ trợ chuỗi phương thức. Nó gần gũi hơn với SQL gốc)

https://dev.to/krtirtho/nestjs-the-framework-of-nodejs-part-3-database-integration-typeorm-4gab

Avatar photo

Clean Code: Nguyên tắc viết hàm trong lập trình…

Trong quá trình phát triển phần mềm, việc viết mã nguồn dễ đọc, dễ hiểu là yếu tố then chốt để đảm bảo code...
Avatar photo Dat Tran Thanh
3 min read

Clean Code: Nguyên tắc đặt tên (Naming)

Clean Code là việc viết mã nguồn rõ ràng, dễ hiểu, dễ bảo trì. Bài viết này sẽ giới thiệu nguyên tắc đầu tiên...
Avatar photo Dat Tran Thanh
4 min read

Leave a Reply

Your email address will not be published. Required fields are marked *