手順
- docker-composeを使ってMySQLサーバーを立ち上げる
- NestJSにtypeormを導入する
- NestJSでデータベースと接続するための設定をする
- NestJSでentityを定義する
- NestJSでデータベースのマイグレーションを行う
- 実際にデータベースとの通信をしてみる
- (おまけ) データベースに格納されたデータを確認する
- CLIで確認する
- GUI(Sequel Pro)で確認する
docker-composeを使ってMySQLサーバーを立ち上げる
今回は 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ポートを使用するようにしています。
[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でデータベースと接続するための設定をする
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ファイルを作成します。
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/migrations
に 1655019576807-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
ここで、マイグレーションファイルの中身を見ておきましょう。
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
で依存するパッケージを追加してから以下のファイルを作成してください。
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を作成します。
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を作成します。
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);
}
}
次にコントローラーを作成します。
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の設定を行っていきます。
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」ボタンを押しましょう。
{
"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データベースに直接アクセスできます。インストールしていなくて気になった方は、以下のページから是非インストールしてみてください。
以下のように設定をして、「接続」ボタンを押すと確認できるようになります。ぜひ試してみてください。
終わりに
ローカルで開発する上で、必要な手順について記述してきました。テストを実行するときもテスト用のコンテナを使用すれば、いけるかな?と思っています。
ただ、CI上でのテストは困ると思います。例えばGitHub Actionsを使っている人は Setup MySQL とか使ったら、いけそうですかねー。ちょっと今度機会を作って試してみたいと思います。