Node.jsの代表的なO/RMであるSequelizeの第五弾。
これまで以下のような内容を取り上げてきました。
今回はここまでの情報の整理も兼ねて、簡単なToDoアプリを作ってみようと思います。
最終的なコード
GitHubで公開しています。手っ取り早く確認したい方はこちらからどうぞ。 github.com
準備
MySQL
MySQLをインストールし適当なユーザー、データベースを作成しておきます。第1回目の冒頭に極簡単な説明があります。 blog.katsubemakito.net
Node.js
Node.jsを入れたら、適当なディレクトリを作成し中に移動。
$ mkdir todo; cd todo
npm init
でpackage.jsonを生成します。
$ npm init
その後必要なライブラリをnpmでインストールします。今回はNode.jsで簡単にWebサーバを構築できるexpressを利用してAPIサーバを構築します。
$ npm install express sequelize mysql2
$ npm install -D sequelize-cli
初期ファイルを準備
まずはテンプレをコマンドで自動生成します。sequelize-cliで一発です。
$ npx sequelize-cli init
「config/config.json」にMySQLへの接続情報を記入しておきます。デフォルトではdevelopment環境が選択されますので特にこだわりがなければdevelopment内の値を編集します。
{ "development": { "username": "root", "password": null, "database": "database_development", "host": "127.0.0.1", "dialect": "mysql" }, "test": { "username": "root", "password": null, "database": "database_test", "host": "127.0.0.1", "dialect": "mysql" }, "production": { "username": "root", "password": null, "database": "database_production", "host": "127.0.0.1", "dialect": "mysql" } }
あとこのファイルはJavaScriptではなく「JSON」であり文法がかなり厳しくチェックされますのでご注意ください(詳しくは第4回にて)
設計する
API設計
今回はSequelizeでCRUDの練習をするのが目的なので、以下の5種類のAPIを準備することにします。
機能 | END POINT | 引数 | レスポンス |
---|---|---|---|
カテゴリ一覧を返却 | GET /api/category | なし |
[ { id: 1, name: "カテゴリ名" } ] |
タスク一覧を返却 | GET /api/task | なし |
[ { id: 1, title: "タイトル", done: false, Category: { id: 1, name: "仕事" } } ] |
タスクを新規追加 | POST /api/task/new |
{ title: "タイトル", category: 1 } |
{status:true} |
タスクを完了 | POST /api/task/done |
{id:1} |
{status:true} |
タスクを物理削除 | POST /api/task/remove |
{id:1} |
{status:true} |
テーブル設計
今回はシンプルにToDoのタスクを登録する「Tasks」とToDoのカテゴリを管理する「Categories」の2つのテーブルを用意します。
Tasks.categoryId = Categories.id
が「n:1」の関係性になっています。
コーディング
モデルの準備
モデルのテンプレを生成します。sequelize-cliをテーブルの数だけ実行します。細かいところは直接コードを書きますのでここではカラム名とデータ型を羅列するだけでOKです。
まずはTask用テーブル。
$ npx sequelize-cli model:generate --name Task \ --attributes title:string,done:boolean
Category用のテーブルです。コマンドを実行する順番はどちらが先でもかまいません。
$ npx sequelize-cli model:generate --name Category \ --attributes name:string
model/category.jsを編集
まずは簡単な方から。タイムスタンプ機能をOFFにしました。それ以外の処理その物はいじっていません
'use strict'; const { Model } = require('sequelize'); module.exports = (sequelize, DataTypes) => { class Category extends Model { static associate(models) { // 必要があればここにテーブルの関連付けを書く // メソッド自体は削除しない } }; Category.init({ name: DataTypes.STRING }, { sequelize, modelName: 'Category', timestamps: false }); return Category; };
model/task.jsを編集
クラス内にメソッドとテーブルの関連付けの設定を追加しました。またコメントの削除と改行位置を変更しています。
'use strict'; const { Model } = require('sequelize'); module.exports = (sequelize, DataTypes) => { class Task extends Model { // テーブルの関連付け static associate(models) { Task.belongsTo(models.Category, { foreignKey: 'categoryId', // デフォルト値なので未指定でも可 targetKey: 'id' // 〃 }) } /** * タスク一覧を返却 (C"R"UD) */ static async getAll(){ try{ const tasks = await this.findAll({ include: 'Category', attributes: ['id', 'title', 'done', 'Category.name'], order: [ ['id', 'ASC'] ] }) return(tasks) } catch(e){ console.error(e) return(false) } } /** * タスクを追加 ("C"RUD) */ static async add(value){ try{ const task = await this.create({ title: value.title, categoryId: value.category, }) return(task) } catch(e){ console.error(e) return(false) } } /** * ステータスを変更する (CR"U"D) */ static async done(id, flag=true){ try{ await this.update({done: flag}, { where:{ id } }) return(true) } catch(e){ console.error(e) return(false) } } /** * タスクを物理削除 (CRU"D") */ static async remove(id){ try{ await this.destroy({ where:{ id } }) return(true) } catch(e){ console.error(e) return(false) } } } Task.init({ title: DataTypes.STRING, done: { type: DataTypes.BOOLEAN, defaultValue: false } }, { sequelize, modelName: 'Task', }); return Task; }
express側の準備
先ほど作成したモデルをexpress側から利用してAPIサーバを構築します。本題とずれるのでexpressについての説明は端折りますが、詳しくは過去記事をご覧ください。 blog.katsubemakito.net
serve.jsを編集
APIサーバを構築します。package.jsonと同じ階層にserve.jsというファイル名でコードを書いていきます。
個別にモデルをrequireしなくても、models/index.js
をrequireするだけで好きなタイミングで好きなモデルを利用できるのがわかります。Validationや厳密なエラー処理は行っていないので本番サービスでは適宜追加してください。
/** * ToDoサーバ */ //--------------------------------------------------------- // modules //--------------------------------------------------------- const models = require('./models') const path = require('path') //--------------------------------------------------------- // define //--------------------------------------------------------- const PORT = 3000 const DOCUMENT_ROOT = path.join(__dirname, 'public') //--------------------------------------------------------- // express //--------------------------------------------------------- //----------------- // サーバー設定 //----------------- const express = require('express') const app = express() app.use(express.json()) app.use(express.urlencoded({ extended: true })) app.use(express.static(DOCUMENT_ROOT)) //----------------- // API準備 //----------------- /** * カテゴリー一覧を返却 */ app.get('/api/category', async (req, res) =>{ const category = await models.Category.findAll() res.json(category) }) /** * タスク一覧を返却 */ app.get('/api/task', async (req, res) =>{ res.json( await models.Task.getAll() ) }) /** * タスクを新規追加 */ app.post('/api/task/new', async (req, res) =>{ const result = await models.Task.add({ title: req.body.title, category: req.body.category }) res.json( {status: result !== false }) }) /** * タスクを完了 */ app.post('/api/task/done', async (req, res) =>{ const result = await models.Task.done(req.body.id) res.json( {status: result !== false }) }) /** * タスクを物理削除 */ app.post('/api/task/remove', async (req, res) =>{ const result = await models.Task.remove(req.body.id) res.json( {status: result !== false }) }) //----------------- // サーバー起動 //----------------- app.listen(PORT, () => { console.log(`listening at http://localhost:${PORT}`); });
public/index.htmlを編集
APIを利用するクライアントは今回はWebページとして用意します。本題とずれるので詳しくはGitHub上のソースをご覧ください。普通にFetch APIで呼んでるだけです。 github.com
環境構築
マイグレーション
MySQLへ反映する内容を準備します。
migrations/20210810xxxxxx-create-task.jsを編集
Tasksテーブルの内容を定義します。ほぼそのままですが外部キー制約を付けているところが一番大きな変更でしょうか。
'use strict'; module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.createTable('Tasks', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, title: { type: Sequelize.STRING, allowNull: false }, done: { type: Sequelize.BOOLEAN, allowNull: false, default: false }, categoryId:{ type: Sequelize.INTEGER, references:{ model: { tableName: 'Categories', key: 'id' } } }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: async (queryInterface, Sequelize) => { await queryInterface.dropTable('Tasks'); } };
migrations/20210810xxxxxx-create-category.js
こちらはいじっていません。
'use strict'; module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.createTable('Categories', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, name: { type: Sequelize.STRING } }); }, down: async (queryInterface, Sequelize) => { await queryInterface.dropTable('Categories'); } };
実行する
では先ほど作成したファイルを元にMySQLへ反映しちゃいます。これもコマンド一発です。development環境へ2つのファイルが実行されたのが分かります。
$ npx sequelize-cli db:migrate Loaded configuration file "config/config.json". Using environment "development". == 20210810093257-create-category: migrating ======= == 20210810093257-create-category: migrated (0.049s) == 20210810095805-create-task: migrating ======= == 20210810095805-create-task: migrated (0.040s)
実際にMySQLへログインするとテーブルが3つ作成されています。SequelizeMetaテーブルには実行したマイグレーションファイルのファイル名が記録されます。
mysql> show tables;
+--------------------------------+
| Tables_in_database_development |
+--------------------------------+
| Categories |
| SequelizeMeta |
| Tasks |
+--------------------------------+
DESCでテーブルの構造も確認しておきます。Tasksの方にはid、タイムスタンプ機能用のカラム(createdAt,updatedAt)、CategoriesとリレーションするためのcategoryIdが自動的に生成されているのが分かりますね。
mysql> desc Categories; +-------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | +-------+--------------+------+-----+---------+----------------+ mysql> desc Tasks; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | title | varchar(255) | NO | | NULL | | | done | tinyint(1) | NO | | NULL | | | categoryId | int(11) | YES | MUL | NULL | | | createdAt | datetime | NO | | NULL | | | updatedAt | datetime | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+
DESCだけではなく、SHOW CREATE TABLE
でCREATE TABLE文を直接確認しておくのも良いと思います。
mysql> show create table Tasks; CREATE TABLE `Tasks` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `done` tinyint(1) NOT NULL, `categoryId` int(11) DEFAULT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, PRIMARY KEY (`id`), KEY `categoryId` (`categoryId`), CONSTRAINT `tasks_ibfk_1` FOREIGN KEY (`categoryId`) REFERENCES `Categories` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
初期データを作成
このままだとカテゴリが空っぽなので、初期データを突っ込んであげます。初期データはSequelizeでは「seed」と呼びます.
テンプレを生成
初期ファイルをsequelize-cliで生成します。
$ npx sequelize-cli seed:generate --name category
seeders/20210810xxxxxx-category.jsを編集
生成されたファイルに具体的にどういったデータを挿入したいか定義します。マイグレーションのときとほぼ同じ仕組みっぽいですが、こちらはデータの挿入に特化する感じです。
'use strict'; module.exports = { up: async (queryInterface, Sequelize) => { return queryInterface.bulkInsert('Categories', [ {name: '仕事'}, {name: 'プライベート'}, {name: '町内会'} ]); }, down: async (queryInterface, Sequelize) => { return queryInterface.bulkDelete('Categories', null, {truncate:true}); } };
実行する
先ほど作成したseedファイルをMySQLへ反映します。
$ npx sequelize-cli db:seed:all
MySQLへログインし本当に挿入されているか確認しておきます。大丈夫そうですね。
mysql> select * from Categories; +----+--------------------+ | id | name | +----+--------------------+ | 1 | 仕事 | | 2 | プライベート | | 3 | 町内会 | +----+--------------------+
アプリを使ってみる
APIサーバを起動
serve.jsをnodeコマンドで実行するだけです。3000番ポートを監視しリクエストが来ると反応します。
$ node serve.js listening at http://localhost:3000
Sequelize経由でMySQLへクエリが投げられると、その際のSQLがTerminalに表示されるので、意図したSQLが実行されているか確認することもできます。
コードを修正した場合は一度Ctrl+Cで終了し、再度起動する必要があります。開発中など頻繁に再起動する必要がある場合はnodemonをインストールしておくと、ファイルが更新されると自動的に再起動してくれるので楽ちんです。
Webブラウザから利用する
お好みのWebブラウザを起動しhttp://localhost:3000
へアクセスすると利用できます。