ys memos

Blog

NestJS 入門コース3 DBとCRUD


nestjs

2022/03/18

前記事

次記事


本記事では,NestJS で作成した REST API を,DB に接続する.

ここでは,TypeORM を用い,MySQL への CRUD(Create, Read, Update, Delete)処理を書いてみる.


REST API に DB を構成する場合,Module の構成は以下のようになる.

TodoController および TodoService は前述のままなので割愛するが,ここではTodoRepositoryと Todo の DB が追加されている.

DB は今回,Docker で建てたローカルの MySQL コンテナを用いる.

また,DB アクセスは TypeORM を用いるため,TodoRepository は特別に実装すること無く,TodoService にインジェクトする形で用いることになる.

graph TB; id1(TodoController); id2(TodoService); id3(TodoRepository); id4(DB); subgraph   subgraph TodoModule id1-->id2-->id3; end id3-->id4; end

Docker のインストール が必要.Ubuntu の方法はこちらに日本語でのインストール方法の紹介を記載した.

ここでは,MySQL を使うため,コンテナイメージを取得およびコンテナの起動をする.

TypeORM で MySQL に接続するテーブルを作成するために,Adminer を起動する.

まずは,以下の内容でdocker-compose.ymlを作成する.

docker-compose.yml
version: '3'

services:
  db:
    image: mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
    ports:
      - 3306:3306
  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
$ docker-compose up -d

ここでdocker psコマンドの結果は,以下のようになっている.

$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
a0ef9f27b8b4   adminer   "entrypoint.sh docke…"   51 minutes ago   Up 49 minutes   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp              my-project_adminer_1
b6a68259d349   mysql     "docker-entrypoint.s…"   51 minutes ago   Up 49 minutes   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   my-project_db_1

ここで,ブラウザからhttp://locahost:8080へアクセスすると Adminer を開くことが出来,以下の内容でログインする.

項目入力
SystemMySQL
Serverdb
Usernameroot
Passwordroot

ログインした後のページで,Create databaseを押し,databaseと入力してSaveをクリックする.


$ npm i @nestjs/typeorm typeorm mysql2

TypeORM とは,TypeScript 製の ORM であり,DB とサーバの間でのデータのやり取りを簡素化してくれる.

NestJS においては,TypeORM を簡単に扱えるように Module が公開されており,そのモジュールを使うと,スキーマの設定,DB への接続,データのやり取りができる.


ここでは,上で起動した MySQL にプログラムでアクセスする.

rootユーザに起動時に指定したパスワードrootを用いてアクセスする.

TypeOrmModuleの設定にはforRoot()メソッドを使い,これはapp.module.tsimportsに追記する.

src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodoModule } from './todo/todo.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      // 接続するDBのタイプを指定,RDBMSのみならず,Mongo等もサポートされている
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'database'
      // DBに格納するエンティティを設定
      entities: [],
      // 本番用ではfalseにする(trueにするとデータを消失する可能性があるとのこと)
      synchronize: true,
    }),
    TodoModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

NestJS のログに以下の行があれば接続完了である.

[Nest] 80646  - 03/19/2022, 11:48:45 PM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms

また,環境変数を扱うための Module であるConfigModuleを扱う場合は,非同期でセットアップするメソッドであるforRootAsync()を使う必要がある.


TypeORM によって提供されている@Entity()を用い,エンティティを作成できる.

src/todo/todo.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

// エンティティを作成
// @Entity('hoge')とするとテーブル名をhogeにできる
@Entity()
export class Todo {
  // 自動生成されるID
  @PrimaryGeneratedColumn()
  id: number;

  // カラム
  @Column()
  title: string;

  @Column()
  description: string;
}

ここで作成したエンティティを DB に保存する.

TypeOrmModule.forRoot()entitiesにエンティティクラスを入れるとエンティティを扱えるようになる.

src/app.module.ts
-     entities: [],
+     entities: [Todo],

先程ログインした Adminer からdatabaseという名前の Database を見ると,todoという Table が作られていることが分かる.


src/todo/にある 2 つのファイルを書き換える.

src/todo/todo.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodoController } from './todo.controller';
import { Todo } from './todo.entity';
import { TodoService } from './todo.service';

@Module({
  // Todoエンティティを使うという設定,forRootとは別で必要
  imports: [TypeOrmModule.forFeature([Todo])],
  controllers: [TodoController],
  providers: [TodoService],
})
export class TodoModule {}
src/todo/todo.controller.ts
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { TodoService } from './todo.service';
import { CreateTodoDto } from './create-todo.dto';

@Controller('todo')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}

  @Get(':id') // 以前の灑ではstringにしていたがnumberに変更
  findOne(@Param('id') id: number) {
    return this.todoService.findOne(id);
  }

  @Post()
  createTodo(@Body() createTodoDto: CreateTodoDto) {
    return this.todoService.createTodo(createTodoDto);
  }
}
src/todo/todo.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTodoDto } from './create-todo.dto';
import { Todo } from './todo.entity';

@Injectable()
export class TodoService {
  constructor(
    // Repository型でtodoRepositoryというプライベートメンバにインジェクトする
    @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
  ) {}

  async findOne(id: number): Promise<Todo> {
    // TypeORMのrepositoryは,findOne()でデータを取得できる
    return this.todoRepository.findOne(id);
  }

  async createTodo(createTodoDto: CreateTodoDto): Promise<Todo> {
    // create(...)でオブジェクトを作成できる(DB保存はしない)
    const todo = this.todoRepository.create(createTodoDto);
    // save(obj)でオブジェクトをDBに保存
    return this.todoRepository.save(todo);
  }
}

Todo を更新・削除するためのエンドポイントをそれぞれ作る.

一旦ここで,src/todo/dto/フォルダを作成し,create-todo.dto.tsを移動しておく.

また,Todo の更新用の DTO をsrc/todo/dto/update-todo.dto.ts新しく作る.

src/todo/dto/update-todo.dto.ts
export class UpdateTodoDto {
  id: number;
  // 任意のフィールドを更新できるようにオプションにしておく
  title?: string;
  description?: string;
}

次に,更新・削除の処理を記述する.

既存のfindOne()については,他の処理にも流用するために例外処理を行う.NestJS で提供されている ExceptionFilter は,API を通じてフロントエンドにエラーコードを投げてくれる.

src/todo/todo/service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Todo } from './todo.entity';
import { CreateTodoDto } from './dto/create-todo.dto';
// 上で作成した,データ更新用のDTO
import { UpdateTodoDto } from './dto/update-todo.dto';

@Injectable()
export class TodoService {
  constructor(
    @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
  ) {}

  async findOne(id: number): Promise<Todo> {
    const todo = await this.todoRepository.findOne(id);
    // データが存在する場合のみ結果を返す
    if (todo) return todo;
    // データが存在しない場合はNotFoundを返す
    throw new NotFoundException(`Todo ${id} not found`);
  }

  async createTodo(createTodoDto: CreateTodoDto): Promise<Todo> {
    const todo = this.todoRepository.create(createTodoDto);
    return this.todoRepository.save(todo);
  }

  async updateTodo(updateTodoDto: UpdateTodoDto): Promise<Todo> {
    const { id, ...other } = updateTodoDto;
    // todoを探して,指定された内容で更新する
    // todoが存在しない場合はfindOne()内の例外処理が行われる
    const todo = {
      ...(await this.findOne(id)),
      ...other,
    };
    return this.todoRepository.save(todo);
  }

  async deleteTodo(id: number): Promise<boolean> {
    const todo = await this.findOne(id);
    return !!(await this.todoRepository.remove(todo));
  }
}

次に,controller へ以下のようにエンドポイントを追加する.

src/todo/todo.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { TodoService } from './todo.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { Todo } from './todo.entity';
import { UpdateTodoDto } from './dto/update-todo.dto';

@Controller('todo')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}

  @Get(':id')
  async findOne(@Param('id') id: number) {
    return this.todoService.findOne(id);
  }

  @Post()
  createTodo(@Body() createTodoDto: CreateTodoDto): Promise<Todo> {
    return this.todoService.createTodo(createTodoDto);
  }

  @Patch()
  updateTodo(@Body() updateTodoDto: UpdateTodoDto): Promise<Todo> {
    return this.todoService.updateTodo(updateTodoDto);
  }

  @Delete(':id')
  deleteTodo(@Param('id') id: number): Promise<boolean> {
    return this.todoService.deleteTodo(id);
  }
}


関連タグを探す