Sequelize で1対多の関係のテーブル定義を作る方法
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カラム : その書籍が登録されているカテゴリの IDnameカラム : 書籍名
ココでは、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()関数に逃がす方法の実装を参考にした