Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Sequelize編~

## 内容
今回はNode.jsのORMであるSequelizeを使って、ExpressサーバーからMySQLにアクセスしデータを取得・レスポンスするまでを行っていきます。

前回の記事まででExpressサーバーと簡単なAPIの作成が完了しているので、今回はSequelize導入とModel作成を中心に行っていこうと思います。

## 前回までの内容
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Vue編~](https://qiita.com/WEBERYOTA/items/b686478ec0c8c9a6bddd)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~MySQL編~](https://qiita.com/WEBERYOTA/items/6a5ef7bdfc5bd972f92b)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Express編~](https://qiita.com/WEBERYOTA/items/649cacbfb1623079b090)

## 作業手順
1. コンテナ同士のネットワークを設定
2. Sequelize 導入
3. Model作成
4. GetTests APIを完成させる
5. フロント側からAxiosでAPIをコールする
6. レスポンスをVuexにバインドして描写する

## 1. コンテナ同士のネットワークを設定
### docker-compose.yml

```yml
version: "3"
services:
  app:
    container_name: app_container
    build: ./docker/app
    ports:
      - 8080:8080
    volumes:
      - ./app:/app
    stdin_open: true
    tty: true
    environment:
      TZ: Asia/Tokyo
    command: yarn serve
    networks:                          #ネットワークを追加
      - default

  api:
    container_name: api_container
    build: ./docker/api
    ports:
      - 3000:3000
    volumes:
      - ./api:/api
    tty: true
    environment:
      CHOKIDAR_USEPOLLING: 1
      TZ: Asia/Tokyo
    depends_on:
      - db
    command: yarn nodemon
    networks:                          #ネットワークを追加
      - default

  db:
    container_name: db_container
    build: ./docker/db
    image: mysql:5.7
    ports:
      - 3306:3306
    volumes:
      - ./db/conf/my.cnf:/etc/mysql/conf.d/mysql.cnf
      - ./db/init_db:/docker-entrypoint-initdb.d
      - test_data:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - TZ="Asia/Tokyo"
    networks:                          #ネットワークを追加
      - default

networks:                              #ネットワークを追加
  default:

volumes:
  test_data:
```
共通のネットワークで接続することでそれぞれのコンポーネント間でやりとりが可能になります。

### コンテナ起動
```
$ docker-compose up -d
```

##2. Sequelize 導入
ここからはNode.js用のORM（リレーショナルデータベースからデータを取得・操作するもの）であるSequelizeを導入し、DBからデータを取得していきます。

### コンテナにアクセス
```
$ docker exec -it api_container sh
```

### 必要なライブラリをインストール
- sequelize
    - Node.js用のORM
- sequelize-typescript
    - TypeScriptベースで記述できるよう導入
- mysql2
    - Sequelize利用のために必要 
- reflect-metadata
    - デコレーター記述するために導入
- log4js
    - ログ出力するために導入
- cors
    - CORS(Cross-Origin Resource Sharing)対応するために導入
- dotenv
    - 環境変数の代わりにenvファイルから読み取るために導入

```
$ yarn add sequelize@5.21.11 sequelize-typescript reflect-metadata mysql2 log4js cors
$ yarn add --dev dotenv @types/validator @types/bluebird @types/cors @types/dotenv
```
<font color="Red">※sequelize のバージョンによってはエラーになるので、インストール時にバージョン確認が必要！</font>

### api/index.ts

```ts
import cors from 'cors' // 追加
import express from 'express'
import router from './routes/index'

const app = express()
const port = 3000

app.use(cors()) // 追加

app.use('/api', router)

app.listen(port, () => console.log(`Example app listening on port ${port}!`))
```

##3. Model 作成

### api/models/settings/.env

```
MYSQL_DATABASE=test_db
MYSQL_USER={MySQLコンテナ作成時に設定したもの}
MYSQL_PASSWORD={MySQLコンテナ作成時に設定したもの}
MYSQL_ROOT_PASSWORD={MySQLコンテナ作成時に設定したもの}
```
ここはルートディレクトリにある.envと同じ設定。

### api/models/settings/db_setting.ts

```ts
import dotenv from 'dotenv'

dotenv.config({ path: __dirname + '/.env' })

interface DatabaseTypes {
  database: string | undefined
  user: string | undefined
  password: string | undefined
}

export const dbSetting: DatabaseTypes = {
  database: process.env.MYSQL_DATABASE,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
}
```
Sequelize用のパスワードやテーブルなどの設定をしておきます。

### api/models/test.ts

```ts
import {
  Table,
  Model,
  Column,
  DataType,
  PrimaryKey,
  AutoIncrement,
} from 'sequelize-typescript'

@Table({
  modelName: 'test',
  tableName: 'test',
})
export class Test extends Model<Test> {
  @PrimaryKey
  @AutoIncrement
  @Column(DataType.INTEGER)
  readonly id!: number

  @Column(DataType.STRING)
  name!: string

  @Column(DataType.STRING)
  description!: string
}
```
test テーブル用のモデルを作成。デコレーターベースで記述。

TypeScriptのエラーが出るのでルールを追加。

### api/tsconfig.json
```json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["./**/*"]
}
```
tsconfigの設定を上記に編集

### api/models/index.ts

```ts
import log4js from 'log4js'
import { Sequelize } from 'sequelize-typescript'
import { dbSetting } from './settings/db_setting'
import { Test } from './test'

const logger = log4js.getLogger('mysql')

export default new Sequelize({
  dialect: 'mysql',
  timezone: '+09:00',
  port: 3306,
  host: 'db',
  username: dbSetting['user'],
  password: dbSetting['password'],
  database: dbSetting['database'],
  logging: (sql: string) => {
    logger.info(sql)
  },
  define: { timestamps: false, underscored: true },
  pool: { max: 5, min: 0, idle: 10000, acquire: 30000 },
  models: [Test],
})

export { Test }
```
ここが今回の肝。
エラーが出まくるとするとここなはずです。
ライブラリのバージョンが異なったり、tsconfigの設定がうまくいっていないとここでエラーがおきます。

##4. GetTests APIを完成させる
### api/routes/tests/get_tests.ts

```ts
import { Request, Response } from 'express'
import { Handler } from '../../core/handler'
import { NO_DATA_EXISTS } from '../../constants/error'
import { Test } from '../../models/index'

export class GetTests {
  handler: Handler

  constructor(req: Request, res: Response) {
    this.handler = new Handler(req, res)
  }

  /**
   * メイン処理
   */
  async main() {
    const data = await this.getTests()

    if (!data) {
      return this.handler.error(NO_DATA_EXISTS)
    }

    return this.handler.json(data)
  }

  /**
   * Testデータを全て取得
   */
  getTests() {
    return Test.findAll({
      attributes: ['id', 'name', 'description'],
    })
  }
}
```
処理フローとしては
1. main 関数がインスタンス化されて呼ばれる
2. main 関数内で getTests 関数が呼ばる
3. getTests 内で Testモデルに対し findAll 関数(データを全て取得)を走らせることでtestテーブルからデータを取得
4. getTests 関数のリターン値が存在しなければエラーを返し、存在すればjson形式でデータを返す

### 確認!
localhost:3000/api/tests にアクセス！
![スクリーンショット 2020-09-22 2.35.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/8b485056-2832-e40a-b640-2c49bf355474.png)
実際のデータがJSONで返ってくればOK！

## 指定したIDから1件取得するAPIを作成

### api/routes/tests/get_test_by_id.ts
```ts
import { Request, Response } from 'express'
import { Handler } from '../../core/handler'
import { PARAMETER_INVALID, NO_DATA_EXISTS } from '../../constants/error'
import { Test } from '../../models/index'

type Params = {
  test_id: number
}

export class GetTestById {
  handler: Handler
  params: Params

  constructor(req: Request, res: Response) {
    this.handler = new Handler(req, res)

    this.params = {
      test_id: Number(req.params.test_id),
    }
  }

  /**
   * メイン処理
   */
  async main() {
    if (!this.params.test_id) {
      return this.handler.error(PARAMETER_INVALID)
    }

    const data = await this.getTest()

    if (!data) {
      return this.handler.error(NO_DATA_EXISTS)
    }

    return this.handler.json(data)
  }

  /**
   * Testデータを全て取得
   */
  getTest() {
    return Test.findOne({
      attributes: ['id', 'name', 'description'],
      where: {
        id: this.params.test_id,
      },
    })
  }
}
```
今回は findOne関数(該当データを一件取得)内で where句による検索をかけてIDが該当したデータを取得します。

### testsController.ts
```ts
import { Router } from 'express'
import { GetTests } from '../tests/get_tests'
import { GetTestById } from '../tests/get_test_by_id' // 追加

const router = Router()

router.get('/', (req, res, next) => {
  new GetTests(req, res).main().catch(next)
})

router.get('/:test_id', (req, res, next) => {   // 追加
  new GetTestById(req, res).main().catch(next)  // 追加
})                                              // 追加

export default router
```
検索する対象のIDはURLのQueryとして GetTestById クラス に渡します。

### 確認！
localhost:3000/api/tests/1 にアクセス
![スクリーンショット 2020-09-22 2.39.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/87dff8f9-ea86-827f-b4dd-1a45f3c28db4.png)

/tests/**{ここで指定したID}** のデータが返ってくれば成功！
試しに他のIDでも確認してください。

##5. フロント側からAxiosでAPIをコールする
ここまででサーバー側は完了です。
ここからは作成したAPIを実際にフロント側から呼ぶ設定を行っていきます。

### コンテナから抜ける
```
$ exit
```

### appコンテナにアクセス
```
$ docker exec -it app_container sh
```

### Axios導入
```
$ yarn add axios
```

### app/src/utils/axios.ts
```ts
import axios from 'axios'

export const api = axios.create({
  baseURL: 'http://localhost:3000/api',
})
```
cors対策とデフォルトURL化を兼ねてモジュール定義をしておきます。
呼び出す際は api.get や api.post でコールすることが可能になります。

### app/src/store/modules/test.ts
```ts
import {
  getModule,
  Module,
  VuexModule,
  Mutation,
  Action,
} from 'vuex-module-decorators'
import store from '../index'
import { api } from '../../utils/axios'

type TestType = {
  id: number
  name: string
  description: string
}

type TestState = {
  apiTests: TestType[]
}

@Module({ store, dynamic: true, namespaced: true, name: 'Test' })
class TestModule extends VuexModule implements TestState {
  apiTests: TestType[] = []

  @Mutation
  SET_API_TESTS(payload: TestType[]) {
    this.apiTests = payload
  }
  @Action
  async getTests() {
    const response = await api.get('/tests')

    if (response.data.data) {
      this.SET_API_TESTS(response.data.data)
    }
  }
}

export const testModule = getModule(TestModule)
```
Vuexの test Store を作成し、Actionの getTests 内で作成したAPIをコールすることで取得したデータを apiTests ステートに格納します。

### app/src/pages/Test.vue

```vue

<template>
  <div class="test">
    <v-data-table :headers="headers" :items="tests"> </v-data-table>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import { testModule } from '../store/modules/test'

@Component
export default class Test extends Vue {
  tests = []

  headers = [
    { text: 'ID', align: 'center', value: 'id' },
    { text: '名前', align: 'center', value: 'name' },
    { text: '詳細', align: 'center', value: 'description' },
  ]

  async created() {
    await testModule.getTests()

    this.tests = testModule.apiTests
  }
}
</script>
```
Vueがcreateされた際に testストアの getTests アクションを走らせることでデータが取得され、apiTestsのデータをバインドすることでVue内でデータを参照します。
そのデータをVuetifyの v-table-table に渡してあげれば完了。

### 確認！
localhost:8080

![スクリーンショット 2020-09-22 2.49.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/c3f5c618-19b5-7d1c-7ff0-69abf1e1b0a9.png)

ここまでフロント側でAPIをコールし、DBからデータを取得し、返ってきたデータをVuexに格納し、それを描写することができました。
ここまででベースができたので同じ要領でAPIとページを追加していけば拡張が可能となります。

ここまででテンプレートが完成なので、次回は簡単なCLUD操作ができるアプリを作成していこうと思います！

## 次回
※ 別途作成中

## 参考
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Vue編~](https://qiita.com/drafts/b686478ec0c8c9a6bddd/edit)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~MySQL編~](https://qiita.com/WEBERYOTA/items/6a5ef7bdfc5bd972f92b)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Express編~](https://qiita.com/WEBERYOTA/items/649cacbfb1623079b090)