2
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

内容

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

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

前回までの内容

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

作業手順

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

1. コンテナ同士のネットワークを設定

docker-compose.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

※sequelize のバージョンによってはエラーになるので、インストール時にバージョン確認が必要!

api/index.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

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

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

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["./**/*"]
}

tsconfigの設定を上記に編集

api/models/index.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

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
実際のデータがJSONで返ってくればOK!

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

api/routes/tests/get_test_by_id.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

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

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

5. フロント側からAxiosでAPIをコールする

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

コンテナから抜ける

$ exit

appコンテナにアクセス

$ docker exec -it app_container sh

Axios導入

$ yarn add axios

app/src/utils/axios.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

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


<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

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

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

次回

※ 別途作成中

参考

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
2
Help us understand the problem. What are the problem?