2022/03/18
はじめに
本記事では,NestJS で作成した REST API を,DB に接続する.
ここでは,TypeORM を用い,MySQL への CRUD(Create, Read, Update, Delete)処理を書いてみる.
Module 構成
REST API に DB を構成する場合,Module の構成は以下のようになる.
TodoController および TodoService は前述のままなので割愛するが,ここではTodoRepository
と Todo の DB が追加されている.
DB は今回,Docker で建てたローカルの MySQL コンテナを用いる.
また,DB アクセスは TypeORM を用いるため,TodoRepository は特別に実装すること無く,TodoService にインジェクトする形で用いることになる.
DB の準備
Docker のインストール が必要.Ubuntu の方法はこちらに日本語でのインストール方法の紹介を記載した.
ここでは,MySQL を使うため,コンテナイメージを取得およびコンテナの起動をする.
TypeORM で MySQL に接続するテーブルを作成するために,Adminer を起動する.
まずは,以下の内容で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 を開くことが出来,以下の内容でログインする.
項目 | 入力 |
---|---|
System | MySQL |
Server | db |
Username | root |
Password | root |
ログインした後のページで,Create database
を押し,database
と入力してSave
をクリックする.
パッケージのインストール
$ npm i @nestjs/typeorm typeorm mysql2
TypeORM とは
TypeORM とは,TypeScript 製の ORM であり,DB とサーバの間でのデータのやり取りを簡素化してくれる.
NestJS においては,TypeORM を簡単に扱えるように Module が公開されており,そのモジュールを使うと,スキーマの設定,DB への接続,データのやり取りができる.
TypeORM で MySQL に接続する
ここでは,上で起動した MySQL にプログラムでアクセスする.
root
ユーザに起動時に指定したパスワードroot
を用いてアクセスする.
TypeOrmModule
の設定にはforRoot()
メソッドを使い,これはapp.module.ts
のimports
に追記する.
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()
を用い,エンティティを作成できる.
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
にエンティティクラスを入れるとエンティティを扱えるようになる.
- entities: [],
+ entities: [Todo],
先程ログインした Adminer からdatabase
という名前の Database を見ると,todo
という Table が作られていることが分かる.
DB アクセスの処理を書く
src/todo/
にある 2 つのファイルを書き換える.
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 {}
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);
}
}
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
新しく作る.
export class UpdateTodoDto {
id: number;
// 任意のフィールドを更新できるようにオプションにしておく
title?: string;
description?: string;
}
次に,更新・削除の処理を記述する.
既存のfindOne()
については,他の処理にも流用するために例外処理を行う.NestJS で提供されている ExceptionFilter は,API を通じてフロントエンドにエラーコードを投げてくれる.
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 へ以下のようにエンドポイントを追加する.
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);
}
}