18
2016

Sequelizeの個人的tips

CATEGORYJavaScript
Node.js の研修で、なんかORMつかうべと見つけた Sequelize を使ってみたものの、結構ハマりポイントがあったので、tipsとしてまとめてみる。
なお、予め断っておくが、2016年現在Node.js用のORMはほぼ選択肢がなく Sequelize 一択の模様。
なのでハマるなら他のにしようという選択肢は、SQLべた書きぐらいしかない。残念!

以下目次。後述するように、この機能あるから使おうねーここにあるよーみたいな話が多め。
では、本文の方へ。


公式ドキュメントにだいたい載ってる ※ただし探しにくい

ドキュメントはちゃんと読もう&どこに何が書かれてるか覚えよう ←重要

まず一番基本はこれ。自分がJavaScript不慣れなのもあるにしても…Sequelize はなんか独特。
ORMとしての基本的な機能は一通り何でも揃ってる感じなんだけど、古いフレームワークらしく、定義の仕方がいちいち独自ルールで、直観的でもなく、最近のJavaScriptの標準化の流れとかにも従ってない。
なので、パッと見てこれどうやるの…?みたいな点が非常に多い!
が、実はマニュアル読むと(サンプルの中にさらっと)使い方が載ってたりする。
なので、公式サイトのドキュメントはちゃんと読みましょう&わからない時は探しましょう。

以下のtipsも、Sequelize にはこんな機能があるからそれを使いましょう!みたいな話が多め。
なお日本語ドキュメントはあんまない。ここぐらい?泣ける。

…でもほんと、公式サイトもなんか分かりにくい(というか機能が多すぎて説明しきれてない?)ので、ググってサンプルとか見つけてそこから辿りなおした方が早いことも、、、

論理削除はparanoidを使う

いろいろと議論のある論理削除だが、Sequelize ではクラスオプションに paranoid: true と指定するだけで有効になる。
このオプションを指定すると、テーブルに deletedAt という削除日時の列が追加されて、各種検索では自動的にその列がNULLなことを条件にするようになる。
findAllとかのオプションに、論理削除した行も対象にするオプションあり。)

手動でSQL書かない限りは、自動的に除外してくれるので、とても使いやすい。自己流でやるより、paranoidを使おう。

scopeを使おう

あんまり他のORMでは見ない?機能として、scopeというものがある。
ようするによく使う検索条件やなんかのセットを定義しておいて検索やら更新やらの際にそれを指定する、というものなのだが、再利用もしやすく、個別に関数を定義するより使いやすい。
const User = sequelize.define('user', {
// 列定義(省略。id, name, password, status 辺りがある users テーブルだと思ってください)
}, {
// クラスオプションでのスコープ定義
defaultScope: {
attributes: {
exclude: ['password'],
},
order: [['name', 'ASC'], ['id', 'ASC']],
},
scopes: {
login: {
where: {
status: {$ne: "disable"},
},
},
withitem: (count) => {
return {
include: [{
model: sequelize.model("item"),
as: 'items',
where: { num: { $gt: count }},
}],
};
},
},
};
User.scope("login").findById(10);
User.scope("login").findOne({ where: {name: name}});
User.scope({ method: ["withitem", 1]}).findById(10);
User.scope({ method: ["withitem", 1]}).findAll();
上がスコープ定義とスコープを使う処理の例。こんな風に findOne() とか findAll() とかいろんなメソッドと組み合わせられるので普通に関数を作るより便利。
スコープは引数を取ることも可能で、上のwithitemはitemsテーブルとJOINしてn件以上のレコードを同時に取ってくるイメージ。
ただ、呼び出し側の書き方はちょっと冗長になる。

デフォルトスコープというのもあって、これはその名の通り、これを指定しておくと全てのメソッドでデフォルトでこのスコープが適用されるようになる。
上の例だと、通常はパスワード列を返さないようにしていて、また常にnameidでソートされるようにしている。

スコープは複数組み合わせて使うことも可能なので、自分はソートのスコープとJOINのスコープを組み合わせたりして活用している。
…が、双方に同じoptions(それぞれ違うテーブルにincludeとか)があると片方しか有効にならない?模様なので、ちゃんとログのSQL文確認して、効いてる組み合わせかチェックした方がよさげ。

hookを使おう

これは他のORMでも見るかな?ようするにトリガー。
hookを用いることで、更新前に値を変換したり、同時に他のオブジェクトも更新したりといったことができる。
/**
* パスワードをハッシュ化する。
* @function beforeSave
* @param {User} user 更新されるユーザー。
* @param {Object} options 更新処理のオプション。
*/
function beforeSave(user, options) {
// 新しいパスワードが設定されている場合、自動でハッシュ化する
if (user.password != undefined && user.password != "" && user.password != user.previous("password")) {
user.password = "ここに何かハッシュ化の処理";
}
}

const User = sequelize.define('user', {
// 列定義(省略。〃)
}, {
// クラスオプションでのスコープ定義
hooks: {
beforeCreate: beforeSave,
beforeUpdate: beforeSave,
},
};
こういう風にすれば、登録/更新処理の前に自動的にパスワードをハッシュ化したりできる。
エラーが発生する可能性のある処理とかには使い難いけど、それ以外では使える。

index.jsからロードする

最初にも書いたけど Sequelizejs は古めのフレームワークなので、EcmaScript 2015 のimport/exportどころかNode.jsのrequire/exportでもなく、自前にimportの仕組みを持ってたりする。
なので、Express に組み込んだりするときはその辺うまいこと辻褄を合わせて上げないといけない。

んで、最初そのやり方が見つからず悩んだのだが、公式ドキュメント内にひっそりとサンプルのURLがあり(--;、それに従うと index.js を作ってそのディレクトリにモデルのソースを置いて、んで使用側では index.js を require する形で丸ごとディレクトリ単位で読み込むのが推奨されているようだった。

なので、素直にサンプルmodels/index.js とか見て同じようにやるのがよいかと。

ログ出力の差し替え

Sequelize、SQLの実行ログがデフォルトでコンソールに出ててデバッグに便利なんだけど当然まあそのまま運用するわけにはいかないから消し方調べる。
でも、例によってしれっと載ってるだけっぽいので、詳しくまとめとく。

このログ出力の制御は、Sequelizeインスタンスをnewするときのオプションの指定で可能(っていうか困ったら大体オプション)。
まず消し方。消し方は簡単で、options.logging を false にしてやればOK。サクッと消えてくれる。
var sequelize = new Sequelize(config.database, config.username, config.password, {
logging: false,
});
次いで、ログ出力先の差し替え方。デフォルトだと console.log になってるけど、ここに関数を渡してやれば普通に差し替わる。
const log4js = require('log4js');
const logger = log4js.getLogger('debug');
var sequelize = new Sequelize(config.database, config.username, config.password, {
logging: (log) => { logger.debug(log) },
});
Log4js の例。SQLログは見れた方が便利なことが多いので、消すよりはログレベル指定して差し替えをお勧め。

関連はモデルと別に定義する

これも良いやり方が分からずハマったとこ。普通にモデルを設計していくと、まあ1モデル1ファイルで書くと思うんだけど…Sequelizeのモデルは前述のように sequelize.import() で読み込む感じなので、あるモデルが初期化されてるときには別のモデルはまだ未定義で…という感じで、他の言語のORMみたいにモデル内で関連をうまく記述することができなそう。

で、文字列で書くとかいや何か方法があるんだろうとしばらく悩んだんだけど、ベストプラクティスとかでググったら関連は index.php とかに外だしするといいよ、となっていたのでこれは分けるのが正解な模様。
ちょい違和感があるが、まあ関連は特定モデルに関する記述ではないので、これも一つの考え方か…。

なお、他のモデルの参照はスコープの例でやってるみたいに sequelize.model("item") とか書けばできることはできる。
でもたぶんfunctionの中とか、初期化後に動く処理のとこじゃないと動かない。

GROUP BYでは普通にincludeの列も使える

最初マニュアルさっと見て、複雑なSQLは生成できないと思って生SQLで書いてたものの、試してみたら意外となんでも使えた、というお話。

Sequelize にもちゃんとSUMやらAVGやらのSQL関数を投げるための仕組みは用意されており、絞り込み条件にGROUP BYやらも指定できる。
が、JOIN先の列を指定する仕方とかがドキュメントになさげ(?)。でも、実際はそういうGROUP BYやらも、単に列名を "item.id" みたいに普通にピリオドで区切ってやれば指定できる。こんな感じ。というか、気を使ってincludeの子側に書いたりしても機能しないので注意。
User.findAll({
attributes: [
'items.itemCd',
[sequelize.fn('COUNT', sequelize.col('user.id')), 'cnt']
],
include: [{
model: sequelize.model("item"),
as: 'items',
required: true,
attributes: [],
where: { num: { $gt: 0 }},
}],
group: ["items.itemCd"],
raw: true
});
ちなみに raw: true と指定すると、モデルとして変換されてない生の結果が返ってくる。

syncのテーブル自動生成は便利

Sequelize には、モデル定義からDBのテーブルを自動生成してくれるsyncという仕組みがあるのだが、これが開発中は便利。SQLとの二重管理にもならないし、モデルのコード変えたらテーブルDROPしてアプリ再起動すればいいだけなのでお手軽。

全テーブルと個別のテーブルのsyncがあるけど、まあ index.js の末尾辺りで全部syncしちゃえば楽。

例外はnameで判別できそう

これは俺が知らないだけでひょっとしたらJavaScriptみんなそうなのかもしれないけど、バリデーションエラーとかDBエラーとかは、SequelizeValidationError やら SequelizeUniqueConstraintError やらの name プロパティを持つ例外オブジェクトで投げられるので、これで判別可能。
また、エラーメッセージは同じく errors プロパティの配列の中に入ってるので、その辺見て出しわけることができそう。

ただ、今回はサーバー側はREST APIのみで、バリデーションとかは簡単にしか使ってない(クライアント側でやった)ので、あんまりちゃんと調べてない。

Active Recordは避けるべき?

最後のtips。ここまででいろいろscopeだとか例外だとか書いてきたけど、まあこんな感じに独特の部分が多いので、Sequelizeのモデルをそのまま使用者に公開してしまうと、Sequelizeの機能に依存するコードになりがち。
User.scope("login").findOne({ where: {name: name}}) とか。DB層のフレームワークが丸見え。)

それと、自分はFatModel的に使ってみたけど、複数モデルが絡む記述とかも前述のように書きにかったりするし、なのでSequelizeはあくまでDBにアクセスするライブラリにとどめて、その上にDAO層を作る形で使った方が良いのかなと思った。
まあ、そっちはそっちで冗長になったりして手間が増えそうだけど。


以上、tipsとしてとりあえず思いついたのはこんなところ。Sequelize便利ではあるんだけど、もう少しなんとかなって欲しい感がある。
サーバーサイドJavaScript、Expressもあんまイケてないし、オールインワンのちゃんとしたフレームワークができたら、一気に人が流れるんじゃなかろうか?
現状はちょっと癖が強い感じがある。選択肢ないから使うけどさ。
スポンサーサイト

Tag: JavaScript Sequelize

0 Comments

Leave a comment