ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

Next.js に sequelize 導入(メモ)

手順をあとで振り返れるように自分用メモ。

1. clone

を起点とする。リポジトリを clone したのち npm i

2. create database

Docker のこと考えると面倒なので、まずはローカルの MySQL を使うようにするところから。
mysql -u root -p で入った後に

create database next_js_mysql;

3. npm i sequelize mysql2

npm i sequelize mysql2

Ref: https://sequelize.org/master/manual/getting-started.html

4. npm i -D sequelize-cli

Ref: https://sequelize.org/master/manual/migrations.html

npm i -D sequelize-cli
npx sequelize-cli init

config/config.json , models/index.js が作成される。

5. index.js の TS 化

いちおう元の index.js を尊重した上で TypeScript 化するとこんな感じ?

import fs from 'fs';
import path from 'path';
import Sequelize from 'sequelize';

const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
let db = {};

let sequelize: Sequelize.Sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize.Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize.Sequelize(config.database, config.username, config.password, config);
}

fs
  .readdirSync(__dirname)
  .filter(file => {
    return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
  })
  .forEach(file => {
    const model = sequelize['import'](path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

const sequelizeHash = {
  sequelize: sequelize,
  Sequelize: Sequelize,
}

db = { ...db, ...sequelizeHash }

module.exports = db;

6. config.json の更新

development の password と database を更新。

後でわかることだが、 operatorsAliases オプションは deprecated らしいので削除。

7. Article.ts の追加

Ref:

冗長かもしれないが models/Article.ts をこう記述した。

import { Sequelize, Model, DataTypes } from 'sequelize';

export default class Article extends Model {
  public id!: number;
  public title!: string;
  public body!: string;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  public static initialize(sequelize: Sequelize) {
    this.init(
      {
        id: {
          type: DataTypes.BIGINT.UNSIGNED,
          primaryKey: true,
          autoIncrement: true,
          allowNull: false,
        },
        title: {
          type: DataTypes.STRING(100),
          allowNull: false,
          defaultValue: '',
        },
        body: {
          type: DataTypes.TEXT,
          allowNull: false,
          defaultValue: '',
        }
      }, {
        tableName: 'article',
        sequelize: sequelize,
      }
    );
    return this;
  }
}

8. index.ts の更新

https://karuta-kayituka.hatenablog.com/entry/2019/07/21/132814 を参考にしながら以下のように更新。

import Sequelize from 'sequelize';
import Article from './Article';

const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];

let sequelize: Sequelize.Sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize.Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize.Sequelize(config.database, config.username, config.password, config);
}

const db = {
  Article: Article.initialize(sequelize),
};

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

export default db;

9. migration file の作成

npx sequelize-cli model:generate --name Article --attributes title:string,body:text で最初作ってみたが、
思っていたのと違うものが出来てしまったので、
migrations/20200502224547-create-article.js は大きく変えて以下のようにした。
collate を指定してるところが地味に大事)

'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('article', {
      id: {
        type: Sequelize.BIGINT.UNSIGNED,
        primaryKey: true,
        allowNull: false,
        autoIncrement: true,
      },
      title: {
        type: Sequelize.STRING(100),
        allowNull: false,
        defaultValue: '',
      },
      body: {
        type: Sequelize.TEXT,
        allowNull: false,
        defaultValue: '',
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
      }
    }, {
      charset: 'utf8mb4',
      collate: 'utf8mb4_bin',
    });
  },
  down: (queryInterface) => {
    return queryInterface.dropTable('article');
  }
};

migrate は以下のコマンドで実行。

npx sequelize-cli db:migrate

dry-run できなくてどんなテーブルが作られるか実行後までわからないのは、なかなかひどい仕様だと思う。
migrate だけ ridgepole なり sqldef なり使うのはアリな気がする。

10. API の作成

テーブルが用意できたのであとは API の実装。

pages/api/article/list.ts に以下のように書いた。

import { NextApiRequest, NextApiResponse } from 'next'
import models from '../../../models';

export default (_: NextApiRequest, res: NextApiResponse) => {
  const listArticles = async () => {
    return models.Article.findAll();
  }

  listArticles()
    .then(articles => res.status(200).json({ articles: articles }))
    .catch(() => res.status(500).json({ error: '500: Exception caught' }));
}

pages/api/article/create.ts は以下。

import { NextApiRequest, NextApiResponse } from 'next'
import models from '../../../models';

export default (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'POST' && req.headers["content-type"] == 'application/json') {
    const request = req.body;

    const createArticle = async () => {
      return await models.Article.create({
        title: request.title,
        body: request.body,
      });
    }

    if (!request.title || !request.body) {
      return res.status(400).json({ error: '400: Missing parameter' });
    }

    if (request.title.length > 100) {
      return res.status(400).json({ error: '400: title length must be lower than 100 chars' });
    }

    createArticle()
      .then(article => res.status(200).json({ article: article }))
      .catch(() => res.status(500).json({ error: '500: Exception caught' }));
  }
}

create したあとの id が取得できないのが課題。

これで

curl -X POST -H 'Content-Type: application/json' http://localhost:8013/api/article/create -d '{"title": "タイトル", "body": "これが本文です"}'

のような形で作成ができ、

curl http://localhost:8013/api/article/list

で一覧取得できる。

> curl -X POST -H 'Content-Type: application/json' http://localhost:8013/api/article/create -d '{"title": "タイトル", "body": "これが本文です"}'
{"article":{"id":null,"title":"タイトル","body":"これが本文です","updatedAt":"2020-05-03T01:21:34.264Z","createdAt":"2020-05-03T01:21:34.264Z"}}
> curl -X POST -H 'Content-Type: application/json' http://localhost:8013/api/article/create -d '{"title": "タイトル2", "body": "これが本文です2"}'
{"article":{"id":null,"title":"タイトル2","body":"これが本文です2","updatedAt":"2020-05-03T01:21:38.361Z","createdAt":"2020-05-03T01:21:38.361Z"}}
> curl http://localhost:8013/api/article/list
{"articles":[{"id":1,"title":"タイトル","body":"これが本文です","createdAt":"2020-05-03T01:21:34.000Z","updatedAt":"2020-05-03T01:21:34.000Z"},{"id":2,"title":"タイトル2","body":"これが本文です2","createdAt":"2020-05-03T01:21:38.000Z","updatedAt":"2020-05-03T01:21:38.000Z"}]}

11. 感想

db migrate 周りがきつい。
Rails の Schemafile と違ってけっこう文字数多くて打ちにくいし、
migration file と Article.ts で似たようなことを書かないといけないところがなかなかにきつい。

sequelize を使うよりも、db migrate は ridgepole なり sqldef を使うとして、
ふつうにSQL書いたほうが悩むこと少ないのでは、って気分がしてきた。