Node.js・ExpressでブラウザからPUT・DELETEリクエストを発行する

2017-08-20

Node.js/ExpressアプリケーションからRDBへ接続してみる。
使用するツール・ライブラリは以下。

以下の記事を読んだ前提で書く。

環境設定

先の記事で紹介したプロジェクトにて以下を実行する。

$ yarn add sequelize sqlite3 morgan
$ mkdir app/db

.gitignore へ以下を追加。

node_modules
app/db/*.sqlite
.DS_Store
./**/.DS_Store

実装

  • app/repositories/sequelize.js
    • Sequelizeを使ったRDB接続用モジュール
  • app/repositories/models.js
    • Clientモデルを作成
    • テーブルの作成とサンプルデータのインサート
  • app/controllers/get_client
    • 上記の models.js 経由でClientの検索

app/repositories/sequelize.js

const Sequelize = require('sequelize');
const path = require('path');
const logger = require('morgan');

const sequelize = new Sequelize('SampleDB', '', '', {
  host: 'localhost',
  dialect: 'sqlite',

  pool: {
    max: 5,
    min: 0,
    idle: 10000
  },

  // SQLite only
  storage: path.join(__dirname, '../db/database.sqlite'),

  logging: logger.info // ログをロガーに流す
});

// Test the connection
sequelize
  .authenticate()
  .then(() => {
    console.log('Connection has been established successfully.');
  })
  .catch(err => {
    console.error('Unable to connect to the database:', err);
  });

module.exports = sequelize;

app/repositories/models.js

const Sequelize = require('sequelize');
const sequelize = require('./sequelize');

const Client = sequelize.define('client', {
  firstName: {
    type: Sequelize.STRING
  },
  lastName: {
    type: Sequelize.STRING
  }
});

// force: true will drop the table if it already exists
Client.sync({force: true}).then(() => {
  // Table created
  return Client.create({
    firstName: 'Hoge',
    lastName: 'Fuge'
  });
});

module.exports = {
  Client: Client
};

app/controllers/get_clients.js

const models = require('../repositories/models');
const Client = models.Client;

const get_clients = (req, res, next) => {
  Client.findAll().then(clients => {
    res.send(clients);
  });
};

module.exports = get_clients;

実行

以下で起動。

$ node app/app.js

http://localhost:3000 にブラウザでアクセスして Client 情報が見れたら成功。

Association

ちょっとハマったので、ここではモデル同士のリレーション・関連付けについて記載する。
リレーションには以下がある。

  • hasOne
    • 1:1 のリレーション
  • belongsTo
    • 1:N のリレーション
    • hasOnehasMany される側
  • hasMany
    • 1:N のリレーション
  • belongsToMany
    • N:M のリレーション

ここでは belongsToMany の例のみ記載する。
参考/チュートリアル参考/リファレンス

実装

  • app/models/user.js
    • ユーザテーブルのモデル
  • app/models/team.js
    • チームテーブルのモデル
  • app/models/team-user.js
    • チームテーブルとユーザテーブルの N:M のリレーションを構築する中間テーブルのモデル
  • app/repositories/sequelize.js
    • Sequelize のロード・設定を行う
    • Sqlite3 を使用した例になっている
  • app/repositories/db.js
    • モデルのリレーション作成、DBとの同期を行う
  • app/app.js
    • 適当な実行ファイル、全く参考にならない
    • node app/app.js で実行

個人的な好みだが、モデルは 単数形 で作成する。
また、 モデルのリレーションを実装してから sync() する のがポイント。(ここでハマった)

app/models/user.js

const Sequelize = require('sequelize');
const sequelize = require('../repositories/sequelize');

const User = sequelize.define('user',
  {
    user_id: {
      type: Sequelize.STRING,
      primaryKey: true,
      allowNull: false,
      autoIncrement: false,
      unique: true
    },
    user_name: {
      type: Sequelize.STRING
    }
  },
  {
    timestamps: false,
    tableName: 'user'
  }
);

module.exports = User;

user_id が主キー。

app/models/team.js

const Sequelize = require('sequelize');
const sequelize = require('../repositories/sequelize');

const Team = sequelize.define('team',
  {
    team_id: {
      type: Sequelize.STRING,
      primaryKey: true,
      allowNull: false,
      autoIncrement: false,
      unique: true
    },
    team_name: {
      type: Sequelize.STRING
    }
  },
  {
    timestamps: false,
    tableName: 'team'
  }
);

module.exports = Team;

team_id が主キー。

app/models/team-user.js

const Sequelize = require('sequelize');
const sequelize = require('../repositories/sequelize');

const TeamUser = sequelize.define('team_user',
  {
    team_id: {
      type: Sequelize.STRING,
      primaryKey: true,
      allowNull: false,
      unique: false,
      references: {
        model: "team",
        key: "team_id"
      }
    },
    user_id: {
      type: Sequelize.STRING,
      primaryKey: true,
      allowNull: false,
      unique: false,
      references: {
        model: "user",
        key: "user_id"
      }
    }
  },
  {
    timestamps: false,
    tableName: 'team_user'
  }
);

module.exports = TeamUser;

user_idteam_id が複合キーで、それぞれ外部キーを参照しているのがポイント。

app/repositories/sequelize.js

const Sequelize = require('sequelize');
const path = require('path');
const logger = require('morgan');

const sequelize = new Sequelize('SampleDB', '', '', {
  host: 'localhost',
  dialect: 'sqlite',

  pool: {
    max: 5,
    min: 0,
    idle: 10000
  },

  // SQLite only
  storage: path.join(__dirname, '../db/database.sqlite'),

  logging: logger.info // ログをロガーに流す
});

// Test the connection
sequelize
  .authenticate()
  .then(() => {
    console.log('Connection has been established successfully.');
  })
  .catch(err => {
    console.error('Unable to connect to the database:', err);
  });

module.exports = sequelize;

app/repositories/db.js

const Sequelize = require('sequelize');
const sequelize = require('./sequelize');
const User = require('../models/user');
const Team = require('../models/team');
const TeamUser = require('../models/team-user');
const UserAccount = require('../models/user-account');

// 以下で m:n のリレーションを作成でき、以下が実行可能になる
// user.addTeam : 既存のteamテーブルのteamモデル(配列も可)をuserと関連付け。
// user.addTeams : 既存のteamテーブルのteamモデル(配列)をuserと関連付け。
// user.countTeams : userに関連付けされたteamを数え上げる。
// user.createTeam : teamオブジェクトを渡して、新規にteamテーブルにteamレコードを作成して且つ関連付け。
// user.getTeams : userと関連付けされたteamモデルを取得する。
// user.hasTeam : userに引数で与えたteamモデルが関連付けされているか確認する。(true/false)
// user.hasTeams : userに引数で与えたteamモデルが関連付けされているか確認する。(true/false)
// user.removeTeam : userと引数で与えたteamモデル(配列も可)の関連付けを削除する。ただし、teamモデルは削除されない。
// user.removeTeams : userと引数で与えたteamモデル(配列)の関連付けを削除する。ただし、teamモデルは削除されない。
// user.setTeams : 意味がわかってない。addのように関連付けはされない。
// 参考:http://docs.sequelizejs.com/class/lib/associations/belongs-to-many.js~BelongsToMany.html
User.belongsToMany(Team, {
  through: TeamUser,
  foreignKey: 'user_id',
  otherKey: 'team_id'
});

// 以下で m:n のリレーションを作成でき、以下が実行可能になる
// team.addUser(s), team.countUsers, team.createUser, team.getUsers,
// team.hasUser(s), team.removeUser(s), team.setUsers
Team.belongsToMany(User, {
  through: TeamUser,
  foreignKey: 'team_id',
  otherKey: 'user_id'
});

User.hasMany(UserAccount, {
  foreignKey: 'user_id'
});
UserAccount.belongsTo(User, {
  foreignKey: 'user_id',
  targetKey: 'user_id'
});

// テーブル作成
User.sync();
Team.sync();
TeamUser.sync();
UserAccount.sync();

module.exports = {
  User: User,
  Team: Team,
  TeamUser: TeamUser,
  UserAccount: UserAccount
};

belongsToMany を使用して中間テーブルを介したモデルを構築する際は必ず through を記載する。
belongsToMany でリレーションを構築するとユーザモデルから以下の関数を実行できるようになる。(チームモデルも同様)

  • user.addTeam
    • 既存のteamテーブルのteamモデル(配列も可)をuserと関連付け。
  • user.addTeams
    • 既存のteamテーブルのteamモデル(配列)をuserと関連付け。
  • user.countTeams
    • userに関連付けされたteamを数え上げる。
  • user.createTeam
    • teamオブジェクトを渡して、新規にteamテーブルにteamレコードを作成して且つ関連付け。
  • user.getTeams
    • userと関連付けされたteamモデルを取得する。
  • user.hasTeam
    • userに引数で与えたteamモデルが関連付けされているか確認する。(true/false)
  • user.hasTeams
    • userに引数で与えたteamモデルが関連付けされているか確認する。(true/false)
  • user.removeTeam
    • userと引数で与えたteamモデル(配列も可)の関連付けを削除する。
    • ただし、teamモデルは削除されない。
  • user.removeTeams
    • userと引数で与えたteamモデル(配列)の関連付けを削除する。
    • ただし、teamモデルは削除されない。
  • user.setTeams
    • add との違いがわかってない。。。

参考

app/app.js

const db = require('./repositories/db');
const User = db.User;
const Team = db.Team;

User.create({
  user_id: '0001',
  user_name: 'user0001'
}).then(() => {
  User.findOne({
    where:{user_id: '0001'}
  }).then((user) => {
    console.log("user_name: " + user.user_name);
    user.createTeam({
      team_id: `0001`,
      team_name: 'team0001'
    }).then(() => {
      user.countTeams().then((num) => {
        console.log("team num of user: " + num);
      }).then(() => {
        user.getTeams().then((teams) => {
          console.log("teams: " + teams[0].dataValues.team_name);
          Team.create({
            team_id: '0002',
            team_name: 'team0002'
          }).then(() => {
            Team.findOne({
              where: {team_id: '0002'}
            }).then((team) => {
              console.log("team_name: " + team.dataValues.team_name);
              user.addTeam(team).then(() => {
                user.countTeams().then((num) => {
                  console.log("team num of user: " + num);
                }).then(() => {
                  let teams = [];
                  teams.push(team);
                  user.removeTeams(teams).then(() => {
                    user.countTeams().then((num) => {
                      console.log("team num of user: " + num);
                    });
                  });
                });
              });
            });
          });
        })
      });
    });
  });
});

おすすめ書籍

おすすめ記事