はじめに
mongooseを倒したと思ったら、諸般の事情でSequelizeを使っております。
TypeScriptでORMをいじるだなんて、考えただけで怖くなってきますが、とりあえず動き出したので備忘録として記述します。
Sequelizeとは??
http://sequelize.readthedocs.org
nodeで使えるORMです。ぼくはMariaDBに繋いで使っています。cliまでついてて、マイグレーション的な事も出来る大変高機能なモノとなっています。
型定義ファイルを読む
DefinityTypedに既にsequelize.d.tsが用意されております。やったね!
ひとまずモデル周りを読みましょう
sequelizeでは、コード内で
- Modelを定義する(define)
- 定義したModel間のリレーションとかを設定する
- 定義したモデルのインスタンスを作る(build)
という手順でモデルを定義、利用します。
定義したモデルをsync()すればDBにテーブルが作られ、作ったインスタンスをsave()すればインサートされ、find()やupsert()出来るようになるわけです。お手軽。
モデルを定義する
sequelizeのdefine()メソッドを呼んで定義します。サンプルを改変して書くとこんな感じです。そのまんまです。
var UserModel = sequelize.define('User', {
name: Sequelize.STRING,
age: Sequelize.INTEGER
});
まあだいたいこんな感じです。この時、UserModelの型がどうなっているのか、この辺の定義ファイルを見てみますと
define<TInstance, TPojo>(daoName: string, attributes: any, options?: DefineOptions): Model<TInstance, TPojo>;
と結構カオスな事になっております。
返り値の型は、Modelですね。Pojoって何なのか、調べるまで知りませんでした。なんか、プレーンなオブジェクトっぽい意味だった気がします。
まあつまり、インスタンスの型とPOJOの型を持ったジェネリックという事です。
TPojo is 何
このPOJO、d.tsの至る所に出てくるので、よくわからんままにしておく訳には行きません。
こいつが何者で、何に使われているのかさらに読み解いていくと
/**
* Builds a new model instance. Values is an object of key value pairs, must be defined but can be empty.
*
* @param values any from which to build entity instance.
* @param options any construction options.
*/
build(values: TPojo, options?: BuildOptions): TInstance;
なんかこんな感じの記述がありました。インスタンスが持つフィールドの集合のようです。
ジェネリック型の省略してはいけない
さて、Pojoが、モデルが持つフィールドの集合だとわかりました。
前述のUserModelにおけるnameとageですね。
読み進めていくと、InstanceにvaluesというTPojo型のプロパティが生えているため、先ほどの例で考えると
var instance = UserModel.build({name:'piyo', age:'17'});
console.log(instance.values.name);//'piyo'
のようなコンパイルが通るべきであると考えられますが、実際には以下のエラーが表示されます。
error| TS2339: Property 'values' does not exist on type '{}'.
どうやら、define時にジェネリック型の指定をさぼった結果、instanceが{}型と判断されているようです。
つまりinstanceの型が{}に暗黙的に判定されている訳です。
なので、define時にきちんと型の指定をしたい所なのですがTInstanceが足を引っ張ります。一旦Sequelize.Instanceを指定してごまかすか……と思いきや、そうはいきません。
TInstance is 何
TInstanceは、型定義ファイルを読む限りでは、buildした結果生まれるインスタンスの型を表しています。(build後のPromiseで渡って来たりする)
Sequelizeは高機能なORMなので、当然、モデルのインスタンスにも数多の便利メソッドが生えています。例えば、save(),reload(),ナドです。
これらは型定義ファイル内でInstanceとしてinterfaceが定義されています。
そのため一旦、これをTInstanceの所に指せば、便利メソッドがいろいろ生えていて、valuesにTPojo型のオブジェクトが刺さっているインスタンスが出来そうなのですが……
SequelizeのInstanceはInstance<TInstance, TPojo>と定義されている、つまりジェネリッククラスなのです。
このままInstance<Instance<Instance<……と書いたのでは無限に時間がかかってしまいますので、頭を使って打開策を考える必要があります。
ジェネリック型の継承
Mongooseの時の経験が役立ちました。
Instanceを継承した、UserInstanceを定義してやります。
ついでなので、Pojoの型も、UserParamsとして定義します。いちいちオブジェクト裏てテラル書くのはナンセンスなので。
interface UserParams {
name: string;
age: number;
}
interface UserInstance extends Sequelize.Instance<UserInstance, UserParams> {
}
こんな感じです。TInstanceに継承結果であるUserInstance自身を突っ込んでいるのがキモですね。
これで、TInstance、TPojoに渡すべき型が決まったため、ようやく正しいdefine文がかけます。
var UserModel = sequelize.define<UserInstance, UserParams>('User', {
name: Sequelize.STRING,
age: Sequelize.INTEGER
});
var instance = UserModel.build({name:'piyo', age:'17'});
console.log(instance.values.name);//'piyo'
instance.values.nameが生えていると、コンパイラが認識してくれました!
後述しますが、UserInstanceはUserParamsも継承した方がやりやすいです。
UserParamsをUserInstanceにも生やす
Sequelizeのインスタンスは、フィールドの値を取得する方法が幾つかあり
user.name;
user.get('name');
user.getDataValue('name');
user.values.name;
これらは同じ値を返します。なので、valuesを経由するよりも、インスタンス自身にTPojoが内包されていた方が都合が良いです。
従って、TInstanceに渡す型は、InstanceとParams、両方のインターフェースを継承した方が良いでしょう。
また、モデル毎に独自のメソッドを持つ場合、実装の定義自体はdefineでやるのですが、TInstanceで渡すインターフェースに定義を書いておかないと、コンパイラが怒り狂う事に注意です。
さらに言っておくと、Staticなメソッド等を定義する場合や、記述量の圧倒的な削減が見込める等の観点より、以下のようなModelも定義しておくと盤石です。
interface UserModel extends Sequelize.Model<UserInstance, UserParams> {
}
おまけ paramsの省略
このままだと、build時など、常にTPojoで定義した引数全てが要求されます。
柔軟性を残した場合は適宜?をつけて省略可能な、プロパティシグネチャにしてください。
まとめ
なんとか、独自モデルの定義にこぎ着けました。かなり頑張れば、TypescriptでもORMが使えますね。
Sequelizeはすごい高機能で便利なORMなようなので、これからも勉強していこうと思います。