sequelize CRUD - Express

利用したパッケージ

  • connect-flash@0.1.1
  • cookie-parser@1.4.6
  • debug@2.6.9
  • ejs@3.1.10
  • express-session@1.18.0
  • express@4.19.2
  • http-errors@1.6.3
  • morgan@1.9.1
  • mysql2@3.9.4
  • sequelize@6.37.3

フォルダ構成

Express Validator で作成します。

  • example
    • bin
      • www
    • config
      • config.json
    • migrations
      • ...中略...
    • models
      • index.js
      • user.js
    • node_modules
      • ...中略...
    • public
      • images
      • javascripts
      • stylesheets
        • style.css
    • routes
      • crud.js
    • seeders
      • file
    • views
      • crud
        • add.ejs
        • delete.ejs
        • edit.ejs
        • index.ejs
        • show.ejs
    • app.js
    • package-lock.json
    • package-json

インストールコマンド

npm install mysql2
npm install sequelize
npm install -g sequelize-cli
npm install connect-flash
npm install express-session

sequelize の作業

次のコマンドを実行し、sequelize の初期化をします。

sequelize init

config/config.json が作成されるため、データベースの設定を自身の環境に合わせて編集します。

設定が終われば、次のコマンドを実行しデータベースを作成します。

sequelize db:create

データベースが作成されたら、次のコマンドを実行しモデルを作成します。

sequelize model:create --underscored --name user --attributes "name:string,age:integer"

models ディレクトリの中に、user.js というファイルが作成されます。

最後に次のコマンドを実行しマイグレーションをします。

sequelize db:migrate

ファイルの内容

app.js

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const session = require("express-session"); // +
const flash = require('connect-flash'); // +

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var crudRouter = require('./routes/crud'); // +

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// +
app.use(session({
  secret: "secret",
  resave: false,
  saveUninitialized: true,
}));
app.use(flash()); // +

 // +
app.use((req, res, next) => {
  res.locals.messages = req.flash();
  next(); 
});


app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/crud', crudRouter); // +

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

routes/crud.js

var express = require('express');
var router = express.Router();
var db = require('../models/');

router.get('/', function(req, res, next) {
  db.user.count().then((total) => {
    db.user.findAll().then(users => {
      res.render('crud/index', { users: users, count: total});
    });    
  });
});

router.get('/add', function(req, res, next) {
  res.render('crud/add');
});

router.post('/add', function(req, res, next) {
  console.log(req.body)
  db.user.create({
    name: req.body.name,
    age: req.body.age
  }).then((result) => {
    console.log(result);
    req.flash("success", "登録されました。");
    res.redirect('/crud');
  });
});

router.get('/show/:id', function(req, res, next) {
  db.user.findOne({
    where: { id: req.params.id }
  }).then((user) => {
    console.log(user);
    res.render('crud/show', { user: user });
  });
});

router.get('/edit/:id', function(req, res, next) {
  db.user.findOne({
    where: { id: req.params.id }
  }).then((user) => {
    console.log(user);
    res.render('crud/edit', { user: user });
  });
});

router.post('/edit', function(req, res, next) {
  db.user.update(
    { name: req.body.name, age: req.body.age},
    { where: { id: req.body.id} }
  ).then((result) => {
    console.log(result);
    req.flash("success", "更新されました。");
    res.redirect(`/crud/edit/${req.body.id}`);
  });
});

router.get('/delete/:id', function(req, res, next) {
  db.user.findOne({
    where: { id: req.params.id }
  }).then((user) => {
    console.log(user);
    res.render('crud/delete', { user: user });
  });
});

router.post('/delete', function(req, res, next) {
  db.user.destroy(
    { where: { id: req.body.id }}
  ).then(() => {
    req.flash("success", "削除されました。");
    res.redirect('/crud');
  });
});

module.exports = router;

views

views/crud/add.ejs

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div>
    <h1>CRUD Add</h1>
    <p><a href="/crud">ページトップへ</a></p>
    <form method="POST" action="/crud/add" >
      <p><label for="name_form">名前:<input type="text" name="name" id="name_form"></label></p>
      <p><label for="age_form">年齢:<input type="number" name="age"></label></p>
      <p><input type="submit" value=" 送信 " /></p>
    </form>
  </div>
</body>
</html>

views/crud/delete.ejs

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div>
    <h1>CRUD - Delete</h1>
    <p><a href="/crud">ページトップへ</a></p>
    <table>
      <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
      </tr>
      <tr>
        <td><%= user["id"] %></td>
        <td><%= user["name"] %></td>
        <td><%= user["age"] %></td>
      </tr>
    </table>
    <form method="POST" action="/crud/delete" >
      <input type="hidden" name="id" value="<%= user['id'] %>">
      <p><input type="submit" value="削除" /></p>
    </form>
  </div>
</body>
</html>

views/crud/edit.ejs

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div>
    <h1>CRUD - Edit</h1>
    <p><a href="/crud">ページトップへ</a></p>
    <% if(messages.success){ %>
      <p><%= messages.success %></p>
    <% } %>
    <form method="POST" action="/crud/edit" >
      <input type="hidden" name="id" value="<%= user['id'] %>">
      <p>ID:<%= user['id'] %></p>
      <p><label for="name_form">名前:<input type="text" name="name" id="name_form" value="<%= user['name'] %>"></label></p>
      <p><label for="age_form">年齢:<input type="number" name="age" value="<%= user['age'] %>"></label></p>
      <p><input type="submit" value=" 送信 " /></p>
    </form>
  </div>
</body>
</html>

views/crud/index.ejs

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="/stylesheets/style.css">
  <title>Document</title>
</head>
<body>
  <div>
    <h1>CRUD index</h1>
    <p><a href="/crud/add">新規追加</a></p>
    <% if(messages.success){ %>
      <p><%= messages.success %></p>
    <% } %>
    <p>レコード総数:<%= count %></p>
    <table>
      <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
        <th>action</th>
      </tr>
    <% for(let i of users){ %>
      <tr>
        <td><%= i["id"] %></td>
        <td><a href="/crud/show/<%= i['id'] %>"><%= i["name"] %></a></td>
        <td><%= i["age"] %></td>
        <td><a href="/crud/edit/<%= i['id'] %>">変更</a><a href="/crud/delete/<%= i['id'] %>">削除</a></td>
      </tr>
    <% } %>
    </table>
  </div>
</body>
</html>

views/crud/show.ejs

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>CRUD - Show</title>
</head>
<body>
  <div>
    <h1>CRUD - Show</h1>
    <p><a href="/crud">ページトップへ</a></p>
    <table>
      <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
      </tr>
      <tr>
        <td><%= user["id"] %></td>
        <td><%= user["name"] %></td>
        <td><%= user["age"] %></td>
      </tr>
    </table>
    <p><a href="/crud/edit/<%= user['id'] %>">変更ページへ</a></p>
    <p><a href="/crud/delete/<%= user['id'] %>">削除ページへ</a></p>
  </div>
</body>
</html>

楽観的ロックの確認

楽観的ロックを追加し動作するかの確認です。

まずモデルに「 version: true 」を追加します。

models/user.js

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class user extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
    }
  }
  user.init({
    name: DataTypes.STRING,
    age: DataTypes.INTEGER
  }, {
    sequelize,
    modelName: 'user',
    underscored: true,
    version: true, // +
  });
  return user;
};

一度、データベースの usersテーブルを削除し、sequelize db:migrate を再度実行します。

とりあえずテーブルにデータを1つ投入します。

マイグレーションが終わった後、「 routes/crud.js 」の edit に関するルーティングを次のように変更します。

routes/crud.js

router.get('/edit/:id', function(req, res, next) {
  try {
    db.user.findOne({
      where: { id: req.params.id }
    }).then((user) => {
      res.render('crud/edit', { user: user });
    }).catch((error => {
      req.flash("errors", "問題が発生しました。");
      res.redirect('/crud');
    }));
  } catch(error) {
    req.flash("errors", "問題が発生しました。");
    res.redirect('/crud');
  }
});

router.post('/edit', function(req, res, next) {
  (async function(){
    let transaction = await db.sequelize.transaction();
    try {
      const user = await db.user.findOne({
          where: {
            id: req.body.id
          }
      }, { transaction: transaction });
      console.log(`age: ${user.age} version: ${user.version}`);
      user.name = req.body.name;
      user.age = req.body.age;
      user.dataValues.version = req.body.version;
      await user.save({ transaction: transaction });
      transaction.commit();
      transaction = null;
      req.flash("success", "更新されました。");
      res.redirect(`/crud/edit/${req.body.id}`);
    } catch(error) {
      console.log(error); // ※  OptimisticLockError [SequelizeOptimisticLockError]: Attempting to update a stale model instance: user
      if (transaction) {
        transaction.rollback();
      }
      req.flash("errors", "更新できませんでした。");
      res.redirect('/crud');
    }
  })();
});

「 views/crud/edit.ejs 」を次のようにします。

views/crud/edit.ejs

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div>
    <h1>CRUD - Edit</h1>
    <p><a href="/crud">ページトップへ</a></p>
    <% if(messages.success){ %>
      <p><%= messages.success %></p>
    <% } %>
    <form method="POST" action="/crud/edit" >
      <input type="hidden" name="id" value="<%= user['id'] %>">
      <!-- ↓ 変更箇所 -->
      <input type="text" name="version" value="<%= user['version']%>" >
      <p>ID:<%= user['id'] %></p>
      <p><label for="name_form">名前:<input type="text" name="name" id="name_form" value="<%= user['name'] %>"></label></p>
      <p><label for="age_form">年齢:<input type="number" name="age" value="<%= user['age'] %>"></label></p>
      <p><input type="submit" value=" 送信 " /></p>
    </form>
  </div>
</body>
</html>

別々のウィンドウで、同じデータの変更のページを開くと「 name="version" 」のテキストエリアに同じ数字が入っています。

まず、1つのウィンドウの更新作業を行います。次に、もう一つのウィンドウも更新をしてみます。

2つ目のウィンドウで更新作業をするとコンソールに「 OptimisticLockError [SequelizeOptimisticLockError]: Attempting to update a stale model instance: user 」と表示されていれば、楽観的ロックは機能しています。