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()
関数に逃がす方法の実装を参考にした