LoginSignup
1
1

More than 1 year has passed since last update.

NestJSとDockerコンテナで立ち上げたMySQLサーバーを接続する

Last updated at Posted at 2022-06-26

手順

  • docker-composeを使ってMySQLサーバーを立ち上げる
  • NestJSにtypeormを導入する
  • NestJSでデータベースと接続するための設定をする
  • NestJSでentityを定義する
  • NestJSでデータベースのマイグレーションを行う
  • 実際にデータベースとの通信をしてみる
  • (おまけ) データベースに格納されたデータを確認する
    • CLIで確認する
    • GUI(Sequel Pro)で確認する

docker-composeを使ってMySQLサーバーを立ち上げる

今回は docker-compose.yml に設定を記述します。

docker-compose.yml
version: '3.7'

services:
  db:
    image: mysql:5.7.38 # https://hub.docker.com/_/mysql
    restart: always
    volumes:
      - ./db/data:/var/lib/mysql
      - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
    environment:
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=tutorial
    ports:
      - '4306:3306'

ここで、portsを設定しているのは、ローカル環境でMySQLを立ち上げている可能性があるので、衝突を起こさないため、4306ポートを使用するようにしています。

db/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[client]
default-character-set=utf8mb4

db/my.cnf を設定してvolumesオプションに設定することで、dockerコンテナ内のMySQLサーバーの設定を上書きすることができます。
今回は日本語も扱いたいため、 utf8mb4 を指定しています。

設定ができましたら、コンテナを立ち上げましょう。

$ docker-compose up

NestJSにtypeormを導入する

$ yarn add --dev @nestjs/typeorm@8.0.4 mysql typeorm@0.2.45

最新バージョンだと

$ npx typeorm migration:generate

したとき、後述のcliオプションが読み込まれなかったり、マイグレーション時の -n がなくてマイグレーションファイル生成時の命名ができなかったので、バージョンを落としてインストールしています。

NestJSでデータベースと接続するための設定をする

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ItemsModule } from './items/items.module';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';

const options: TypeOrmModuleOptions = {
  type: 'mysql',
  host: '127.0.0.1',
  port: 4306,
  username: 'root',
  password: 'root',
  database: 'tutorial',
  charset: 'utf8mb4_unicode_ci',
  autoLoadEntities: true,
  entities: ['dist/entities/*.entity.js'],
  migrations: ['dist/migrations/*.js'],
  cli: {
    entitiesDir: 'src/entities',
    migrationsDir: 'src/migrations',
  },
};

@Module({
  imports: [ItemsModule, TypeOrmModule.forRoot(options)],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

基本的に、docker-composeで設定した内容をもとに接続先情報の設定を記述しています。

この設定は、プロジェクト直下に ormconfig.js に記述しても良いのですが、型をつけるために切り出しています。 const options~~ の部分は別ファイルに切り出した方がわかりやすいかもしれません。

また、 charset: 'utf8mb4_unicode_ci' を指定しないと、dbの設定はできていてもアプリケーションのレイヤーで ERROR [ExceptionsHandler] ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: Incorrect string value のエラーが吐かれます。dbの設定だけでなく、NestJS側の設定もしなければいけない点に、注意してください。

NestJSでentityを定義する

先ほどの設定で、entityは src/entities 配下にファイルを置くように指定しましたので、そちらにentityファイルを作成します。

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

@Entity()
export class Item {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

  @Column()
  price: number;

  @Column()
  description: string;

  @Column()
  createdAt: string;

  @Column()
  updatedAt: string;
}

NestJSでデータベースのマイグレーションを行う

では、前の手順で定義したentityを基にマイグレーションファイルを生成します。

$ npx typeorm migration:generate -n CreateItem

こちらを実行すると、 src/migrations1655019576807-CreateItem.ts のような名前のマイグレーションファイルが作成されます(マイグレーションファイルの生成ディレクトリは cli.migrationsDir で設定したディレクトリです)。頭の数字の文字列は生成時のタイムスタンプで、後者の文字列はコマンド実行時の -n オプションで渡した文字列になります。どのようなマイグレーションを行ったかが一目でわかるので、この -n オプションはなるべく使用するべきだと個人的に思っています。

では、実際にマイグレーションを実行しましょう。

$ npx typeorm migration:run

以下のようなログが確認できればマイグレーション成功です。

query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'tutorial' AND `TABLE_NAME` = 'migrations'
query: CREATE TABLE `migrations` (`id` int NOT NULL AUTO_INCREMENT, `timestamp` bigint NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB
query: SELECT * FROM `tutorial`.`migrations` `migrations` ORDER BY `id` DESC
0 migrations are already loaded in the database.
1 migrations were found in the source code.
1 migrations are new migrations that needs to be executed.
query: START TRANSACTION
query: CREATE TABLE `item` (`id` varchar(36) NOT NULL, `name` varchar(255) NOT NULL, `price` int NOT NULL, `description` varchar(255) NOT NULL, `createdAt` varchar(255) NOT NULL, `updatedAt` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB
query: INSERT INTO `tutorial`.`migrations`(`timestamp`, `name`) VALUES (?, ?) -- PARAMETERS: [1655019576807,"CreateItem1655019576807"]
Migration CreateItem1655019576807 has been executed successfully.
query: COMMIT

ここで、マイグレーションファイルの中身を見ておきましょう。

src/migrations/1655019576807-CreateItem.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateItem1655019576807 implements MigrationInterface {
  name = 'CreateItem1655019576807';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `CREATE TABLE \`item\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(255) NOT NULL, \`price\` int NOT NULL, \`description\` varchar(255) NOT NULL, \`status\` varchar(255) NOT NULL, \`createdAt\` varchar(255) NOT NULL, \`updatedAt\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE \`item\``);
  }
}

上記のマイグレーションファイルは、entityとその時のマイグレーション情報から自動でsql文を生成してくれます。
今回は、table作成の必要があったため create table というsql文を実行しています。カラムが不要になったり、追加したり、するときは、その時のマイグレーションの情報にあったsql文を自動で生成してくれます。ぜひ試してみてください。

成功したら、以下のコマンドで巻き戻せることも確認しておきましょう。

$ npx typeorm migration:revert

実際にデータベースとの通信をしてみる

ここではGetとPostのAPIを定義します。

まずは、dto(Data Transfer Object)を定義します。

$ yarn add class-transformer class-validator

で依存するパッケージを追加してから以下のファイルを作成してください。

src/items/dto/create-item.dto.ts
import { Type } from 'class-transformer';
import { IsInt, IsNotEmpty, IsString, MaxLength, Min } from 'class-validator';

export class CreateItemDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(40)
  name: string;

  @IsInt()
  @Min(1)
  @Type(() => Number)
  price: number;

  @IsString()
  @IsNotEmpty()
  description: string;
}

次に、repositoryを作成します。

src/items/item.repository.ts
import { Item } from './../entities/item.entity';
import { EntityRepository, Repository } from 'typeorm';
import { CreateItemDto } from './dto/create-item.dto';

@EntityRepository(Item)
export class ItemRepository extends Repository<Item> {
  async createItem(createItemDto: CreateItemDto): Promise<Item> {
    const { name, price, description } = createItemDto;
    const item = this.create({
      name,
      price,
      description,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    });
    await this.save(item);
    return item;
  }
}

次に、serviceを作成します。

src/items/items.service.ts
import { Item } from './../entities/item.entity';
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateItemDto } from './dto/create-item.dto';
import { ItemRepository } from './item.repository';

@Injectable()
export class ItemsService {
  constructor(private readonly itemRepository: ItemRepository) {}
  private items: Item[] = [];

  async findAll(): Promise<Item[]> {
    return await this.itemRepository.find();
  }

  async findById(id: string): Promise<Item> {
    const found = await this.itemRepository.findOne(id);
    if (!found) {
      throw new NotFoundException();
    }
    return found;
  }

  async create(createItemDto: CreateItemDto): Promise<Item> {
    return await this.itemRepository.createItem(createItemDto);
  }
}

次にコントローラーを作成します。

src/items/items.controller.ts
import { Item } from './../entities/item.entity';
import {
  Body,
  Controller,
  Get,
  Param,
  ParseUUIDPipe,
  Post,
} from '@nestjs/common';
import { ItemsService } from './items.service';
import { CreateItemDto } from './dto/create-item.dto';

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Get()
  async findAll(): Promise<Item[]> {
    return await this.itemsService.findAll();
  }

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

  @Post()
  async create(@Body() createItemDto: CreateItemDto): Promise<Item> {
    return await this.itemsService.create(createItemDto);
  }
}

最後にmoduleの設定を行っていきます。

src/items/items.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ItemRepository } from './item.repository';
import { ItemsController } from './items.controller';
import { ItemsService } from './items.service';

@Module({
  imports: [TypeOrmModule.forFeature([ItemRepository])],
  controllers: [ItemsController],
  providers: [ItemsService],
})
export class ItemsModule {}

これでNestJS側の実装は完了です。

ではサーバーを立ち上げてから、試しにPostmanでデータの登録と取得をしてみましょう。

まずは

$ yarn run start:dev

で立ち上げます。次に、データを登録していきます。Postmanを開いて、Bodyを埋めて「Send」ボタンを押しましょう。

スクリーンショット 2022-06-26 11.33.58.png

{
    "name": "item1",
    "price": "15000",
    "description": "これはitem1です",
    "createdAt": "2022-06-26T02:29:52.609Z",
    "updatedAt": "2022-06-26T02:29:52.609Z",
    "id": "af743ca2-5544-4690-a4e6-a4cf2ec07736"
}

以上のデータが返却されたら成功です。
もし、Postmanの使い方がわからなければ、以下の記事が大変わかりやすいので、見てみてください!

(おまけ) データベースに格納されたデータを確認する

CLIで確認する

まず、以下のコマンドでコンテナに入ります。

$ docker-compose exec db /bin/bash

次に、コンテナで立ち上げているdbに接続します。パスワードが求められるので、パスワードを入力しましょう。

root@ca19b89fde11:/# mysql -u root -p
Enter password:

では実際に、テーブルが作成されているか、みていきましょう。

mysql> use tutorial;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+--------------------+
| Tables_in_tutorial |
+--------------------+
| item               |
| migrations         |
+--------------------+
2 rows in set (0.00 sec)

確認すると、itemテーブルとmigrationsテーブルが作成されていますね。

select * from item; をすると、テーブルの中身が確認できるので、実際にレコードが作成されているか確認してみましょう。

GUI(Sequel Pro)で確認する

Sequel Proは、MySQLデータベースを操作するための高速で使いやすいMacOS専用のGUIツールです。Sequel Proを使用すると、ローカルサーバーとリモートサーバー上のMySQLデータベースに直接アクセスできます。インストールしていなくて気になった方は、以下のページから是非インストールしてみてください。

以下のように設定をして、「接続」ボタンを押すと確認できるようになります。ぜひ試してみてください。

スクリーンショット 2022-06-26 11.41.30.png

終わりに

ローカルで開発する上で、必要な手順について記述してきました。テストを実行するときもテスト用のコンテナを使用すれば、いけるかな?と思っています。
ただ、CI上でのテストは困ると思います。例えばGitHub Actionsを使っている人は Setup MySQL とか使ったら、いけそうですかねー。ちょっと今度機会を作って試してみたいと思います。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1