pig's diary

何でも忘れるので万年初心者ね

苛烈!マングースは3度噛む

node.js のMongoDBドライバに Mongoose というものがあります。これの model.update 関数のコールバックの動きが分かりにくかったので、注意して様子を見てみました。

まず、ネイティブのREPLの動きを再確認します。

$ mongo
MongoDB shell version: 2.2.1
connecting to: test

// 新規データベースを作成
> use myBalanceBook
switched to db myBalanceBook

// 文書の作成
> db.girls.insert({name: 'honda tsubasa', spent: 500})  // 1つめの文書を作成
> db.girls.insert({name: 'mizuhara kiko', spent: 1500})  // 2つめ
> db.girls.insert({name: 'nounen rena', spent: 20})  // 3つめ

// 文書の書き換え
> db.girls.update({name: 'nounen rena'}, {xxx: 'xxx'})  // 選択した1つの文書を上書き
> db.girls.find()
{ "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 }
{ "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "name" : "mizuhara kiko", "spent" : 1500 }
{ "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" }  // 上書かれてしまった文書

// 文書にキーを追加
> db.girls.update({name: 'mizuhara kiko'}, {$set: {age: 22}})  // 選択した文書に、ageキーを追加
> db.girls.find()
{ "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 }
{ "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" }
{ "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 1500 }  // ageキーが追加された文書

// upsert(あればupdate、無ければinsert)
> db.girls.update({name: 'kojima fujiko'}, {name: 'kojima fujiko', spent: 4}, true)  // upsert(第3引数がtrue)
> db.girls.find()
{ "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 }
{ "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" }
{ "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 1500 }
{ "_id" : ObjectId("516a87be9a5e6b80d7b75284"), "name" : "kojima fujiko", "spent" : 4 }  // upsert(insert)された文書

// multi update (複数の文書を書き換え)
> db.girls.update({}, {$inc: {spent: 1000}}, false, true)  // multi(第4引数がtrue):セレクタにヒットする全ての文書に対し、変更を加える
                                                                                    // (ここでは、spentキーの値を1000インクリメントする)
> db.girls.find()  // 全ての文書のspentキーの値が1000増えている
{ "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 1500 }
{ "_id" : ObjectId("516a84f543b0013848d9a9ae"), "spent" : 1000, "xxx" : "xxx" }
{ "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 2500 }
{ "_id" : ObjectId("516a87be9a5e6b80d7b75284"), "name" : "kojima fujiko", "spent" : 1004 }
> 

注意すべき点はここです。

  • updateは、デフォルトでは1件のみを書き換える
  • multiフラグ(第4引数)がtrueの場合、セレクタ(第1引数)にヒットした全ての文書を書き換える

それでは、NodeからMongoDBを操作するドライバmongoose の update関数の動きを見て行きます。

ドキュメント: http://mongoosejs.com/docs/api.html#model_Model.update

予め、上記データベースのgirlsコレクションを操作するためのGirlModelを作成しておきます。

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/myBalanceBook');
var GirlModel = mongoose.model('girls', {
  name: String,
  spent: Number,
  cantAfford: Boolean,
  age: Number
})

それでは文書1件に対し、spentキーの値をインクリメントします。

GirlModel.update({name: 'mizuhara kiko'}, {$inc: {spent: 2000}}, function(err, numberAffected, raw) {
  console.log(err); // null
  console.log(numberAffected); // 1
  console.log(raw);
  // {
  //   updatedExisting: true,
  //   n: 1,
  //   connectionId: 48,
  //   err: null,
  //   ok: 1
  // }  
});

1件の文書を操作しました。コールバック関数の引数には何が渡って来るのでしょうか?
第1引数からは、通常のNodeのコールバックと同じように、エラーが起きた場合エラーオブジェクトが来ます。
第2引数は、操作した文書の件数です。通常は0 か 1 が来るはずです。後述するmultiフラグをtrueにしない限り、書き換えは最大1件に対してしか行わないからです。
第3引数は、rawとありますから、生データのようです。このうち、

  • updatedExisting: 何か1つでもセレクタにヒットしたらtrue
  • n: 操作された文書の件数(コールバックの第2引数numberAffectedと同じ)
  • connectionId: 知りません。どうせリクエストのUIDか何かでしょう。
  • err: rawで返されるエラーオブジェクトのようです。
  • ok: オッケーなら1、ダメなら0 のようです。試しにnumberAffected がゼロの結果でも、 ok は 1 でした。errと一体何が違うんだ。

続いて、mongooseから複数の文書をupdateしてみます。

GirlModel.update({
  spent: { $gt: 50 }
}, {
  $set: {cantAfford: true}
}, {
  multi: true
}, function(err, numberAffected, raw) {
  console.log(err); // null
  console.log(numberAffected); // 4
  console.log(raw);
  // {
  //   updatedExisting: true,
  //   n: 4,
  //   connectionId: 69,
  //   err: null,
  //   ok: 1
  // }  
});

mongooseでは、multiなどのオプションを第3引数にオブジェクトでまとめて渡します。そのためコールバック関数は第4引数になります。multiフラグをtrueにしたので、操作された文書の件数:numberAffected が 4 になっているのが分かります。ふむふむ。

書いていて気づいたのですが、言うほど分かりにくくはなかったです。

npm q べんり

qはPromises/A提案ベースの非同期管理ユーティリティです。
https://github.com/kriskowal/q/wiki/API-Reference
と思ってたら、今はPromises/A+ というものがあるようです。
http://promises-aplus.github.io/promises-spec/

qがあれば、非同期処理が増えてインデントが増えて読みにくくなったり、クロージャ構造で変数を参照し過ぎてリファクタリングしにくいコードになるのを、ある程度防ぐことができます。

以下は、同期処理と非同期処理を順番に処理していく例です。

var Q = require('q');

var step1 = function(v) {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve(v);
    }, 100);
    return d.promise;
};

var step2 = function(v) {
    console.log(v);
};

var step3 = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('ohh');
    }, 800);
    return d.promise;
};

// 同期的に 'yeah' という文字列を取得するところからスタート
Q.when('yeah')

// 非同期処理。引数に 'yeah' が渡ってくる。ここでは、100ms 後に、もういちど文字列 'yeah' を返す。
.then(step1)

// 同期処理。前のステップが終わってから実行される処理。ここでは、渡ってきた値を出力する。
.then(step2)

// 再び非同期処理。今度は、800ms後に文字列 'ohh' を渡す。
.then(step3)

// お終い。最後のステップで返される文字列 'ohh' が渡ってくる。
.done(function(v) {
    console.log(v, '... done. ');
});

上の例では非同期処理がただのsetTimeoutですが、これが「ファイルアクセス」や「HTTPリクエスト」等の非同期だったとして考えてみてください。

このように、返り値であるpromiseオブジェクトのthenメソッドで連ねていくと、1本道のステップを同期・非同期に関係無く簡単に作ることができます。

更に、qは「非同期処理を並列実行し、2つの処理が終わった直後に、2つの返り値を利用して処理を続行する」という複雑な処理も、簡単に書くことができます。

var Q = require('q');

// ステップ1のA。2000ms 後に、文字列を返す。
var step1A = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('result of step1A');
    }, 2000);
    return d.promise;
};

// ステップ1のB。700ms 後に、文字列を返す。
var step1B = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('result of step1B');
    }, 700);
    return d.promise;
};

// ステップ2。並列処理の結果を出力する。
var step2 = function(step1A_result, step1B_result) {
    console.log(step1A_result + ', ' + step1B_result);
}

// ステップ1A と、ステップ1B を並列実行する。
Q.allResolved([step1A(), step1B()])

// 両方の処理が終わったらすぐに、ステップ2 を実行する。
.spread(step2);

これを自前でやろうとすると、おかしなコードになりがちです。2つの並列処理の最後に、お互いの処理が既に終わっているかどうかをチェックし、trueなら第3の処理を続行。しかし2つの処理結果の引数を渡しにくいし、順序も分かりにくいし、コードも重複しがちです。さらに、並列処理が10個だとどうでしょうか? きっと何もかもが嫌になり家に帰って10時間眠りたくなるでしょう。qがあれば、安全かつ最速でステップを進めることができます。

安全に、と言えば、qはステップの途中に処理を離脱するコードも1カ所に書いておけるのも魅力です。

var Q = require('q');

// ランダムで、OKあるいはNGになるステップを作成。
var okOrNgStep = function() {
    var d = Q.defer();
    var timeout = Math.random() * 1000;
    setTimeout(function() {

        // 1:1 の確率で、OK あるいは NG と判断します。
        timeout > 500 ? d.resolve('More than 500!') : d.reject('timeout <= 500..');
    }, timeout);
    return d.promise;
};

Q.when()

// OKあるいはNGになるステップを実行。
.then(okOrNgStep)

// OKなら、こちらにステップが進む。
.then(function(msg) {
    console.log('OK! ', msg); 
})

// NGなら、こちらにステップが進む。
.fail(function(msg) {
    console.log('NG... ', msg);
})

// 最後には必ずここに来る。
.done(function() {
    console.log('... done.');
});

これは、ステップのどこかで起きた例外のハンドリングのためにとても有用です。仮に10ステップの長いフローがあったと考えてみてください。もしこの機能が無ければ、全てのステップの中に if (err) handleError(err); と書いておかねばならないでしょう。

q以外にも、非同期ユーティリティは他にもたくさんあるようです(https://github.com/joyent/node/wiki/modules#wiki-async-flow)。しかし私が触ったいくつかのnpmモジュールは qを使っていたので、比較的広く導入されているんじゃないかと思います。

個人的には、引数の取り方が柔軟過ぎる気もしますが、それほど問題にならないのかも知れません。

あなたは非同期管理をどうしていますか?

sass のmixin の省略記法、= と +

sass 3.2.0 でmixinの省略記法が導入されたようです。

=box-shadow($properties)
  -webkit-box-shadow: $properties
  -moz-box-shadow:    $properties
  box-shadow:         $properties

.my-box
  +box-shadow(2px 2px 2px #000)

上のsassをコンパイル

.my-box {
  -webkit-box-shadow: 2px 2px 2px black;
  -moz-box-shadow: 2px 2px 2px black;
  box-shadow: 2px 2px 2px black; }

いい!

参考:

Soy(Closure Template)で Object をイテレートする

keys() が去年の夏にサポートされたようです。
https://groups.google.com/d/msg/closure-templates-discuss/plF5xSdYCqs/Va1Eh_oVGv4J

/**
 * @param obj
 */
{template .printKeyValue}
    {foreach $key in keys($obj)}
        key...{$key}
        value...{$obj.key} or {$obj[$key]}
    {/foreach}
{/template}

Sencha Touch 2.1 のコードの雰囲気

この記事は、Sencha Advent Calendar 2012の12月08日の記事です。

残念ながら僕はSencha Touch のことを良く知らないので、初期化の流れとコードの雰囲気をぐだぐだと探索していきたいとおもいます。

ファイルを書き出します。

$ pwd
/Users/nomorerolling/git/sencha-touch-2.1

$ sencha generate app GS ../GS/public/
Sencha Cmd v3.0.0.250
[INF]		init-properties:
[INF]		init-sencha-command:
[INF]		init:
...
[INF]		generate-app:

$ cd ../GS/public
$ ls -a
drwxr-xr-x  11 nomorerolling  nomorerolling   374 12  8 14:54 .
drwxr-xr-x   8 nomorerolling  nomorerolling   272 12  8 14:56 ..
drwxr-xr-x   4 nomorerolling  nomorerolling   136 12  8 14:54 .sencha
drwxr-xr-x   7 nomorerolling  nomorerolling   238 12  8 14:54 app
-rw-r--r--   1 nomorerolling  nomorerolling  1368 12  8 14:54 app.js
-rw-r--r--   1 nomorerolling  nomorerolling  4972 12  8 14:54 app.json
-rw-r--r--   1 nomorerolling  nomorerolling  1234 12  8 14:54 build.xml
-rw-r--r--   1 nomorerolling  nomorerolling  1740 12  8 14:54 index.html
-rw-r--r--   1 nomorerolling  nomorerolling  5040 12  8 14:54 packager.json
drwxr-xr-x   7 nomorerolling  nomorerolling   238 12  8 14:54 resources
drwxr-xr-x  11 nomorerolling  nomorerolling   374 12  8 14:54 touch

サーバを起動してindex.html にアクセスし、ページが表示されることを確認。

ですが、何も分からないです。何がどうなってこのページが表示されてるのか。

ページのソースを見てみたら

  <head>
    ...
    <!-- The line below must be kept intact for Sencha Command to build your application -->
    <script id="microloader" type="text/javascript" src="touch/microloader/development.js"></script>
  </head>

スクリプトはこれだけ。あー、分かってきました。開発時モードなわけで、development.jsさんが、ファイルのパスを読み込んで、scriptタグを書き出すパターンですね。

じゃあ、ファイルパスはどこに書いてあるんでしょうか?

// touch/microloader/development.js

    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'app.json', false);
    xhr.send(null);

    var options = eval("(" + xhr.responseText + ")"),
        scripts = options.js || [],
        styleSheets = options.css || [],
        i, ln, path;

app.json に書いてあるみたいですね。同期通信、見慣れないな。

で、app.json を見てみると

    "js": [
        {
            "path": "touch/sencha-touch.js"
        },
        {
            "path": "app.js",
            "bundle": true,  /* Indicates that all class dependencies are concatenated into this file when build */
            "update": "delta"
        }
    ],
    ...
    "css": [
        {
            "path": "resources/css/app.css",
            "update": "delta"
        }
    ],

ありました。js[index].path。

ちょっと分かって来ました。現在開発モードで、読むファイルはこの順番。
1. development.js
2. app.json
3. touch/sencha-touch.js(15,000行)
4. app.js(50行)
次は、と。短い方から読もう。

app.js はExt.application 関数を実行しているだけ。巨大なオブジェクトを引数に与える。このobject ばっかりの感じがSencha っぽいイメージ。どこもかしこもオブジェクトリテラル

Ext.application({
    name: 'GS',

    requires: [
        'Ext.MessageBox'
    ],

    views: ['Main'],

ところで、全然関係ないけど、オプションを渡すたびに毎回オブジェクトを生成するのって遅くないんですかね?つまり、コンストラクタに渡すオプションオブジェクトを、毎回生成するよりも、どこか外に定義しておいて、使う時に参照するほうが速いんじゃないですかね?と思って。調べてみました。

https://gist.github.com/4239149

毎回オブジェクトを生成する方が、2倍速かった。なんで!?予想外。検証何か間違ってる?Node.js 使ったからかな?スコープを辿るのにもたついてるからか?・・・コンパイラがproductionコードをどうしているかは知らないが、とにかく、どうもSenchaのオブジェクトを逐次生成するスタイルは結構速そうだ。

まあいいや。app.jsで実行してる、Ext.application を見てみる。/touch/sencha-touch.js の9596行目。短い関数に、長いコメント。中で、Ext.app.Application をnew してるなぁ。

    application: function(config) {

        ...

        requires = Ext.Array.from(config.requires);
        config.requires = ['Ext.app.Application'];

        onReady = config.onReady;
        scope = config.scope;

        config.onReady = function() {
            config.requires = requires;
            new Ext.app.Application(config);

            if (onReady) {
                onReady.call(scope);
            }
        };

        Ext.setup(config);
    },

引数でわたって来たconfig.requires を、後で使うように細工してる。onReady とかscopre とかも。引数を渡したとたん、これだもんな。これがSenchaスタイルってわけですか。使い回すオブジェクトの値が、みるみるうちに書き換えられていく。で、新しいonReady のリスナらしき関数がセットされてる。この、config.onReady ってだれがcall するんだろう? というわけで、Ext.setup を見る。/touch/sencha-touch.js 9226行目。

Ext.setup長い・・200行くらいある・・とりあえず、コメントから。

    /**
     * Ext.setup() is the entry-point to initialize a Sencha Touch application. Note that if your application makes
     * use of MVC architecture, use {@link Ext#application} instead.

Ext.setup は、Ext.application と同じアプリケーションの初期化関数で、ユーザーはExt.setup を使ってもいいけど、MVCしたいならExt.application しちゃえば?と言ってるみたい。Ext.application はExt.setupをラップしてたのか。だからさっき引数オブジェクトに変な細工を。

で、今知りたいのは、Ext.setup が onReady をcall してるかどうか。どこかでcallしてるよね?

    setup: function(config) {
        var ...
            emptyFn = Ext.emptyFn,
            onReady = config.onReady || emptyFn,
            onUpdated = config.onUpdated || emptyFn,
            scope = config.scope,
            requires = Ext.Array.from(config.requires),

        ...

        delete config.requires;
        delete config.onReady;
        delete config.onUpdated;
        delete config.scope;

・・・ん、ちょっと関係無い所が気になる。delete してますよね。何でいちいち消してるんでしょう? きっと、変数config が参照するオブジェクトは存続し続けるので(new Ext.app.Applicationの引数にも使い回されてたし)オブジェクトのキーの中でも、生きてると分かりにくくなるキーや、生きてるとまずいキーを、不要になったそばから消してるんでしょうね。

管理を怠ると、すぐに分かりにくくなってしまいそうですね。1つのオブジェクトが、あらゆる初期化関数の引数に内部で使い回されています。デバッグで死ねそう。「この値、どこで入った!?」「この値、知らない間に書き変わってる!」などなど。ただSencha がカオスなのではなく、Javascriptがそうなんでしょうが。で、オブジェクトを使い回すなら、上のコードのように明示的に不要なキーをdelete していくのが、生き残るために非常に大事そう。そういうセルフサービス感がJavascript、面白いです。

で。onReady を探していたんだった。

Ext.onReady の値(関数)が魔術のごとく入れ替わっていくのは見て見ぬ振りをしておきつつ、onDocumentReady を待ち、Ext.require がスクリプトを読み込み(Ext.factoryConfig の中身は重要そうだな)、viewport 関連のセットアップを終えて、Loader#onReady、8383行目。

        // duplicate definition (documented above)
        onReady: function(fn, scope, withDomReady, options) {
            var oldFn;

ここで、Ext.setup にわたってきたonReadyが、documentReady と相談しながら、どちらもOKだったらようやくcall されるようだ。
このあと、new Ext.app.Application(config) され、app.js で Ext.application に渡した onReady が実行される。初期化の流れと、Sencha の雰囲気がちょっとだけ分かった気がする。

実は、最初はExt.define のコードを読んでいたのですが、最後まで良くわからないまま終わってしまいました。。Sencha 、奥が深そうです。Sencha Advent Calendar はまだまだ空きがあるようですので、興味のあるかたはぜひ執筆してください!

goog.ui.ThousandRows - 大量の一覧を軽快に表示するGoogle Closure Library モジュール

Closure Library で、大量に列のあるリストを軽快に表示できるコンポーネントを作りました。



デモ:http://stakam.net/closure/120722/
GitHubhttps://github.com/piglovesyou/closure-thousandrows

特徴

初期表示、スクロール時の動きが速いです。 

使えるケース

数千件〜の一覧の表示に力を発揮します。過去ログなど大量のデータを表示するときに使えるかも知れません。

使えないケース

  • リストが数十件程度の場合。遅延読み込みの必要がありません。
  • それぞれの列の高さがデザイン上違う場合は使えません。列は全て同じ高さである必要があります。これはコンポーネント側の制約です。

つくった経緯など

このコンポーネントは、既存のWebページに用いられていた「ページング」と「無限スクロール」の問題点を解決したデータの表示ができます。

そもそも、大量のデータを一度にWebページに表示しようとすると、どんな問題があるのでしょうか。サーバの負荷、データ転送量の増大、レンダリング時間の増大が挙げられます。レンダーツリーがあまりに多いと、スクロールなどの動作も重く、ユーザーストレスの原因にもなります。

よくある「ページング」は、一覧の上下に「1 2 3 ... 99」といった数字のリンクがあるデータの表示パターンです。分割して少ない量を読み込むため、サーバもクライアントも安心です。でも、ユーザーとしては毎回ページを読み込みし直すのが億劫です。それがAjaxになっても、「上から下に読んだ一覧を、また上から読み直さないといけない」のがちょっと残念です。

無限スクロールは、ずーっと下にスクロールして情報を見続けることができます。僕もTumblrを見てますが、はまると眺めているのがだんだん辞められなくなります。でも、だんだんページが重くなってくる問題があります。ページの上にDOMが残ったままなので、スクロール時のリフローコストが増大していってしまいます。

それらを解決したのが、ThousandRowsです。

スクロールしていくと通信し、DOMをappendして行くのは、無限スクロールと同じです。でも、不要な列を消していくので、ページが重くなることはありません。さらに、スクロールバーによって、無限スクロールにはない「下へのジャンプ」ができるようになりました。

でもひとつ制約があります。ロードしてないデータの列の高さを予測できないため、全ての列の高さを同じにしなければなりません。そこらへんから、ThousandRowsはiOSのUITableViewに近い動きをすると思っています。

大量の一覧データの表示に困っている方は、ぜひ利用してみてください。

goog.ui.Scroller - Google Closure Library モジュール

Closure Library でうごくスクローラーをつくりました。よく、スクロールバーのデザインをカスタムしたいときとかに使うやつです。
縦、横、両方でスクロールを実装できます。

デモ:http://stakam.net/closure/120618/
GitHub: https://github.com/piglovesyou/closure-scroller