Sequelize入門 Node.jsとExpressからPostgreSQLを使う

こんにちは。レモンティーです。
今回はNode.jsで使えるORMのSequelizeを使います。
github.com

導入

まずプロジェクトを作成して必要なモジュールをインストールします。

express --view=ejs sample
cd sample
npm install

さらにSequelizeとpgをインストール。この時、モデルの追加やmigrationが簡単にできるCLIも一緒にインストールします。

npm install --save sequelize pg sequelize-cli

さっそくCLIを使って必要なディレクトリ等を生成します。

npx sequelize init

これでSequelizeが使うconfigファイルや、これからモデルやmigration情報が追加されるディレクトリができます。

環境を設定

config.jsonファイルには環境ごとに使うデータベースを設定できます。今回はPostgreSQLを使い、username等は環境変数から取得するのでdevelopmetの部分を以下のようにします。(productionもこれと同じ設定にするとHerokuPostgreをそのまま使えます。)

  "development": {
    "dialect": "postgres",
    "use_env_variable":"DATABASE_URL"
  },

続いて環境変数DATABASE_URLを設定しておきます。
DATABASE_URLは次のような形式で指定します

postgres://username:password@host:port/dbname

なのでpowershellなら

$env:DATABASE_URL="postgres://username:password@host:port/dbname"

を実行すればOKです。これがdevelopmentで適用されます。こうして指定した環境変数はターミナルを終了すると消えてくれるので開発後にゴミが残りません。

デフォルトではpostgreSQLは5432番のポートを使うので、例えばsampleという名前のデータベースをつくった場合は以下のようになります。

$env:DATABASE_URL="postgres://username:password@localhost:5432/sample"

モデル作成

続いてモデルとmigration用のファイルを作成します。これもCLIでできます。

今回は人間とペットのモデルをつくります。

npx sequelize model:create --name Human --attributes name:string,age:integer
npx sequelize model:create --name Pet --attributes name:string,kind:string

migration

migrationして実際にデータベースにテーブルを作成します。

npx sequelize db:migrate --env development

そのままだと全ての環境で実行しようとするので--envオプションでdevelopmentのみにしています。

CRUD操作

テーブルができたので簡単な操作をしてみます。(公式)
まずはmodel/index.jsをインポートします。

var models = require("../models");

Sequelizeでは簡単なCRUD操作に便利なメソッドがあり、プロミスで書けます。メソッドにはcreate, findAll, findOne, findByPK, update, destroyなどがあります。update ,destroy ,findOneなどで対象を絞り込む時には引数のオプションオブジェクトのwhereプロパティに{id:3}のような条件のオブジェクトを渡します。また、findByPKのPKとはPrimaryKeyのことで、PKがidなら引数にfindByKey(123)のように数値を入れる...という感じになります。

sample

今回はサンプルなのでroutes/index.jsに無理矢理以下のように書いてしまいます。また、バリデーション等もしていません。Sequelizeに関することだけ書いています。

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

/* GET home page. */
router.get('/', function(req, res, next) {
  models.Human.findAll().then(humans => {
    res.render("index",{humans:humans});
  });
});

router.post('/human/add',(req, res, next) => {
  models.Human.create({
    name:req.body.name,
    age:req.body.age
  }).then(newUser => {
    console.log(newUser);
    res.redirect("/");
  });
});

router.post('/human/destroy',(req, res, next) => {
  models.Human.destroy({
    where:{id:req.body.id}
  }).then(deletedUser => {
    console.log(deletedUser);
    res.redirect("/");
  })
});

module.exports = router;

また、それに合わせてviews/index.ejsも以下のように変更します。

<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1>Sample</h1>
    <p>Welcome to Sample</p>

    <h2>Human Table</h2>
    <table>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Age</th>
      </tr>
      <% for(var human of humans) {%>
        <tr>
          <td><%= human.id %></td>
          <td><%= human.name %></td>
          <td><%= human.age %></td>
        </tr>
      <% } %>
    </table>

    <h2>Create Human</h2>
    <form action="/human/add" method="POST">
      <input type="text" name="name" placeholder="name">
      <input type="number" name="age" placeholder="age">
      <input type="submit" value="Create">
    </form>

    <h2>Destroy Human</h2>
    <form action="/human/destroy" method="POST">
      <input type="number" name="id" placeholder="id">
      <input type="submit" value="Destroy">
    </form>
  </body>
</html>

これで実行してみましょう。

npm start

で実行できます。

うまくいけば以下のように表示され、ユーザーを追加したり削除したりできます。

f:id:sawalemontea:20190401224041p:plain
express sequelize postgreSQL test result

Association

リレーショナルデータベースではその名の通り複数のテーブルをキーとなるカラムやテーブルを使って関連づけることができますが、Sequelizeではこの機能もAssociationを利用することでシンプルに扱えます。
medium.com

今回はHumanとPetにOneToManyの関係を持たせてみます。
Sequelizeでは先ほどコマンドでつくったモデルファイル(modelsディレクトリにあります)にAssociationを定義します。

Human

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Human = sequelize.define('Human', {
    name: DataTypes.STRING,
    age: DataTypes.INTEGER
  }, {});
  Human.associate = function(models) {
    // associations can be defined here
    models.Human.hasMany(models.Pet);
  };
  return Human;
};

コメントの直下の1行を追加するだけです。

Pet

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Pet = sequelize.define('Pet', {
    name: DataTypes.STRING,
    kind: DataTypes.STRING
  }, {});
  Pet.associate = function(models) {
    // associations can be defined here
    models.Pet.belongsTo(models.Human);
  };
  return Pet;
};

こちらも同様です。

migrationの更新

ただしもう一つ作業があります。これだけで実行されるSQL文のforeignKeyの名前までは自動でHumanIdに決まるのですが、そのカラムの追加は自分でやる必要があります。すでにmigrationしたテーブルの更新は、CLIで生成するmigrationファイルに更新内容を書き込んでもう一度migrateすることで実現します。

まずmigrationファイルを生成

npx sequelize migration:create --name add-HumanId-pet

その内容を次のように変えます。upにmigrateで加えたい変更内容、downにdb:migrate:undoでそのmigrationをやり直すときの変更内容を書きます。これでPetsテーブルにHumanIdカラムを追加します。

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    /*
      Add altering commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.createTable('users', { id: Sequelize.INTEGER });
    */
   return queryInterface.addColumn("Pets","HumanId",{type:Sequelize.INTEGER});
  },

  down: (queryInterface, Sequelize) => {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.dropTable('users');
    */
    return queryInterface.removeColumn("Pets","HumanId");
  }
};

あとはもう一度先ほどと同様にmigrateすればAssociationが使えるようになりました。
Associationの使い方は簡単で、findするときにオプションオブジェクトのincludeプロパティに一緒に取得したいモデルを指定するだけです。

こんな感じです。

models.Hoge.findAll({
  include:[models.Huga]
}).then(hoges => {
  //これでhoges.Hugasに関連付けられたHugaの配列がはいっています
})

(ただしMany側からOneを取得する時はHugaは複数形にならずHugaのままです。)

sample

さっそくroutes/index.jsとviews/index.ejsをそれぞれ次のように書き換えて試してみましょう。サンプルなので全部index.jsに書きます。

index.js

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

/* GET home page. */
router.get('/', function(req, res, next) {
  models.Human.findAll({
    include:[models.Pet]
  }).then(humans => {
    res.render("index",{humans:humans});
  });
});

router.post('/human/add',(req, res, next) => {
  models.Human.create({
    name:req.body.name,
    age:req.body.age
  }).then(newUser => {
    console.log(newUser);
    res.redirect("/");
  });
});

router.post('/human/destroy',(req, res, next) => {
  models.Human.destroy({
    where:{id:req.body.id}
  }).then(deletedUser => {
    console.log(deletedUser);
    res.redirect("/");
  })
});

router.post('/pet/add',(req, res, next) => {
  models.Pet.create({
    name:req.body.name,
    kind:req.body.kind,
    HumanId:req.body.HumanId
  }).then(() => {
    res.redirect("/");
  })
});

module.exports = router;

index.ejs

<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1>Sample</h1>
    <p>Welcome to Sample</p>

    <h2>Human Table</h2>
    <table>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Age</th>
      </tr>
      <% for(var human of humans) {%>
        <tr>
          <td><%= human.id %></td>
          <td><%= human.name %></td>
          <td><%= human.age %></td>
          <% for(var pet of human.Pets) {%>
            <td><%= pet.name %></td>
          <% } %>
        </tr>
      <% } %>
    </table>

    <h2>Create Human</h2>
    <form action="/human/add" method="POST">
      <input type="text" name="name" placeholder="name">
      <input type="number" name="age" placeholder="age">
      <input type="submit" value="Create">
    </form>

    <h2>Destroy Human</h2>
    <form action="/human/destroy" method="POST">
      <input type="number" name="id" placeholder="id">
      <input type="submit" value="Destroy">
    </form>
  </body>

  <h2>Create Pet</h2>
  <form action="/pet/add" method="POST">
    <input type="text" name="name" placeholder="name">
    <input type="text" name="kind" placeholder="kind">
    <input type="number" name="HumanId" placeholder="Owner's ID">
    <input type="submit" value="Create">
  </form>
</html>


それでは実行してみます。
実行は先ほどと同じで

npm start

でOKです。

これでうまくいけば以下のように飼い主のIDでPetとHumanを関連付けることができます。

f:id:sawalemontea:20190401233937p:plain
express sequelize postgreSQL association test result

(おまけ)Herokuでは

もし自分で何かつくってherokuにデプロイする場合、migrationは
heroku run bashで開いてsequelize db:migrate --env productionすればOKです。



おわり

今回はこれでおしまいです。
www.sawalemontea.com