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:
- https://sequelize.org/master/manual/typescript
- https://karuta-kayituka.hatenablog.com/entry/2019/07/21/132814
冗長かもしれないが 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書いたほうが悩むこと少ないのでは、って気分がしてきた。