NestJS v9 + TypeORM v0.3 + MySQL + Docker で開発環境を構築する

# はじめに

NestJS の環境を Docker コンテナを使って作成しようとしたところ、NestJS とTypeORM の最新バージョンが上がっていました。
せっかくなので、最新のバージョンを使いましたが、断片的な情報しかなくて結構苦労しました。

これから触る方に参考となればと思い、一通り全ての作業記録を載せておきます。
指定した主なバージョンは次のとおりです。
mac を使用していますが windows でも基本的には変わらないと思います。

|ライブラリ|バージョン|
|---|---|
|Node.js|16.17.0|
|NestJS|9.1.2|
|TypeORM|0.3.9|
|MySQL|8.0|

＜**参考記事**＞
・[TypeORM 0.3系のマイグレーション](https://qiita.com/Aurum64/items/f5962bd2a643447dbef9)
・[NestJSをTypeORM 0.3 で使う](https://zenn.dev/hasegawasatoshi/articles/8110ce79119dd0)
・[NestJS公式 - Database](https://docs.nestjs.com/techniques/database#migrations)
・[Dockerを利用してNestJS＋TypeORM+MySQLの環境構築する](https://zenn.dev/senri/articles/331162304a78e0)
・[NestJSとDockerコンテナで立ち上げたMySQLサーバーを接続する](https://qiita.com/mball/items/3defb402b23d3d14ea86)
・[NestJS + TypeORM + MySQL を使ってテーブルを作ってみる](https://go-tech.blog/nestjs-typeorm-mysql/)
・[NestJS 入門コース３ DBとCRUD](https://ysuzuki19.github.io/post/nestjs-cource3-rest-api-db)

# 1. 準備

## 1-1. NestJS のインストール

Node.js がインストールされていることが前提です（Node のインストールは省略します）。
手元のバージョンは次のとおりです（現在の LTS）。

```
$ node -v
v16.17.0
```

バージョンアップなどは次の記事を参照ください。
・[n (Node.js管理) のインストール手順](https://qiita.com/tora_oba/items/2761452fe8c2b0035ba0)

NestJS は次のコマンドでインストール（またはアップグレード）します。バージョンは `9.1.2` を指定しています（エラーが出たら sudo を付けて実行してみてください）。

```
$ npm i -g @nestjs/cli@9.1.2
```

なお、これを書いている時点では、`$ npm i -g @nestjs/cli` で v9.1.2 がインストールされます。

`nest -v` でバージョンの確認ができます。

```
$ nest -v
9.1.2
```

## 1-2. NestJS プロジェクト作成

適当なフォルダを選んで NestJS のプロジェクトを１つ作ります。
プロジェクト名を nest-docker-sample として次のコマンドを実行します。

```
$ nest new nest-docker-sample
```

コマンドを実行すると、どの package manager を使用するか聞かれますので、ここでは npm を選びます。
![package-manager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/0a322630-912b-be2f-eeb9-6e13c73eb6fc.png)
しばらくすると、プロジェクトが作成されます。

# 2. Dockerfile と docker-compose.yml の作成

プロジェクトが作成されたら、ルートディレクトリ直下に３つのファイル（`Dockerfile`, `docker-compose.yml`, `.dockerignore`）を作成します（下図）。

```
nest-docker-sample
├──Dockerfile  # 作成
├──docker-compose.yml  # 作成
├──.dockerignore  # 作成
├──package.json
├──package-lock.json
└──(...先に作成したその他の nest のファイル)
```


＜**参考記事**＞
・[いまさらだけどDockerに入門したので分かりやすくまとめてみた](https://qiita.com/gold-kou/items/44860fbda1a34a001fc1#build)

## 2-1. Dockerfile

### 2-1-1. 基本的な記載事項（最小限の内容）

Dockerfile に記載する基本的な内容は次のとおりです。
なお、この内容では、コンテナ内で git を使用することはできません。

```DockerFile:Dockerfile
FROM node:16-alpine3.16
WORKDIR /api
COPY package*.json ./
RUN npm ci
COPY . .
```

指定している内容は、ざっと次のとおりです。

|コマンド（命令）|概要|備考|
|---|---|---|
|**[FROM](https://docs.docker.jp/engine/reference/builder.html#from)**|ベースイメージを指定します。|ここでは、[Node.js の公式 Docker イメージ](https://hub.docker.com/_/node/)を使用しています（OS は [alpine](https://ja.wikipedia.org/wiki/Alpine_Linux)※）。|
**[WORKDIR](https://docs.docker.jp/engine/reference/builder.html#workdir)**|コンテナ内での「コマンドの実行場所」を指定します。|ここでは `/api` と指定しています。|
|**[COPY](https://docs.docker.jp/engine/reference/builder.html#copy)**|左側にコピー元（ホスト側）<br />右側にコピー先（コンテナ側）<br />のディレクトリを指定します。|**① `COPY package*.json ./`**<br />コピー元の `package*.json` は、package.json と package-lock.json の２つのファイルです。<br/>コピー先の `./` は上記の WORKDIR の直下（つまり `/api/`）です。<br />**② `COPY . .`**<br />ホスト側に作成したファイルをコンテナにコピーしています。|
|**[RUN](https://docs.docker.jp/engine/reference/builder.html#run)**|実行するコマンドを指定します。|コンテナに COPY された package.json と package-lock.json を元にライブラリをインストールします。|

※ alpine は厳密には OS そのものではないですが、ここでは OS と呼んでおきます（参考: [Linuxディストリビューション](https://ja.wikipedia.org/wiki/Linux%E3%83%87%E3%82%A3%E3%82%B9%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)）。

### 2-1-2. git をインストールする場合

コンテナ内でも git を使用する場合は、次のように記載してください（詳細は「[5. Git について](#5-git-について)」参照）。

```Dockerfile:Dockerfile
FROM node:16-alpine3.16
RUN apk update && \
    apk add bash git make && \
    apk add --upgrade grep
RUN git clone https://github.com/awslabs/git-secrets /home/alpine/git-secrets
WORKDIR /home/alpine/git-secrets
RUN make && make install
WORKDIR /api
COPY package*.json ./
RUN npm ci
COPY . .
```

### 2-1-3. Nest CLI をインストールする場合

コンテナ内で `nest g module` などの nest のコマンドを実行する必要がある場合は、Nest CLI もインストールしておきます。
以下のように、`npm ci && npm i -g @nestjs/cli@9.1.2` とすれば、Nest CLI もインストールされます。

```Dockerfile:Dockerfile
FROM node:16-alpine3.16
RUN apk update && \
    apk add bash git make && \
    apk add --upgrade grep
RUN git clone https://github.com/awslabs/git-secrets /home/alpine/git-secrets
WORKDIR /home/alpine/git-secrets
RUN make && make install
WORKDIR /api
COPY package*.json ./
RUN npm ci && \
    npm i -g @nestjs/cli@9.1.2
COPY . .
```

## 2-2. docker-compose.yml

docker-compose.yml には次のように記載します。

```docker-compose.yml
version: '3.8'
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    restart: always
    ports:
      - 3000:3000
    tty: true
    volumes:
      - type: bind
        source: .
        target: /api
    depends_on:
      - db
  db:
    image: mysql:8.0
    restart: always
    ports:
      - 3306:3306
    environment:
      MYSQL_DATABASE: sample
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: user
      MYSQL_PASSWORD: password
    volumes:
      - ./db/data:/var/lib/mysql
```

指定した内容をざっと書き出すと次のとおりです。

|項目|内容|備考|
|---|---|---|
|**version:**|[Compose ファイルのバージョン](https://matsuand.github.io/docs.docker.jp.onthefly/compose/compose-file/compose-file-v3/) です。|今は `3` とか `3.8` で良いようです。|
|**service:**|以下に、コンテナの定義を記載します。|ここでは、**service 名**を`api` と `db` とした２つのコンテナを作成しています。|
|**build:**|指定された Dockerfile から、Docker イメージを作成します。|`context:` ワーキングディレクトリを指定<br />`dockerfile:` ドッカーファイルのファイル名（ディレクトリ）を指定|
|**restart:**|コンテナの再起動について指定します。||
|**ports:**|左側にホスト側のポート、右側にコンテナ側のポートを指定します。|MySQL のホスト側の port を変えたい場合は、`3307:3306` のように記載します（ここの修正のみでOKで、後掲の [data-source.ts](#4-3-datasource-の作成) の修正は不要です。[参考](https://www.web-dev-qa-db-ja.com/ja/mysql/nodejs%E3%81%AFmysqldocker%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%97%E3%81%BE%E3%81%99econnrefused/833127081/)）。|
|**tty:**|コンテナを起動させ続けるために記載します|参考：[DockerのTTYって何?](https://zenn.dev/hohner/articles/43a0da20181d34)|
|**volumes:**|ホスト側のデータと、docker コンテナ側のデータをバインドさせます。|**① long syntax**<br />　`type:` [マウントの種類](https://docs.docker.jp/compose/compose-file/compose-file-v3.html#id20)を指定<br />　`source:` ホスト側ディレクトリを指定<br />　`target:` コンテナ側ディレクトリを指定<br />**② short syntax**<br />　左側: ホスト側のディレクトリを指定<br />　右側: コンテナ側のディレクトリを指定|
|**environment:**|環境変数を指定します。|`MYSQL_DATABASE:` DB 名<br />`MYSQL_ROOT_PASSWORD:` root のパスワード<br />`MYSQL_USER:` ユーザー名<br />`MYSQL_PASSWORD:` ユーザーのパスワード|
|**depends_on:**|サービス間の[依存関係](https://docs.docker.jp/compose/compose-file/compose-file-v3.html#depends-on)を指定します。|`api:`<br />`　depends_on:`<br />`　　- db`<br />この例では api より前に db が起動することになります。|

コンテナが消えても DB の内容を保持できるように、ホスト側に `/db` というディレクトリを作成して、MySQL のデータを保存するようにしています。
保存場所については、Docker の volume に保存する方法もありますので、そのあたりは他のサイトなどで確認してみてください（ここでは行いません）。

```yml:docker-compose.yml 抜粋
    volumes:
      - ./db/data:/var/lib/mysql
```

＜参考サイト＞
・[Compose ファイル・リファレンス](https://docs.docker.jp/v1.12/compose/compose-file.html)
・[docker-compose.ymlの書き方について解説してみた](https://qiita.com/yuta-ushijima/items/d3d98177e1b28f736f04)

## 2-3. その他のファイル

### 2-3-1. .dockerignore

.dockerignore ファイルには、Dockerのビルド時に無視するディレクトリ（ファイル）を指定します。

```.dockerignore
.git/
node_modules/
db/
```

先述のとおり `db/` には、MySQL のコンテナ内のデータを保存していますので、これを更に `api` コンテナ側に保存しないようにしています。

### 2-3-2. .gitignore に１行追加

`.gitignore` ファイルにも、`/db` を追加しておきます。

```.gitignore
# compiled output
/dist
/node_modules
/db # これを追加

# 以下略
```


## 2-4. Docker の起動確認

プロジェクトのルートディレクトリに移動します。
次のコマンドで、docker を起動させます。

```
$ docker compose up -d
```

`docker ps` コマンドでコンテナが起動しているか確認します。

```
$ docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                               NAMES
78918ab18ad4   nest-docker-sample_api   "docker-entrypoint.s…"   36 seconds ago   Up 33 seconds   0.0.0.0:3000->3000/tcp              nest-docker-sample-api-1
3fdc62a476f7   mysql:8.0                "docker-entrypoint.s…"   36 seconds ago   Up 35 seconds   0.0.0.0:3306->3306/tcp, 33060/tcp   nest-docker-sample-db-1
```

確認のため、`npm run start` コマンドでサーバを起動してみます。
コンテナ内でコマンドを実行するので、頭に `docker compose exec api` を付けています（`api` は docker-compose.yml ファイルで指定した service名です。）。

```
$ docker compose exec api npm run start
```

無事に実行できれば、次のように表示されます。
![npm-run-start.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/5d72ca5c-2903-a4a7-c856-b52134479195.png)
http://localhost:3000/ にアクセスして「Hello World!」が表示されればコンテナの起動は成功です。
![HelloWorld_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/bdb9da83-6dc1-ead5-ab2d-02022c3c95da.png)
作業を続けるため、一旦、**Ctrl + C** でサーバを停止します。

# 3. VSCode Remote-Container

ここでは、VSCode Remote-Container を使用します。
特に必須の内容ではなく、コンテナ内に入ってコマンドが実行できれば良いので、不要な人は「[4. TypeORM の導入](#4-typeorm-の導入)」に進んでください。

＜**参考記事**＞
・[VSCode Remote Containerが良い](https://qiita.com/d0ne1s/items/d2649801c6f804019db7)
・[VS Code Remote - Containers を Docker Compose で使うのだー！](https://bufferings.hatenablog.com/entry/2020/06/11/233201)
・[みんながきっと１万回は聞いている、VS Code Remoteでコンテナ開発をやる方法](https://zenn.dev/niisan/articles/9abd372ae86fc1)

## 3-1. コンテナでのコマンド実行方法の確認

コンテナでのコマンド実行方法は、いくつかあります。

**① コンテナの外からコマンドを実行する場合（参考）**
コンテナに対してコマンドを実行するには、実行したいコマンドの前に、`docker compose exec <service名>` と付ければ操作できます（servise名は docker-compose.yml で指定した名称となります）。
以下は、`npm run start` を実行する例です（ここでは採用しません）。

```
$ docker compose exec api npm run start
```

**② コンテナに入ってコマンドを実行する場合（参考）**
次のコマンドで、コンテナに入れば、docker compose exec api と頭に付けなくとも操作できます（ここでは採用しません）。

```
$ docker exec -it <コンテナID> sh
```

OS（Linuxディストリビューション） で ubuntu を使用する場合は、末尾は `bash` ですが、ここでは alpine を使用しているので `sh` となっています。

**③ Remote-Container を使用する**
ここでは、手軽にコンテナに入るために、Remote-Container を使用します。
導入方法は、次項以下のとおりです。

## 3-2. Remote-Container の導入

### 3-2-1. インストール

VSCode の拡張機能として、以下の Remote-Container をインストールします。

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

### 3-2-2. VSCode でコンテナを開く

**① Remote-Container の使用**
インストールが完了すると、VSCode の左下に緑のアイコンが表示されますので、これをクリックします。
![remote1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/a39fe445-8230-999a-f6ec-47b1d7d13133.png)
**② 操作の選択**
選択肢が現れますので、Reopen in Container を選択します。
![remote2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/62e6e9f6-6d67-30ea-1a44-cde1f51df3b6.png)
**③ Remote-Container を実行するファイルを選択**
From 'docker-compose.yml' を選択します。
![remote3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/69f5ab43-db17-6209-68e8-dac1c429e365.png)
**④ service名を選択**
service 名を聞かれますので、`api` を選択します。
この service 名は、docker-compose.yml で指定した service 名となります。
![remote4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/27dd0474-1c53-47eb-d6eb-896306c8acdb.png)
**⑤ WorkSpace の選択（※自動で選択されなかった場合）**
Workspace が存在しないというメッセージが出てきたら、Open Workspace をクリックします。
（※おそらく、Dockerfile の WORKDIR と、docker-compose.yml のservice 名が同じだとこの事象が生じてしまいます。）
![remote5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/77123170-2656-6b41-8463-0e97e1ef2432.png)
フォルダーは `/api` を選択して、OK をクリックします。
この `/api` は、Dockerfile の WORKDIR で指定したディレクトリです。
![remote6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/8a6d8809-821e-1807-820b-f0a43e45aa9c.png)
初期設定は以上です。

### 3-2-3. 状態の確認と若干の補正

コンテナに入ると、次のような画面になっています。
![remote7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/8f4bb582-47d2-6292-ce5f-caf1bf32957c.png)
**① ターミナル**
入力部分が `78918ab18ad4:/api#` というようになっています。
これは `<コンテナID>:<service名>#` の形式で、ここで直接コンテナに対してコマンドを打つことができます。

```
78918ab18ad4:/api#
```

なお「78918ab18ad4:/workdir#」となる場合もありますが、そちらも正常なので問題はありません。

**② 作成されるファイル**
Remote-Containerを導入すると、ルートの直下にディレクトリ `.devcontainer` と２つのファイルが作成されます。

```
nest-docker-sample
├─ .devcontainer
│   ├── devcontainer.json
│   └── docker-compose.yml
└──(...その他のファイル)

```

**③ WorkSpace のディレクトリを修正（※自動で選択されなかった場合）**
前項の「⑤ WorkSpace の選択（※自動で選択されなかった場合）」に当てはまった場合は、次のように修正しておきます（自動でディレクトリが選択される場合は不要です）。
devcontainer.json ファイルに "workspaceFolder" を指定するプロパティがあるので、ここを "/workspace" から "/api" に書き換えます。

```json:.devcontainer/devcontainer.json（抜粋）
# 書き換え前
"workspaceFolder": "/workspace"
```

```json:.devcontainer/devcontainer.json（抜粋）
# 書き換え後
"workspaceFolder": "/api"
```

`/api` は、Dockerfile の WORKDIR で指定したディレクトリです。

### 3-2-4. Remote-Container の終了と再実行

Remote-Container を終了するには、左下の緑の部分をクリックして、`Close Remote Connection` を選択します。
![remote8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/f88b1393-cc69-a49a-8b10-db0210f9a5c3.png)
再度コンテナの中に入るには、左下の緑のアイコンをクリックして、Reopen in Container を選択します。
![remote9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/4cb340c9-e2a1-1cad-24ca-baf3ff205430.png)

## 3-3. 拡張機能（ESLint, Prettier）をコンテナに追加する

コンテナ内に拡張機能を追加するには、次のように devcontainer.json ファイルの `extensions` に拡張機能の識別子を追加します。

```json:.devcontainer/devcontainer.json（抜粋）
{
	// 略
	"extensions": [
		"dbaeumer.vscode-eslint",
		"esbenp.prettier-vscode"
	]
	// 略
}
```

拡張機能の識別子は、それぞれの拡張機能の画面の右下（下図赤枠のところ）に表示されていますので、それを使用します。
![extensions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/a9cf6416-adbd-a0ef-9b40-f56828a8def4.png)

＜**一度コンテナを立ち上げた後に拡張機能を追加した場合**＞
私の場合、ESLint の警告（赤い波線）が表示されず不便であったため、後から extensions を追加しました。
その際に、ファイルを書き換えただけでは反映されなかったため、次のことを試すことで VSCode に反映されました（２つのうちどちらか不要かもしれません）。

①`docker compose down` で一回コンテナを削除して、再度 Reopen in Container に入る。
②VSCode の Reload Window（Cmmand + shift + P で検索）で画面を再読み込みする。

＜**参考**＞
今回の設定を行うと、devcontainer.json の全体は次のようになります。

```.devcontainer/devcontainer.json
{
	"name": "Existing Docker Compose (Extend)",
	"dockerComposeFile": [
		"../docker-compose.yml",
		"docker-compose.yml"
	],
	"service": "api",
	"workspaceFolder": "/api",
	"extensions": [
		"dbaeumer.vscode-eslint",
		"esbenp.prettier-vscode"
	]
}
```

# 4. TypeORM の導入

これから行う作業が完了した後の `src` ディレクトリは、おおよそ次のようにります。

```
📦src
 ┣ 📂entities
 ┃ ┗ 📜comment.entity.ts
 ┣ 📂migration
 ┃ ┗ 📜1663429464503-CommentMigration.ts
 ┣ 📜app.controller.ts
 ┣ 📜app.module.ts
 ┣ 📜app.service.ts
 ┣ 📜data-source.ts
 ┗ 📜main.ts
```

追加していくファイルは、①`comment.entity.ts`、②`1663429464503-CommentMigration.ts`（ファイル名は毎回異なる）、③`data-source.ts` の３つです。
また、`app.module.ts` を一部修正することになります。

## 4-1. TypeORM のインストール

次のコマンドで TypeORM のバージョンを指定してインストールします。

```
# npm install --save @nestjs/typeorm typeorm@0.3.9 mysql2
```

これを書いている時点では、`typeorm@0.3.9` のところを `typeorm` というようにバージョンを指定しなくとも、同じバージョンの TypeORM がインストールされます。


次のコマンドで、インストールされた TypeORM のバージョンが確認できます。

```
# npm info typeorm version
0.3.9
```

参考までに、設定が完了した後の `package.json` を貼っておきます。
<details><summary><font color="blue">package.json</font></summary><div>

```package.json
{
  "name": "nest-docker-sample",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs/common": "^9.0.0",
    "@nestjs/core": "^9.0.0",
    "@nestjs/platform-express": "^9.0.0",
    "@nestjs/typeorm": "^9.0.1",
    "mysql2": "^2.3.3",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0",
    "typeorm": "^0.3.9"
  },
  "devDependencies": {
    "@nestjs/cli": "^9.0.0",
    "@nestjs/schematics": "^9.0.0",
    "@nestjs/testing": "^9.0.0",
    "@types/express": "^4.17.13",
    "@types/jest": "28.1.8",
    "@types/node": "^16.0.0",
    "@types/supertest": "^2.0.11",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "eslint": "^8.0.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "28.1.3",
    "prettier": "^2.3.2",
    "source-map-support": "^0.5.20",
    "supertest": "^6.1.3",
    "ts-jest": "28.0.8",
    "ts-loader": "^9.2.3",
    "ts-node": "^10.0.0",
    "tsconfig-paths": "4.1.0",
    "typescript": "^4.7.4"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}
```
</div></details>

## 4-2. Entity の作成

テーブル定義として、テスト用の Entity クラスを作成します。
以下のように、`src/entities/` ディレクトリに `comment.entity.ts` というファイルを作成しました（中身は適当です）。
これを元に、DB にテーブルが作成されることになります。

```src/entities/comment.entity.ts
import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity()
export class Comment {
  @PrimaryGeneratedColumn()
  id?: number;

  @Column()
  body: string;

  @CreateDateColumn()
  createdAt?: Date;

  @UpdateDateColumn()
  updatedAt?: Date;

  constructor(body: string) {
    this.body = body;
  }
}
```

## 4-3. DataSource の作成

TypeORM v0.3 では、データベースの接続を定義する DataSource ファイルを作成します。
TypeORM v0.2 においては、`ormconfig.js` のようなファイルを作成していました（現在は非推奨のようです。）。

`src/` ディレクトリの直下に以下のように `data-source.ts` ファイルを作成します。
設定内容は、コード中のコメントのとおりです。

```src/data-source.ts
import { Comment } from './entities/comment.entity';
import { DataSource } from 'typeorm';

export const AppDataSource = new DataSource({
  type: 'mysql', // MySQL の場合
  host: 'db', // docker-compose.yml で指定したコンテナの service 名
  port: 3306, // ポート番号
  username: 'user', // docker-compose.yml の MYSQL_USER
  password: 'password', // docker-compose.yml の MYSQL_PASSWORD
  database: 'sample', // docker-compose.yml の MYSQL_DATABASE
  logging: true, // コンソール画面に実行したSQLが表示される
  synchronize: false, // true にすると migration が自動で実行されます。
  entities: [Comment], // エンティティクラスを指定する（複数の場合はカンマで区切る）
  migrations: ['dist/migration/*.js'], // dist ディレクトリ内の js ファイルを指定する
});
```

＜**参考サイト**＞
・[npm - TypeORM](https://www.npmjs.com/package/typeorm)

## 4-4. app.module.ts の修正

DataSource ファイルを読み込むために `src/app.module.ts` を編集します。

```src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // 追加
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppDataSource } from './data-source'; // 追加

@Module({
  imports: [TypeOrmModule.forRoot(AppDataSource.options)], // 修正
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

TypeOrmModule.forRoot() の引数には、DataSource から import した AppDataSource　の option プロパティを指定します（`AppDataSource.options`）。

## 4-5. マイグレーションファイルの生成（migration:generate）

マイグレーションファイルの生成コマンド（migration:generate）も書き方が変わっています。

```
# npx typeorm-ts-node-commonjs migration:generate src/migration/CommentMigration -d src/data-source.ts
```

冒頭の `npx typeorm-ts-node-commonjs migration:generate` は共通です。
次の `src/migration/CommentMigration` は、マイグレーションファイルを作成するディレクトリ/ファイル名を指定しています。
最後の `-d src/data-source.ts` には、接続情報となる DataSource ファイルを指定しています。
この辺りは、冒頭にも紹介した[この記事](https://qiita.com/Aurum64/items/f5962bd2a643447dbef9)を参考とさせていただいています。

コマンドを実行すると、ターミナルに次のような表示がされます。
![migration_g2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/269b2779-bbc1-7e4a-bd79-ae3495cace8e.png)
無事に成功すれば、指定した `src/migration/` フォルダに以下のようなマイグレーションファイルができていると思います。

```src/migration/1663429464503-CommentMigration.ts
import { MigrationInterface, QueryRunner } from "typeorm";

export class CommentMigration1663489067218 implements MigrationInterface {
    name = 'CommentMigration1663489067218'

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`CREATE TABLE \`comment\` (\`id\` int NOT NULL AUTO_INCREMENT, \`body\` varchar(255) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
    }

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

}
```

## 4-6. マイグレーションの実行（migration:run）

### 4-6-1. 先にアプリをビルドする

dist の中身を書き換えないと、マイグレーションが実行されないので、実行前に一度ビルドします。
次のコマンドを実行します。

```
# npm run build
```

なお、ここではしませんが、`npm run start` などでサーバを立ち上げても、dist が更新されます（つまり、dist が更新できれば何でも良いです）。

### 4-6-2. migration:run

さて、いよいよ migration:run を実行します。
コマンドは、次のとおりです。

```
# npx typeorm-ts-node-commonjs migration:run -d src/data-source.ts
```

`npx typeorm-ts-node-commonjs migration:run` はコマンド実行時の共通部分です。
`-d src/data-source.ts` には、DataSource ファイルを指定します。

コマンドを実行すると、次のようにコンソール画面に表示されます。
![migration_r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/4b622fbc-52a4-b0bf-cd0c-5db365181483.png)
無事に実行できたら、サーバを立ち上げてみます。
エラーが起きなければ成功です。

```
# npm run start
```

## 4-7. DB を確認する

### 4-7-1. MySQL のコンテナに入る

テーブルが作成されているかの確認をしておきます。
Docker コンテナ内のターミナルでは実行できませんので、別途、新しいターミナルを起動させます。

docker ps コマンドで、実行中の MySQL のコンテナ ID を確認します。

```
$ docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                               NAMES
78918ab18ad4   nest-docker-sample_api   "docker-entrypoint.s…"   4 hours ago    Up 29 minutes   0.0.0.0:3000->3000/tcp              nest-docker-sample-api-1
3fdc62a476f7   mysql:8.0                "docker-entrypoint.s…"   4 hours ago    Up 30 minutes   0.0.0.0:3306->3306/tcp, 33060/tcp   nest-docker-sample-db-1
```

私の手元では、MySQL のコンテナIDは `3fdc62a476f7` なので、次のようにコマンドを実行することで、コンテナの中に入れます。

```
$ docker exec -it 3fdc62a476f7 sh
```

### 4-7-2. DB の内容を確認する

コンテナに入ったら、次のコマンドを実行して、MySQL の操作を開始します。

```
# mysql -u root -p
```

パスワードが求められるので、`password` と入力します。
いうまでもないですが、このパスワードは、docker-compose.yml の `MYSQL_ROOT_PASSWORD` で指定したものです。

無事に、MySQL に入れたら、`show databases` コマンドでデータベースの一覧を確認してみます。
次のように sample とあれば OK です。

```
> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sample             |
| sys                |
+--------------------+
5 rows in set (0.01 sec)
```

use コマンドで、sample データベースを選択します。

```
> use sample;
```

`show tables;` コマンドで、テーブル一覧を確認します。
comment というテーブルがあれば OK です。

```
> show tables;
+------------------+
| Tables_in_sample |
+------------------+
| comment          |
| migrations       |
+------------------+
2 rows in set (0.01 sec)
```

最後に、comment テーブルの定義を確認します。
`show create table <テーブル名>` コマンドを実行します。
コマンドの末尾は、`;` ではなく `\G` とした方が見やすいです。

```
> show create table comment\G
*************************** 1. row ***************************
       Table: comment
Create Table: CREATE TABLE `comment` (
  `id` int NOT NULL AUTO_INCREMENT,
  `body` varchar(255) NOT NULL,
  `createdAt` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  `updatedAt` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
```

以上の感じで、テーブルが作成されていれば成功です。

# 5. Git について

git をコンテナ内で動作させるのに苦労したため、作業記録を残しておきます。

## 5-1. git のみインストールした場合

次のように、`apk update && apk add bash git` として、git と bash をインストールするだけで、git の使用はできます。
bash をインストールするのは、これがないと git を動作させることができないためです。

```Dockerfile:Dockerfile
FROM node:16-alpine3.16
RUN apk update && \
    apk add bash git
WORKDIR /api
COPY package*.json ./
RUN npm ci
COPY . .
```

ただ、この場合、コンテナ内で git commit を実行しようとすると、次のようなエラーが生じます。

```
# git commit -m "test commit"
git: 'secrets' is not a git command. See 'git --help'.
```

これは、[git-secrets](https://github.com/awslabs/git-secrets) がコンテナ内にインストールされていないためです。

**※暫定処置**
リスクが生じますが、コンテナ内で以下のコマンドを実行すれば commit 時に git-secrets を呼び出さないようにすることができます。

```
# rm -r .git/hooks
```

git-secrets は、AWS アクセスキーの遺漏を防ぐなどセキュリティ上、大事な役割を果たすものですので、この方法はあまり好ましくないと思われます。

## 5-2. git と git-secrets をインストールする場合

git と git-secrets をインストールする場合の Dockerfile です。
試行錯誤しましたが、これで git commit や git push などのコマンドを正常に実行することができます。

```Dockerfile:Dockerfile
FROM node:16-alpine3.16
RUN apk update && \
    apk add bash git make && \
    apk add --upgrade grep
RUN git clone https://github.com/awslabs/git-secrets /home/alpine/git-secrets
WORKDIR /home/alpine/git-secrets
RUN make && make install
WORKDIR /api
COPY package*.json ./
RUN npm ci
COPY . .
```

① git-secrets をインストールするために make のインストールが必要となります。
② git commit 時に grep でエラーを吐くので、grep のアップグレードも必要となります。

＜**参考記事**＞
・[moduscreate/alpine-git-secrets](https://registry.hub.docker.com/r/moduscreate/alpine-git-secrets)
・[[alpine] sh: make: not found](https://mroldan.medium.com/alpine-sh-make-not-found-1e87ab87c56)
・[UbuntuからAlpineにイメージを移行して容量を減らす例](https://qiita.com/zembutsu/items/000fcc3c046f1af44cc5)
・[[RFC] Make it work with Alpine Linux / BusyBox grep](https://github.com/jish/pre-commit/issues/265)

# さいごに

NestJS と TypeORM v0.3 については、記事があまり見当たらないところなので、備忘として残しておきました。
Docker など、詳しい分野ではないので、間違い等があるかもしれません。
お気づきのことなどあれば、ご教示いただけると幸いです。