ひよくあブログ

Node.jsとか。

Sails.jsを使ってハマったところとか

この記事はNode.js Advent Calendar 2013の17日目の記事です。

先日勉強がてらNode.jsを使った簡単なWebページを作ってみようということで、ちょうど話題に上がっていたSails.jsを使ってみました。

その中で最初よくわからなかったことやハマったことを書こうと思います。

Sails.jsについて

Sails.jsはRuby on RailsのMVCを模倣して作られたNode.js用のフレームワークです。
ただRailsのようにフルスタックというわけではなく、Model-View-Controllerやそれぞれのジェネレータについては標準でサポートされていますが、テストフレームワークなどは提供されていません。
個人的には、特筆すべきものは以下の2つかと思います。

Realitime機能

ControllersとPoliciyが普通のHTTPリクエストだけでなく、Socket.io / WebSocketを自動的にハンドル可能となっていて、そういったアプリ開発と親和性が高いです。

i18nや認証やアクセスコントロールをビルドインでサポート

この辺はエンタープライズっぽい利用もちゃんと考えているなあという印象。

Modelの使い方

最初に詰まったのはModelの使い方でした。
Sails.jsのgithubのwikiにドキュメントがありますが、Sails.jsのORM部分はwaterlineという別モジュールとして開発をされていて、そちらの方のREADMEやテストを読んだ方が参考になります。
アダプターでMySQLやMongoDBなどを共通のインターフェースで使えるようにする、というコンセプトは面白いですが、まずはその共通インターフェースの学習をする必要があります。

DB操作系のAPIについてはcallbackとdeferredとpromiseと3つのインターフェースが提供されているのですが、最初それに気づかずにドキュメントを読む箇所によって書き方が変わっていて混乱をしてしまいましたw

よくわからなかったのが、Modelへのstaticメソッドとインスタンスメソッドの追加方法でした。試してみたところ、以下のように行うようです。

// api/models/User.js
module.exports = {

  attributes: {
    _id:         'integer', 
    name:     'string',
    // インスタンスメソッド
    getNickname: function() {
        return this.name || '名無し';
    },
  },
  // staticメソッド
  findNewUsers: function(cb) {
    this.find({
          limit: 20,
          sort: '_id desc'
    }, cb);
  }

};

勝手にエンドポイントを追加しないようにしたい

CRUDなエンドポイントなどを自動で作ってくれるのは便利なのですが、外部に公開するアプリの場合は忘れているとproductionで穴を開けたままリリースしてしまう可能性があるので、まず最初にオフにしました。
config/controllers.jsを開くとそれぞれのオプションについてコメントで解説をしてあるので、必要の無いものはオフにしておきましょう。

- shortcuts: true,
+ shortcuts: false,

- rest: true,
+ rest: false,

- expectIntegerId: false
+ expectIntegerId: false

expressのmiddlewareを追加したい

app.jsで変更できるののかと書いてみたのですが、反映されずググってみたところ、config/express.jsというファイルを追加して以下のような記述をすることでexpressのmiddlewareを追加することができました。

module.exports.express = {
  customMiddleware: function (app) {
    // 圧縮してレスポンスを返す
    app.use(require('../node_modules/sails/node_modules/express').compress());
  }
};

production環境と開発環境で設定を変えたい

見た感じconfig/local.jsでポートなどは変更できるようなのですが、DBの接続先などの変更方法がないように見えました。
production環境ではNODE_ENV=productionに環境変数を設定するので、例えばconfig/adapters.jsを以下のように切り替えています。

var dev = {
    'default': 'mongo',
    mongo: {/* development setting */}
};
var prod = {
    'default': 'mongo',
    mongo: {/* production setting */}
};
module.exports.adapters = (process.env.NODE_ENV == "production") ? prod : dev;

sails-mongoの罠

Sails.jsは標準だとMySQLですが、sails-mongoというMongoDB用のアダプタが提供されています。sails-mongoをnpm installしてconfigを書き換えれば、MySQLとも共通のインターフェースでさくっと使うことができます。
で、このアダプタで一つハマりました。 MySQLではデフォルトではアルファベットの大文字と小文字をSELECT時に区別しない(case-insentiveである)のですが、MongoDBは区別をされます。なので、例えば"Hatena"と保存をしていると"hatena"でMongoDBで検索しても引っ掛かりません。
簡単な回避策としては、MongoDBがクエリに正規表現が使えるので、db.collection.find({name: /^hatena$/i})のようにiオプションを使うことです。ただ、iオプションを使った場合はインデックスが使われなくなってしまうため、ある程度の規模のサイトを作るのであればこのやり方は適当ではありません。 で、sails-mongoなのですがMySQLとの互換性を保つために、findの条件の値にストリングを渡したときに/^hatena$/iを勝手にやってくれるという罠がありました。MongoDBでプロファイリングをやっていて、見覚えのないクエリが発行されていて調べていたら気づきました。こういうところが、ORMの怖いところですね。
対処法としては以下のように自前で正規表現でクエリを発行します。前方一致かつcase-insentiveでなければちゃんとインデックスが使われます。

User.find({
    name: new RegExp('^' + name + '$')
}, cb);

ユーザからの入力を受け付ける場合は上記のままだと正規表現に使えないものがあったりするので、以下のような感じでエスケープをする必要があります。(replaceの正規表現はsail-mongoのものを流用しています)

User.find({
    name: new RegExp('^' + name.replace(/[-[\]{}()+?*.\/,\\^$|#]/g, "\\$&") + '$')
}, cb);

使ってみての感想

Railsに比べてしまうと足りない機能は多いですが、なかなか完成度が高くてさくっとWebアプリケーションが作れるフレームワークになっているな、という印象です。ただ、設定やモジュールの実装方法などはSails.js独自のものが多いので、ちゃんと使うにはその辺の学習をする必要があるのですが、まだドキュメント不足な感じは否めないです。
sails-mongoというかModelのアダプターの考え方は嫌いではないのですが、MongoDBならではのクエリやAPIはたくさんあるので、ガチでMongoDBを使うことを考えると困ることが多そうです。なので、Sails.js+MongoDBでちゃんと作るなら、自前でORMもどきを作るか、Mongooseを使う事になりそうです。
今回はRealtimeな使い方はしなかったので次の機会には試してみたいと思っています。

ということで、これから使う人や現在ハマっている人にこの記事が参考になれば幸いです。