Heroku Postgres を扱うために、以前試した Sequelize を使うことにした。そこで、「1対多」の関係にあるテーブルの関係を定義する必要が出てきたので、そのやり方をまとめる。
- Sequelize のおさらい
- 複数のモデルを定義する場合のよくある雛形
- 親子関係にあるテーブルの定義
- 関係の定義をモデル生成後に一括実行する
- 関係を定義するメリット : JOIN しやすくなる
- 参考文献
Sequelize のおさらい
Sequelize は Node.js 向けの O/R マッパー。PostgreSQL、SQLite、MySQL などに対応していて、テーブルに対応するモデルを定義することで CRUD 操作を行える。
PostgreSQL との接続時は、Sequelize と一緒に pg
パッケージも一緒にインストールしておく。
$ npm install --save sequelize pg
Heroku Postgres との接続は、process.env.DATABASE_URL
で参照できる接続文字列を利用するのが手っ取り早い。詳しくはこの次のサンプルコードで説明する。
複数のモデルを定義する場合のよくある雛形
Sequelize で複数のモデル (= テーブル) を扱いたい場合は、以下のような単一のファイルを作ると操作しやすい。
// model.js const Sequelize = require('sequelize'); // 必要に応じて dotenv パッケージで環境変数をロードする require('dotenv').config(); /** Sequelize と生成したモデルを束ねる */ const Model = {}; // DB 接続する const connectionString = process.env.DATABASE_URL; const sequelize = new Sequelize(connectionString, { timezone: '+09:00', // JST タイムゾーン : Sequelize で SELECT した値は UTC 形式 (ISOString) になっている logging: false // ログ出力を抑制する }); // TODO : 各モデルを定義する Model.User = require('./user-model')(sequelize); // Sequelize を格納する Model.Sequelize = Sequelize; Model.sequelize = sequelize; module.exports = Model;
途中に出てきた user-model.js
はこんな感じで作る。
// user-model.js const Sequelize = require('sequelize'); /** users テーブルのモデルを定義する */ module.exports = (sequelize) => { // define() の第1引数がテーブル名 // 第2引数のキーが、モデルのプロパティ名になり、実際のテーブルのカラム名は field プロパティで示す const User = sequelize.define('users', { id : { field: 'id' , type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, userName: { field: 'user_name', type: Sequelize.TEXT , allowNull: false } }); // テーブルがなければ作成し、同期する User.sync(); // モデルを返す return User; };
このように作っておくことで、モデルを利用したいところでは、以下のように利用できる。
// モデルを取得する const Model = require('./model'); // ユーザ名が「サンプルユーザ」に合致するデータを1件取得する Model.User.findOne({ where: { userName: 'サンプルユーザ' } }) .then((result) => { // 結果は result.dataValues 配下にあり、プロパティ名はモデル定義のキーになる console.log('ユーザ情報を取得', result.dataValues.id, result.dataValues.userName); }) .catch((error) => { console.error(error); });
この時、require('./model');
という require()
が複数のファイルに記述されていても、DB 接続の処理は1回しか実行されないので一安心。
親子関係にあるテーブルの定義
ココまでの例では、users
テーブルは単独で存在するテーブルだったので、コレで十分だった。
ココからは、「1つのカテゴリに複数の書籍が登録されているデータベース」を表現する、2つのテーブルを作ろうと思う。
- categories テーブル
- id カラム : カテゴリ ID (カテゴリごとに一意に設定される)
- name カラム : カテゴリ名 (「小説」「雑誌」みたいなイメージ)
- books テーブル
- id カラム : 書籍 ID (1つの書籍に一意に設定される)
- category_id カラム : その書籍が登録されているカテゴリの ID
- name カラム : 書籍名
ココでは、books
テーブルの1レコード、つまり1つの書籍は、必ず1つのカテゴリに所属する、という関係とする。つまり「カテゴリ:書籍」は「1:n」 (= 1 対 多 = one-to-many) となる。
この関係を Sequelize 上で定義しておくと、books.category_id
を「外部キー」として宣言できるようになる。sync()
によって Sequelize のモデルからテーブルを生成させる時に、実際の DB 上に制約を付与できるようになるので、上手く定義してやりたい。
自分に紐づく複数の子が存在する : hasMany
まず、「1:n」の「1」側となる、categories
テーブルに、子テーブルが存在することを定義する。先程の user-model.js
と同じ作りで、category-model.js
を作ったとする。
const Sequelize = require('sequelize'); module.export = (sequelize) => { const Category = sequelize.define('categories', { id : { field: 'id' , type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, // カテゴリ ID name: { field: 'name', type: Sequelize.TEXT, allowNull: false }, // カテゴリ名 }); // categories : books で 1:n の関係であることを示す // FIXME : このコードは動かない Category.hasMany(Model.Book, { foreignKey: 'categoryId' // 対象 (book テーブル) のカラム名を指定する }); Category.sync(); return Category; };
この時点で分かるかもしれないが、このコードは動かない。Sequelize の hasMany()
の仕様では、子テーブルを示すために、hasMany()
の第1引数に対象のモデルを渡す必要がある。つまり今回の場合は、このあと示す Book
モデルを渡す必要があるのだが、このタイミングでは Book
モデルの存在は分からないのである。
この点は後で直すとして、ひとまず API としては、hasMany()
を使うと子テーブルとの関係を示せる、ということだけ押さえておこう。
一つの親が存在する : belongsTo
次に「1:n」の「n」側となる、books
テーブルを定義する。book-model.js
を作ったとする。
const Sequelize = require('sequelize'); module.export = (sequelize) => { const Book = sequelize.define('books', { id : { field: 'id' , type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, // 書籍 ID categoryId: { field: 'category_id', type: Sequelize.INTEGER, allowNull: false }, // 紐付くカテゴリ ID name : { field: 'name' , type: Sequelize.TEXT, allowNull: false } // カテゴリ名 }); // categories : books で 1:n の関係であることを示す // FIXME : このコードは動かない Book.belongsTo(Model.Category, { foreignKey: 'categoryId', // books.category_id のカラム名を指定する targetKey : 'id' // 対応する category テーブルのカラム名を指定する }); Book.sync(); return Book; };
こちらも動作しないコードであることが分かるだろうか。親テーブルの存在を示す belongsTo()
メソッドの第1引数に、対象のモデルクラスを与える必要があるのだ。
さて、コレをどう解決するか。
関係の定義をモデル生成後に一括実行する
親と子のテーブルモデルで、それぞれお互いを参照する必要がある。ということは、hasMany()
・belongsTo()
のいずれかを実行するタイミングでは、2つのモデルの定義が完了していないといけない、ということだ。コレをどのように実現するか。
色々な文献を見ていたところ、良いやり方が見つかったので紹介する。
category-model.js
const Sequelize = require('sequelize'); module.export = (sequelize) => { const Category = sequelize.define('categories', { id : { field: 'id' , type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, // カテゴリ ID name: { field: 'name', type: Sequelize.TEXT, allowNull: false }, // カテゴリ名 }); // categories : books で 1:n の関係であることを示す Category.associate = (Model) => { Category.hasMany(Model.Book, { foreignKey: 'categoryId' // 対象 (book テーブル) のカラム名を指定する }); }; Category.sync(); return Category; };
book-model.js
const Sequelize = require('sequelize'); module.export = (sequelize) => { const Book = sequelize.define('books', { id : { field: 'id' , type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, // 書籍 ID categoryId: { field: 'category_id', type: Sequelize.INTEGER, allowNull: false }, // 紐付くカテゴリ ID name : { field: 'name' , type: Sequelize.TEXT, allowNull: false } // カテゴリ名 }); // categories : books で 1:n の関係であることを示す Book.associate = (Model) => { Book.belongsTo(Model.Category, { foreignKey: 'categoryId', // books.category_id のカラム名を指定する targetKey : 'id' // 対応する category テーブルのカラム名を指定する }); }; Book.sync(); return Book; };
先程動作しないと書いた hasMany()
・belongsTo()
の処理を、それぞれのモデルに定義した associate()
というメソッドに内包した。つまり各ファイルの中では「associate
プロパティに関数を定義しただけ」で、この時点では外部キーの関係を宣言できていない。
この associate()
関数を呼び出すのは、これらのモデルを取りまとめる、メインの model.js
となる。
model.js
const Sequelize = require('sequelize'); // 必要に応じて dotenv パッケージで環境変数をロードする require('dotenv').config(); /** Sequelize と生成したモデルを束ねる */ const Model = {}; // DB 接続する const connectionString = process.env.DATABASE_URL; const sequelize = new Sequelize(connectionString, { timezone: '+09:00', // JST タイムゾーン : Sequelize で SELECT した値は UTC 形式 (ISOString) になっている logging: false // ログ出力を抑制する }); // 各モデルを定義する Model.User = require('./user-model')(sequelize); // -------------------------------------------------- // ココまでは前述のコードと同じ。 // Category と Book モデルを読み込む Model.Category = require('./category-model')(sequelize); Model.Book = require('./book-model')(sequelize); // 各モデルの定義が終わったら associate 関数を呼び出し、テーブルの関係を定義させる Object.keys(Model).forEach((key) => { const model = Model[key]; if(model.associate) { model.associate(Model); } }); // 追記ココまで // -------------------------------------------------- // Sequelize を格納する Model.Sequelize = Sequelize; Model.sequelize = sequelize; module.exports = Model;
お分かりいただけただろうか。
各モデルの定義は、require()
で先に済ませておき、定数 Model
に蓄えておく。
全てのモデルの定義が終わったら、Object.keys(Model)
で Model
内のプロパティをループし、モデルを走査する。この時、そのモデルが associate()
関数を持っていれば、引数に Model
自身を渡して実行する、という作りだ。
コレなら、Category.associate()
(→ hasMany()
) が実行された時は Model.Book
が参照できるようになっているし、Book.associate()
(→ belongsTo()
) が実行された時は Model.Category
が参照できるようになっている、というワケだ。
関係を定義するメリット : JOIN
しやすくなる
このように各モデル (= テーブル) とその関係を定義してやれば、Sequzelize を通じて DB の定義も同期でるだけでなく、SELECT
時の JOIN
がやりやすくなる。
// カテゴリ ID : 1 のデータと、それに紐付く書籍データを取得する Model.Category.findById(1, { include: [{ model: Model.Book, // 子テーブルを示す required: false // false で OUTER JOIN になる (true で INNER JOIN) }] }) .then((results) => { console.log(results); });
このようにすると、次のような連想配列の構造でデータが取得できるようになる。
{ "id": 1, "name": "小説カテゴリ", "books": [ { id: 101, categoryId: 1, name: "なんたらミステリー" }, { id: 105, categoryId: 1, name: "なんたらサスペンス" }, { id: 129, categoryId: 1, name: "なんたらラブストーリー" } ] }
include : model
で渡したモデルのプロパティ (ココでいう books
) が増えており、その中に Book
モデルの形式に沿った形で、category_id
が 1
なデータが配列で格納されているのだ。
実際に SQL を書いて SELECT
した場合は、どうしても categories
テーブルのカラム情報が含まれたレコードの形になってしまうが、コレなら Book
モデルの情報は books
テーブルのカラムしか含まれない、プレーンな状態で取得できるというワケだ。テーブル構造がそのまんま連想配列で表現されていて分かりやすい。
参考文献
- mysql - How to make Sequelize use singular table names - Stack Overflow …
define()
の第1引数で指定するモデル名は単数形で書いても「複数形」のテーブルが存在するモノとして勝手に変換されてしまう。コレを直すにはfreezeTableName: true
オプションを設定するか、tableName
オプションでテーブル名を指定する。 - Tutorial | Sequelize | The node.js ORM for PostgreSQL, MySQL, SQLite and MSSQL … モデルの
sync()
メソッドにより、接続先の DB にそのテーブルがなければ自動的にCREATE TABLE
文を発行してくれる。sync({ force: true })
とするとテーブルが存在していても強制的にCREATE TABLE
する。 - Sequelizeのassociation - Qiita
- Simple is Beautiful.
- Simple is Beautiful.
- express-example/index.js at 605508d29ee70af5f1821a3b6f07697ecaa055c0 · sequelize/express-example · GitHub …
associate()
関数に逃がす方法の実装を参考にした

Construindo APIs REST com Node.js: Caio Ribeiro Pereira (Portuguese Edition)
- 作者: Caio Ribeiro Pereira
- 出版社/メーカー: Casa do Código
- 発売日: 2016/02/05
- メディア: Kindle版
- この商品を含むブログを見る
![データベースエンジニア教本 MySQL&PostgreSQL&NoSQL編【電子書籍】[ Software Design編集部 ] データベースエンジニア教本 MySQL&PostgreSQL&NoSQL編【電子書籍】[ Software Design編集部 ]](https://thumbnail.image.rakuten.co.jp/@0_mall/rakutenkobo-ebooks/cabinet/5879/2000005595879.jpg?_ex=128x128)
データベースエンジニア教本 MySQL&PostgreSQL&NoSQL編【電子書籍】[ Software Design編集部 ]
- ジャンル: 本・雑誌・コミック > PC・システム開発 > その他
- ショップ: 楽天Kobo電子書籍ストア
- 価格: 2,268円