はじめに
前回の記事で、sequelizeを使ってモデル定義して使うあたりまでは行けました。
独自のメソッドを持たないモデルならアレで良かったのですが、独自にメソッドを生やす必要が出て来て、ちょっとつまづいたのでそこをメモしておこうと思います。
インスタンスメソッド、クラスメソッドを生やす
こんな風にオブジェクトを用意し、define時にparamsとは別に渡します。
var option = {
classMethods: {
classHoge: function () {....}
},
instanceMethods: {
instanceHoge: function (moji: string) {....}
}
};
sequelize.define("HogeModel", params, option);
これで、HogeModelにはstaticメソッドとしてclassHoge()、HogeModelをcreateしたりfindしたりした結果のインスタンスにはinstanceHoge()が生えてくれます。
ただし、TypeScriptはそんな事知りませんので
var HogeModel = sequelize.model("HogeModel");
HogeModel.classHoge();// エラー
このようなコードを書いても通らないです。
インターフェースの拡張
原因は前回の記事でも少し言及しましたが、何もしないままだとsequelize.modelの返り値の型はModel<{},{}>となってしまい、ちょっと凝った事をしようとすると死にます。というか、コンパイルがまともに通りません。
そのため、戻り値を適切なinterfaceにはめてキャストしてやる必要があります。(実体としては存在しているため、anyで回避しても同じ事が出来ます)
// こんな感じのインターフェースを定義
interface HogeModel extends Sequelize.Model<HogeInstance, HogeParams> {
classHoge(): void;
}
interface HogeInstance extends Sequelize.Instance<HogeInstance, HogeParams> {
instanceHoge(moji: string): void;
}
// 拾ってくるときにキャスト
var HogeModel = <HogeModel>sequelize.model("HogeModel");
HogeModel.classHoge();// OK
// インスタンスもキャスト
var instance = <HogeInstance>HogeModel.create(……);
instance.instanceHoge("aaa");// ok
キャストが多いソースコードになってしまいますね……
Modelに生えてるメソッドのオーバーライド
Sequelizeは高機能なORMなので、Modelにも、それから作ったインスタンスにも、便利メソッドが山ほど生えています。
独自メソッドを生やすのと同じようなやり方で、Sequelizeのメソッドを上書きする事が出来ます。
var option = {
classMethods: {
classHoge: function () {....},
build: function() {
console.log("hogeBuild");
}
},
instanceMethods: {
instanceHoge: function (moji: string) {....}
}
};
さきほどのoptionに、このように記述するだけです。Typescriptはこのoptionがどのように使われるか知らないので、buildに渡している関数の引数や戻り値等は一切感知できません。
buildは本来、適切なparamsを受け取って新しいinstanceを返すメソッドですが、今の例では、受け取った引数は全部無視して、console.logして終わり、というメソッドに変わります。
当然、戻り値は無いので、辛い辛い実行時エラーが待っていると思います。
Typescriptは継承元インターフェースのオーバーライドは出来ません。(当たり前っちゃーそうですが)
なので、引数と戻り値が変化するようなオーバーライドをする場合は、any回避以外に方法は無いと思います。そんなヘンテコなオーバーライドをする事は無いと思いますが……
ちなみに引数の追加もダメです。
オーバーライドしたメソッド内で継承元のメソッドを呼ぶ
先ほどの例でいえば、super.build(...)的な事をする、という事です。
Sequelizeのドキュメントにも書いてありますが、オーバーライドする関数内で
this.constructor.prototype.build.apply(this, arguments);
これでOKです。見たまんまですね。
同様にインスタンスメソッドを呼ぶ場合は
this.constructor.super_.prototype.toJSON.apply(this, arguments)
というような感じです。
オーバーライド時の影響範囲
これも当然かもしれないですが、オーバーライドしたいメソッド自身が、モデル内の他のメソッドから呼び出されている可能性を考慮しましょう。
たとえば、findAll()メソッドは、内部ではfind()を呼んでいるため、find()をオーバーライドした結果はfindAll()にも影響を与えます。
最終的には、sequelizeの該当コードを読む事になります。というか怖くなったので読みました。
Hooksもあるよ
Hooksにかなり丁寧に書いてあります。
要は任意の関数を渡しておくと、beforeValidate、afterValidate、といった風に、Sequelizeのモデルが行う処理の前後で実行させる事が出来るという物です。
例えば、createメソッドをオーバーライドして、作られたモデルのログを吐きたい、というような時は、代わりにHooksのafterCreateに関数を叩き込んでおいた方が安全です。
また、関数の配列に対応しており、その場合は頭から順に実行されます。
僕はまだあまり利用法を思いついていないですが、たとえばbeforeValidateで、パスワードを平文からハッシュに直すとかどうでしょうか。
Typescriptで記述する際の注意点
モデルの定義時に、アロー使って関数を書くと、コンパイル時にvar _this = this; がぶち込まれます。
当然、関数定義の呼び元はModel自身ではないため、まったく破綻したコードになってしまいます。
僕は思考停止でアローを使っていたため、thisをconsole.logするまで気づきませんでした。個人的には、thisを意識し直すいい機会になりました。
まとめ
モデルに独自メソッドを定義したり、既存のメソッドをオーバーライド出来るようになりました。
大胆なオーバーライドはinterfaceの都合上安全にやるのは難しいので、新しくメソッドを生やしたりしてお茶を濁すのが現実的かなと思います。
また、ちょっとメソッドを拡張したいな、という時、かなりの場合代わりにHooksで何とかなるんじゃないかと思ったので、記憶の片隅においておこうと思いました。