Closed6

NestJS触ってみる(DB)

kei3devkei3dev

DB環境構築

Docker で MySQL を使用

terminal
$ docker -v
Docker version 20.10.12, build e91ed57

docker-compose.ymlを作成

terminal
$ pwd
project/backend # ← プロジェクトのルートディレクトリという意味

$ touch docker-compose.yml
docker-compose.yml
version: '3'
services:
  db:
    image: mysql
    platform: linux/amd64
    env_file: .env
    ports:
      - "3306:3306"

ルートディレクトリに.envファイルを作成し MySQL の環境変数を定義

.env
MYSQL_ROOT_PASSWORD="password"
MYSQL_DATABASE="db"
MYSQL_USER="user"
MYSQL_PASSWORD="password"
TZ="Asia/Tokyo"

イメージ作成とコンテナ作成起動

terminal
$ docker-compose up -d

コンテナに入ってデータベースが作成できているか確認

terminal
$ docker-compose exec db mysql -u user -p
Enter password:   # ← MYSQL_PASSWORDを入力

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| db                 | # ← MYSQL_DATABASEの名前で作成できてれば成功
| information_schema |
+--------------------+
kei3devkei3dev

Prismaの導入

ORM に Prisma を使用

terminal
$ yarn add prisma --dev

Prisma の初期設定

terminal
$ yarn prisma init
prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}
.env
DATABASE_URL="mysql://root:password@localhost:3306/db"
kei3devkei3dev

テーブルを作成

schema.prismaに Item モデルを定義

schema.prisma
// --------- 略 ---------

model Item {
  id          String     @id @default(uuid())
  name        String
  price       Int
  description String
  status      ItemStatus
  created_at  DateTime   @default(dbgenerated("NOW()")) @db.Timestamp(0)
  updated_at  DateTime   @default(dbgenerated("NOW() ON UPDATE CURRENT_TIMESTAMP")) @db.Timestamp(0)

  @@map("items")
}

enum ItemStatus {
  ON_SALE
  SOLD_OUT
}

現時点で Prisma でタイムゾーンの変更ができない。
created_atupdated_at で DBで設定のタイムゾーンから取得して設定するようにしている。
こちらの記事を参考
Issue

タイムゾーンのことを意識しない場合はこれでいけそう↓

schema.prisma
model Item {
// --------- 略 ---------
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt
// --------- 略 ---------
}

マイグレーションファイル作成とテーブル作成

terminal
$ yarn prisma migrate dev --name create_items_table

↪︎ prisma/migrationsにマイグレーションファイルが作成される
↪︎ データベースにテーブルが作成される

データベースにテーブルが作成されているかを確認

terminal
$ yarn prisma studio
kei3devkei3dev

データベースへCRUD操作

@prisma/clientをインストール

terminal
$ yarn add @prisma/client

データベースへの接続を行う prisma.service.tssrc ディレクトリに作成

prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect()
  }
}

items.module.ts に追加

items.module.ts
import { Module } from '@nestjs/common'
import { ItemsController } from './items.controller'
import { ItemsService } from './items.service'
+ import { PrismaService } from '../prisma.service'

@Module({
  controllers: [ItemsController],
- providers: [ItemsService],
+ providers: [ItemsService, PrismaService],
})
export class ItemsModule {}

POST

items.service.tscreateメソッドを実装

items.service.ts
import { Item, Prisma } from '@prisma/client'
import { PrismaService } from 'src/prisma.service'
// --------- 略 ---------
@Injectable()
export class ItemsService {
  constructor(private prisma: PrismaService) {}
// --------- 略 ---------
  async create(createItemDto: CreateItemDto): Promise<Item> {
    const { name, price, description } = createItemDto
    const data: Prisma.ItemCreateInput = {
      name,
      price: Number(price),  // ← ここでキャストしないとエラーになる
      description,
      status: 'ON_SALE',
    }
    return this.prisma.item.create({ data })
  }
// --------- 略 ---------
}

↪︎ DTO クラスで class-validator を使用して定義したバリデーションを使用

バリデーション方法はこちらバリデーション の通り

itemsコントローラーの@Postを編集

items.controller.ts
// --------- 略 ---------
import { Item } from '@prisma/client'
// --------- 略 ---------
@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}
// --------- 略 ---------
  @Post()
  async create(@Body() createItemDto: CreateItemDto): Promise<Item> {
    return this.itemsService.create(createItemDto)
  }
// --------- 略 ---------
}

POST で DB に保存されるか確認

terminal
$ curl -X POST -d "name=Mac&price=100000&description=綺麗です" http://localhost:3000/items | jq

{
  "id": "6c6e429a-7606-4b46-9b6a-364a3………",
  "name": "Mac",
  "price": 100000,
  "description": "綺麗です",
  "status": "ON_SALE",
  "created_at": "2022-04-08T11:06:44.000Z",
  "updated_at": "2022-04-08T11:06:44.000Z"
}

↪︎ id が自動的に uuid で付与されている
↪︎ DateTime が JST になっている


クエリのログが出力されるようにする
prisma.service.ts を編集

prisma.service.ts
import { Injectable, OnModuleInit, Logger } from '@nestjs/common'
import { PrismaClient, Prisma } from '@prisma/client'

@Injectable()
export class PrismaService
  extends PrismaClient<Prisma.PrismaClientOptions, Prisma.LogLevel>
  implements OnModuleInit
{
  private readonly logger = new Logger(PrismaService.name)
  constructor() {
    super({ log: ['query', 'info', 'warn', 'error'] })
  }
  async onModuleInit() {
    this.$on('query', (event) => {
      this.logger.log(
        `Query: ${event.query}`,
        `Params: ${event.params}`,
        `Duration: ${event.duration} ms`,
      )
    })
    this.$on('info', (event) => {
      this.logger.log(`message: ${event.message}`)
    })
    this.$on('error', (event) => {
      this.logger.log(`error: ${event.message}`)
    })
    this.$on('warn', (event) => {
      this.logger.log(`warn: ${event.message}`)
    })
    await this.$connect()
  }
}

こちらの記事を参考 ↓
https://zenn.dev/takepepe/articles/nestjs-prisma-logger


GET

items.service.tsfindAllメソッドとfindByIdメソッドを実装
(全件取得と個別取得)

items.service.ts
// --------- 略 ---------
@Injectable()
export class ItemsService {
// --------- 略 ---------
  async findAll(): Promise<Item[]> {
    return this.prisma.item.findMany()
  }

  async findById(where: Prisma.ItemWhereUniqueInput): Promise<Item> {
    const found = this.prisma.item.findUnique({ where })
    if (!found) {
      throw new NotFoundException()
    }
    return found
  }
// --------- 略 ---------
}

Controller の@Getを編集

items.controller.ts
// --------- 略 ---------
@Controller('items')
export class ItemsController {
// --------- 略 ---------
  @Get()
  async findAll(): Promise<Item[]> {
    return this.itemsService.findAll()
  }

  @Get(':id')
  async findById(@Param('id', ParseUUIDPipe) id: string): Promise<Item> {
    return this.itemsService.findById({ id })
  }
// --------- 略 ---------
}

PATCH

updateStatusメソッドを実装
(statusをSOLD_OUTにするメソッド)

items.service.ts
// --------- 略 ---------
@Injectable()
export class ItemsService {
// --------- 略 ---------
  async updateStatus(where: Prisma.ItemWhereUniqueInput): Promise<Item> {
    return this.prisma.item.update({
      where,
      data: { status: 'SOLD_OUT' },
    })
  }
// --------- 略 ---------
}

Controller の@Patchを編集

items.controller.ts
// --------- 略 ---------
@Controller('items')
export class ItemsController {
// --------- 略 ---------
  @Patch(':id')
  async updateStatus(@Param('id', ParseUUIDPipe) id: string): Promise<Item> {
    return this.itemsService.updateStatus({ id })
  }
// --------- 略 ---------
}

DELETE

deleteメソッドを実装

items.service.ts
// --------- 略 ---------
@Injectable()
export class ItemsService {
// --------- 略 ---------
  async delete(where: Prisma.ItemWhereUniqueInput): Promise<Item> {
    return this.prisma.item.delete({ where })
  }
// --------- 略 ---------
}

Controller の@Deleteを編集

items.controller.ts
// --------- 略 ---------
@Controller('items')
export class ItemsController {
// --------- 略 ---------
  @Delete(':id')
  async delete(@Param('id', ParseUUIDPipe) id: string): Promise<Item> {
    return this.itemsService.delete({ id })
  }
// --------- 略 ---------
}
このスクラップは2022/04/18にクローズされました