けさらんぱさらん

方向性は定めず、ただ思いつくままに

JasmineでjQueryのajaxメソッドのテストをする

JavaScriptのテストライブラリJasmineでテストを書いていて嵌ったのでメモ

テストしたいコードはこんな感じ

function Details() {
    this.id = 0;
    this.name = "";
}

Details.prototype.getData = function (url, id) {
    var self = this;

    $.ajax({
        url: url,
        type: "GET",
        datatype: "json",
        data: { id: id },
    }).done(function (data) {
        self.id = data.Id;
        self.name = data.Name;
    }).fail(function (message) {
        alert(message);
    });
};

まあこんな感じのよくあるjQueryを使ったAjaxでデータを取得する処理です。

このgetDataメソッドをテストしたいのですが
サーバと通信してテストするんじゃなくてモックを使って通信は無しの方向で行きたいです。

調べてみるとオプション引数のsuccessを使う例は結構あったのですが、ajax.doneメソッドを使った例はあまり無かったんですよね
ということで( ..)φメモメモ

describe("Detailsクラスは", function () {
    var details = null;

    beforeEach(function () {
        details = new Details();
    });

    afterEach(function () {
        details = null;
    });

    it("getDataを呼び出すとidとnameに値が代入される", function () {
        spyOn($, "ajax").andCallFake(
            function () {
                var d = $.Deferred();
                d.resolve({ Id: 1, Name: "test1" });
                return d.promise();
            });
        details.getData("http://test.com",1);
        expect(details.id).toEqual(1);
        expect(details.name).toEqual("test1");
    });

    it("getDataでデータの取得に失敗するとAlertが表示される", function () {
        spyOn($, "ajax").andCallFake(
            function (pos) {
                var d = $.Deferred();
                d.reject("error");
                return d.promise();
            });

        details.getData("http://test.com",1);
    });
})

ぶっちゃけまだよく分かっていないのですが上記でちゃんと動きました。
まあ失敗するテストは、ちゃんとテストにはなっていないのですが・・・一応failメソッドがちゃんと呼ばれてました。

linq.jsとTypeScript

ちょっと調査したいことがあってlinq.jsを使ったんですが
間抜けなことで嵌ったので自戒を込めて書いておきます。

正しく動くコード

$(function () {
    $('#button').bind('click', function () {
        Enumerable.range(1, 30)
            .select(function (x) { return x.toString() })
            .where(function (x) { return x.indexOf("3") !== -1 })
            .forEach(function (x) { $('#result').html($('#result').html() + " " + x) });
    });
})

まあ3を含んだ数字を出力するというどうでもいいプログラムです。
(知りたかったことはC#同様に1つずつ列挙操作が行われるのかなんですけどね)
linq.jsは、VisualStudio上だとコード保管も聞くので非常に気持ち良く記述することが出来ます。
でも気持ちよすぎてついやっちゃうんですよ

$(function () {
    $('#button').bind('click', function () {
        Enumerable.range(1, 30)
            .select(function (x) { x.toString() })
            .where(function (x) { x.indexOf("3") !== -1 })
            .forEach(function (x) { $('#result').html($('#result').html() + " " + x) });
    });
})

returnが無い・・・
functionは忘れなかったんですけどねw
そして構文上はエラーでは無いのでVSは怒ってくれない。
whereの中のindexOfが実行時エラーになるのですが、しばし悩みましたね。 はい。

やっぱLINQには、ラムダ式が必要だ~っと思ったら神の声がw
「TypeScriptを使いなさい」
そういえばそんな手がw

module Sample {

    export class Counter {

        static count() {
            Enumerable.range(1, 30)
                .select( x =>  x.toString() )
                .where( x => x.indexOf("3") !== -1 )
                .forEach( x => $('#result').html($('#result').html() + " " + x) );
        }
    }

}

これは気持ち良すぎるね
まだPreviewだけど使っちゃおうかな~

KnockoutjsでTwitter風の表示をするには?

KnockoutjsでTwitter風の表示を作りたいと試行錯誤してみました。
とりあえずこうゆう風に表示できればOK
f:id:cer1974:20130114140535p:plain
つぶやきの一覧の中に更に返信の一覧が表示できるようにしたい。


Knockoutjsのバージョンは2.2.0を使います。


サーバ側のソース

Model
/// <summary>
/// つぶやき
/// </summary>
public class Tsubuyaki
{
    public int Id { get; set; }
    public string UserName { get; set; }
    public string Comment { get; set; }
    public bool ExistReply { get; set; }
    public string CreatedAt { get; set; }

    public IEnumerable<Reply> Replies { get; set; }
}

/// <summary>
/// 返信
/// </summary>
public class Reply
{
    public int Id { get; set; }
    public int ParentId { get; set; }
    public string UserName { get; set; }
    public string Comment { get; set; }
    public string CreatedAt { get; set; }
}
Controller
/// <summary>
/// 最初にデータが無い画面だけ取得
/// </summary>
/// <returns></returns>
public ActionResult Index()
{
    return View();
}

/// <summary>
/// つぶやき情報をjsonで取得
/// </summary>
/// <returns></returns>
public ActionResult ContentIndex()
{
    var tsubuyakis = new Tsubuyaki[]{
        new Tsubuyaki{ 
            Id = 1,
            UserName = "Bob", 
            Comment = "Hello",
            CreatedAt = DateTime.Now.ToString( "yyyy/MM/dd HH:mm:ss" ),
            Replies = new List<Reply>(),
        },
        new Tsubuyaki{ 
            Id = 2,
            UserName = "たかし", 
            Comment = "こんにちは^^", 
            CreatedAt = DateTime.Now.AddMinutes( -1 ).ToString( "yyyy/MM/dd HH:mm:ss" ),
            ExistReply = true,
            Replies = new Reply[]{
                new Reply{ UserName = "たろう", Comment = "ども", CreatedAt = DateTime.Now.AddMinutes( 10 ).ToString( "yyyy/MM/dd HH:mm:ss" ) },
                new Reply{ UserName = "じろう", Comment = "おっす", CreatedAt = DateTime.Now.AddMinutes( 12 ).ToString( "yyyy/MM/dd HH:mm:ss" ) }
            }.OrderByDescending( x => x.CreatedAt ),
        },
        new Tsubuyaki{ 
            Id = 3,
            UserName = "Nick", 
            Comment = "Hi!", 
            CreatedAt = DateTime.Now.AddMinutes( -3 ).ToString( "yyyy/MM/dd HH:mm:ss" ),
            Replies = new List<Reply>(),
        },
                
    }.OrderByDescending( x => x.CreatedAt );

    return Json( tsubuyakis, JsonRequestBehavior.AllowGet );
}

クライアント側のソース

サーバ側は、ほぼどうでも良く今回の問題はクライアント側です。

HTML
<div id="timeline" data-bind="foreach: tsubuyakis">
    <div class="tsubuyaki">
        <div data-bind="foreach: Replies">
            <div class="replies" >
                <div data-bind="text: UserName"></div>
                <div data-bind="text: Comment"></div>
                <div data-bind="text: CreatedAt"></div>
            </div>
        </div>
        <div data-bind="attr: { id: Id }, text: UserName"></div>
        <div data-bind="text: Comment"></div>
        <div data-bind="text: CreatedAt"></div>
    </div>
</div>

つぶやきのループの中に返信のループがネストする構造です。

Javascript
<script type="text/javascript">
    $(function() {

        function viewModel() {
            var self = this;
                
            self.tsubuyakis = null;
        };

        $.ajax({
            type: "GET",
            url: "/Tsubuyaki/ContentIndex",
            dataType: 'json',
            success: function(data) {
                var v = new viewModel();
                v.tsubuyakis = ko.observableArray(data);
                ko.applyBindings(v);
            },
        });
    });
</script>

Indexメソッドで画面だけ取得してあとから別途Jsonで情報を取得するようにしました。
最初は返信も一度に返しちゃいます。
その結果が一番上の画像です。


最新のつぶやきを取得する

まずタイムラインを更新して新しいつぶやきを追加できるようにしてみます。

これが
f:id:cer1974:20130114152529p:plain
[最新のつぶやきを取得]リンクを押すとこうなる(返信は消しました)
f:id:cer1974:20130114152606p:plain

Controller

Controllerにつぶやきを追加できるようにメソッドを追加します。

public ActionResult NextTsubuyakis()
{
    var tsubuyakis = new Tsubuyaki[]{
        new Tsubuyaki{ 
            Id = 4,
            UserName = "きよし", 
            Comment = "あうあう",
            CreatedAt = DateTime.Now.AddMinutes( 20 ).ToString( "yyyy/MM/dd HH:mm:ss" ),
            Replies = new List<Reply>(),
        },
        new Tsubuyaki{ 
            Id = 5,
            UserName = "さとし", 
            Comment = "ねもい", 
            CreatedAt = DateTime.Now.AddMinutes( 25 ).ToString( "yyyy/MM/dd HH:mm:ss" ),
            Replies = new List<Reply>(),
        },
                
    }.OrderBy( x => x.CreatedAt );

    return Json( tsubuyakis, JsonRequestBehavior.AllowGet );
}
HTML

リンクを追加

<a href="#" data-bind="click: getNextTsubuyakis">最新のつぶやきを取得</a>
Javascript

ViewModelにメソッドを追加

self.getNextTsubuyakis = function() {
    $.ajax({
        type: "GET",
        url: "/Tsubuyaki/NextTsubuyakis",
        dataType: 'json',
        success: function(data) {
            $.each(data, function() {
                self.tsubuyakis.unshift(this);
            });
        }
    });
}

ViewModelのtubuyakisにデータを追加します。
これだけでつぶやき一覧が更新され上記の図のような状態になります。


つぶやきの返信を取得する

Controller

Controllerに返信が取得できるメソッドを追加します。

public ActionResult ReplyIndex(int Id)
{
    var replies = new Reply[]{
                new Reply{ UserName = "さぶろう", Comment = "・・・", CreatedAt = DateTime.Now.AddMinutes( 15 ).ToString( "yyyy/MM/dd HH:mm:ss" ) },
                new Reply{ UserName = "しろう", Comment = "ちゃお!", CreatedAt = DateTime.Now.AddMinutes( 20 ).ToString( "yyyy/MM/dd HH:mm:ss" ) }
            }.OrderBy( x => x.CreatedAt );
            
    return Json( replies, JsonRequestBehavior.AllowGet );
}
HTML

各つぶやきにリンクを付けますが、返信があるものだけにリンクを付けるように判定をします。

<div id="timeline" data-bind="foreach: tsubuyakis">
    <div class="tsubuyaki">
        <div data-bind="foreach: Replies">
            <div class="replies" >
                <div data-bind="text: UserName"></div>
                <div data-bind="text: Comment"></div>
                <div data-bind="text: CreatedAt"></div>
            </div>
        </div>
        <div data-bind="attr: { id: Id }, text: UserName"></div>
        <div data-bind="text: Comment"></div>
        <div data-bind="text: CreatedAt"></div>
        <div data-bind="if: ExistReply">
            <a href="#" data-bind="click: $parent.getReply">会話を表示</a>
        </div>
    </div>
</div>
Javascript

リンクがクリックされたときのイベントをViewModelに追加します。

self.getReply = function(d, e) {

    $.ajax({
        type: "GET",
        url: "/Tsubuyaki/ReplyIndex",
        data: { id: d.Id },
        dataType: 'json',
        success: function(data) {
            $.each(data, function() {
                d.Replies.unshift(this);
            });
        },
    });
};

でもこのまま実行してもうまく行きません。
ajaxメソッドは呼ばれているのでそこはOKなんですが
Kockoutjsのメソッドが呼ばれていないのでうまくバインドできていないようです。

VisualStudioデバッグしてみるとコレクションには登録されているんですけどね
f:id:cer1974:20130114213024p:plain


withを使うのかなーと思ってこんなんしてみましたが・・・

<div data-bind="with: Replies">
    <div data-bind="foreach: $data">
        <div class="replies" >
            <div data-bind="text: UserName"></div>
            <div data-bind="text: Comment"></div>
            <div data-bind="text: CreatedAt"></div>
        </div>
    </div>
</div>

・・・だめでした。

どなたか分かれば教えていただけると助かります。

自己解決しました。
Javascript載せておきます。

$(function() {

    function viewModel() {
        var self = this;
                
        self.tsubuyakis = ko.observableArray();

        self.getNextTsubuyakis = function() {
            $.ajax({
                type: "GET",
                url: "/Tsubuyaki/NextTsubuyakis",
                dataType: 'json',
                success: function(data) {
                    $.each(data, function() {
                        this.Replies = ko.observableArray();
                        self.tsubuyakis.unshift(this);
                    });
                }
            });
        }

        self.getReply = function(d, e) {

            $.ajax({
                type: "GET",
                url: "/Tsubuyaki/ReplyIndex",
                data: { id: d.Id },
                dataType: 'json',
                success: function(data) {
                    $.each(data, function() {
                        d.Replies.unshift(this);
                    });

                },
            });
        };
    };

    var tsubuyakiViewModel = new viewModel();

    $.ajax({
        type: "GET",
        url: "/Tsubuyaki/ContentIndex",
        dataType: 'json',
        success: function(data) {
            $.each(data, function() {
                this.Replies = ko.observableArray();
                tsubuyakiViewModel.tsubuyakis.push(this);
            });

            ko.applyBindings(tsubuyakiViewModel);
        },
    });
});

ポイントは、Repliesをko.observableArray()で初期化すること
まあ出来てしまえば当たり前のことでしたね・・・たどり着くまでに時間かかったわ~

mongoDBでASP.NETのGridViewのソートとページングを試してみる

先日のVSUGこちらのブログで@onosさんが紹介されている
ASP.NETの新しい機能がとっても良さそうだったので
mongoDBでもやってみました。


Memberクラスを作成します

public class Member
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Mail { get; set; }
}

あっとMongoDriverをNuGetでインストールしておきましょう

install-package mongocsharpdriver

適当にデータを登録しておきます。
f:id:cer1974:20121221233410p:plain

mongoDBからデータを取得します。
IQueryableで戻り値を返します。

protected IQueryable<Member> SelectMembers()
{
    MongoDatabase db = new MongoClient( "mongodb://127.0.0.1" ).
        GetServer().GetDatabase( "aspnet_sample" );
    MongoCollection<Member> collection = 
        db.GetCollection<Member>( "members" );

    return collection.AsQueryable();
}

GridViewを配置してSelectMethodに上記で作成したSelectMembersを設定します。

<form id="form1" runat="server">
    <div>
        <asp:GridView ID="GridView1" runat="server"SelectMethod="SelectMembers"></asp:GridView>
    </div>
</form>

実行します。
全件表示されました。
f:id:cer1974:20121221235859p:plain

次にソートとページングの設定を加えます。
件数が少ないのでページサイズを3にしておきます。

<form id="form1" runat="server">
    <div>
        <asp:GridView ID="GridView1" runat="server" SelectMethod="SelectMembers"
            AllowSorting="true" AllowPaging="true" PageSize="3"></asp:GridView>
    </div>
</form>

@onosさんのブログでは、ここで例外になっていましたが
こちらはうまく行ってしまいました。はて?
f:id:cer1974:20121224215842p:plain

MongoDriverのソースを見てみるとMongoQueryableはIOrderQueryableを実装しているので
例外にならないんですね~ なるほど!

public class MongoQueryable<T> : IOrderedQueryable<T>

ページングしてみます。
f:id:cer1974:20121224225208p:plain

ソートしてみます。
f:id:cer1974:20121224225400p:plain

どちらも出来てますね。すばらしい!
こんなに簡単にソートとページングが出来るなんて!

ここで疑問なのは、どこでソートとか処理してるのって話ですね
全件取得してクライアント側でやってる?
まあそれは無いよね。でも一応、見てみる。
f:id:cer1974:20121224230128p:plain
大丈夫そうですね。(当たり前ですかね

送信情報も確認してみましょう
f:id:cer1974:20121224230633p:plain
はいちゃんと呼んでますね

mongoDBのログも見てみましょう
ページング
f:id:cer1974:20121224232330p:plain
Countしてますね。これは総件数でも取ってるのかな?
SkipとかTake的なものが無い感じ?
でも何故か3件しか返してないですね。

全件取るとこんなログになる
f:id:cer1974:20121224232909p:plain

あえてSkipとTakeを入れるとこんな感じ
f:id:cer1974:20121224233939p:plain
ちゃんとSkipとLimit(LINQではTakeだけど)されてますね~
ちょっと分からないですね

ソート
f:id:cer1974:20121224234441p:plain
こちらは、想定通りにログが出てますね

ちょっとmongoDBのログの見方が分かっていないので微妙なところもありますが
ASP.NETのソートとページングは、当たり前ですがちゃんとDBから取得してるってことですかね
まあmongoDBでもちゃんと出来ましたよって話でした!

mongoDBもLINQで行こう!

この記事は、C# Advent Calender 2012 の13日目の記事です。
前日の記事は、@masakさんのC# 5.0 で変わった事 - foreach の破壊的変更です。

自分はあまり深いことは書けませんので最近ちょっと触り始めたmongoDBを操作する方法を紹介したいと思います。

始めに

mongoDBのバージョンは2.2.0でWindowsにインストールしています。
C#ドライバは1.7を使用します。(特に1.7でないと出来ないようなことはしていないです)

ざっくりmongoDBとは

突然、mongoDBをLINQで検索してもmongoDBが良く分からない方もいると思うので
まず簡単にmongoDBってなんなのって話。
mongoDBは、所謂NoSQLに分類されるデータベースです。
ドキュメント指向DBでスキーマレスなのです。
データはBSONと呼ばれるJSONちっくな構造で格納されます。
レプリケーションやシャーディングという機能でサーバ間に分散してデータを持つことができます。
ざっくり説明終わり!(ざっくり過ぎてすいません)

スキーマレスってどゆこと?

スキーマレスというのはスキーマが無いってことです。(あう、そのまま
RDBだと必ず存在するテーブル定義が無いんです。
分かりづらいですね
実際に見てみましょう!

まず何もコレクション(RDBで言うところのテーブル)が無い状態を確認する。

$ mongo
use advent_sample

f:id:cer1974:20121205233132p:plain
なにもDBが無い時にuse [データベース名]をするとデータベースが作成されます。

コレクションが存在するか確認してみます。

show collections


何も表示されないですね!

ここでRDBだとCreate文を流してテーブルを作る。
でもmongoDBは、ここでいきなりデータを挿入しちゃう

db.samples.insert( {name: "aaa", age: 20} );
show collections


テーブル定義的なものが無くていきなりデータが挿入できちゃいました。
更にデータを追加しますが今度はフィールド(RDBで言うこところのカラム)を増やして追加します。

db.samples.insert( {name: "bbb", age: 30, tel: "00-0000-0000"} );
db.samples.find();


find()メソッドで検索すると2ドキュメント表示されます。
2つのドキュメントで定義が異なるのが分かります。
ということでスキーマレスなんですね

今回使うデータ構造

商品の購入システムみたいなものにしてみます。
「注文」と「注文明細」があって「注文」は「注文明細」を複数持ちます。
「注文」は「会員」を参照するようにします。

クラス図だとこんな感じ
f:id:cer1974:20121205233717p:plain

MongoDriverのインストール

C#からmongoDBに接続するためには、公式からリリースされているMongoDriverを使用します。
MongoDriverは、NuGetで取得できるので簡単にインストールできます。

Install-Package mongocsharpdriver

まあGUIでやった方が楽ですけどね

モデルクラス

注文クラス
[BsonIgnoreExtraElements]
public class Order
{
    public BsonObjectId Id { get; set; }
    public DateTime OrderAt { get; set; }

    public ICollection<OrderDetail> OrderDetails { get; set; }
    public int MemberId { get; set; }
        
    [BsonIgnore]
    public Member Member { get; set; }
}

注文クラスは、注文明細(OrderDetail)をコレクションで持ちます。
会員(Member)は、mongoDB上、注文に含まれるわけでは無いので BsonIgnore を付けてマップ対象から除外します。

注文明細クラス
public class OrderDetail
{
    public string ItemName { get; set; }
    public int ItemCount { get; set; }
    public int SubTotal { get; set; }
}
会員クラス
public class Member
{
    public BsonObjectId Id { get; set; }
    public int Id { get; set; }
    public string MemberName { get; set; }
    public string EMail { get; set; }
}

データを挿入してみる

まずマスタとなるMemberを使ってデータを挿入してみます。

[TestMethod]
public void Member_Insert()
{
    MongoDatabase db = new MongoClient( "mongodb://127.0.0.1" ).GetServer().GetDatabase( "advent_sample" );
    MongoCollection<Member> memberCollection = db.GetCollection<Member>( "members" );

    WriteConcernResult result = memberCollection.Insert( 
            new Member { MemberId = 1, MemberName = "memberA", EMail = "memberA@example.com" } 
        );

    Assert.IsTrue( result.Ok );
}

さて挿入されたかmongoShellで確認してみます。
f:id:cer1974:20121209124745p:plain
ちゃんと出来ていますね。BsonIdは自動で振られます。

今度はデータ取得してみます。

[TestMethod]
public void Member_First()
{
    // Collectionを取得するところは煩雑なのでメソッド化
    var memberCollection = GetCollection();

    Member member = memberCollection.FindOne();

    Assert.IsNotNull( member );

    Console.WriteLine( String.Format( "bsonId:{0} memberId:{1}, name:{2}, emaile:{3}",
        member.Id, member.MemberId, member.MemberName, member.EMail ) );
}

出力結果を確認します。
f:id:cer1974:20121209212712p:plain
取れてますね。OK!

OrderとOrderDetailを挿入するためにデータを作成します。
mongoDBは、ドキュメント内にネストしてドキュメントを持てるので
Orderの中にOrderDetailを入れておいてOrderの挿入を行います。

[TestMethod]
public void Order_Insert()
{
    MongoDatabase db = new MongoClient( "mongodb://127.0.0.1" ).GetServer().GetDatabase( "advent_sample" );
    MongoCollection<Order> orderCollection =  db.GetCollection<Order>( "orders" );

    Order order = new Order
    {
        OrderAt = DateTime.Now,
        MemberId = 1,
        OrderDetails = new[]{
            new OrderDetail{ ItemName="テレビ", ItemCount=1, SubTotal=50000},
            new OrderDetail{ ItemName="DVD", ItemCount=5, SubTotal=15000},
        }
    };

    WriteConcernResult result = orderCollection.Insert( order );

    Assert.IsTrue( result.Ok );
}

mongoShellで確認してみます。
f:id:cer1974:20121209142901p:plain
ちゃんと挿入されていますね
分かりにくいですが、1つのドキュメントにネストしてドキュメントが存在しています。

C#からも取得してみます。

[TestMethod]
public void Order_First()
{
    // Collectionを取得するところは煩雑なのでメソッド化
    var orderCollection = GetCollection();

    Order order = orderCollection.FindOne();

    Assert.IsNotNull( order );
    Assert.IsTrue( order.OrderDetails.Any() );

    Console.WriteLine( String.Format( "bsonId:{0} orderAt:{1}, memberId:{2}",
        order.Id, order.OrderAt, order.MemberId ) );

    order.OrderDetails.ToList().ForEach( x => 
        Console.WriteLine( String.Format( "itemName:{0} itemCount:{1} subToal:{2}",
        x.ItemName, x.ItemCount, x.SubTotal ) ) );
}

f:id:cer1974:20121209213153p:plain
受注詳細のデータもちゃんと取得されてオブジェクトへのマッピングも出来てますね。

準備はここまでで終わりです。

データを検索してみる

はぁ~長くってすいません。
ようやくLINQの出番ですよ!

挿入したデータでいろいろと試してみます。
※データ適当に増やしています。

mongoDBでLINQを使うには、MongoDB.Driver.Linq名前空間をusingする必要があります。


まずは、普通にWhereメソッドでもやってみましょう。
会員IDが1の人の購入履歴を取得する。

[TestMethod]
public void Order_Where_Member_1()
{
    var orderCollection = GetCollection();

    var orders = orderCollection.AsQueryable().Where( x => x.MemberId == 1 ).ToList();

    orders.ForEach( x =>
            {
                WriteOrder( x );
                x.OrderDetails.ToList().ForEach( y => WriteOrderDetail( y ) );
            }
        );
}

結果
f:id:cer1974:20121209223952p:plain


Queryableなので当然といえば当然ですが、遅延評価されますので後から条件を追加することも出来ます。
会員IDが3で2012年12月9日の受注を一覧しますが
会員IDの条件と日付の条件は別なWhereメソッドで記述します。

※クエリ部分だけ記述します。

var temp = orderCollection.AsQueryable().Where( x => x.MemberId == 3 );
var orders = temp.Where( x => x.OrderAt >= new DateTime( 2012, 12, 9 ) && x.OrderAt < new DateTime( 2012, 12, 10 ) ).ToList();

結果
f:id:cer1974:20121210233659p:plain
結果としては正しいですね。でも1クエリで問い合わせたかは分からないのでログを確認してみます。

Mon Dec 10 23:31:41 [conn12] query advent_sample.orders query: { MemberId: 3, OrderAt: { $gte: new Date(1354978800000), $lt: new Date(1355065200000) } } ntoreturn:0 keyUpdates:0 locks(micros) r:182 nreturned:2 reslen:401 0ms

1クエリになってますね。

ネストされたドキュメント側のフィールドでも検索できます。

var orders = orderCollection.AsQueryable()
    .Where( x => x.OrderDetails.Any( y => y.ItemName == "CD" ) ).ToList();

結果
f:id:cer1974:20121211000053p:plain

ログはこんな感じ

Mon Dec 10 23:55:32 [conn16] query advent_sample.orders query: { OrderDetails: { $elemMatch: { ItemName: "CD" } } } ntoreturn:0 keyUpdates:0 locks(micros) r:214 nreturned:3 reslen:508 0ms

ちょっとデータを絞ってみます。上記から会員IDが3の人だけにします。

var orders = orderCollection.AsQueryable()
  .Where( x => x.MemberId == 3 && x.OrderDetails.Any( y => y.ItemName == "CD" ) ).ToList();

結果
f:id:cer1974:20121211000740p:plain

ログ

Tue Dec 11 00:03:22 [conn17] query advent_sample.orders query: { MemberId: 3, OrderDetails: { $elemMatch: { ItemName: "CD" } } } ntoreturn:0 keyUpdates:0 locks(micros) r:225 nreturned:2 reslen:382 0ms

こんな感じで普通のフィルタ処理であれば簡単にできます。


会員マスタと連結もしてみたいと思います。

[TestMethod]
public void Member_Reference()
{
    var orderCollection = GetCollection();
    var memberCollection = GetMemberCollection();

    var orders = orderCollection.AsQueryable()
        .Where( x => x.MemberId == 3 )
        .Join( memberCollection.AsQueryable(), o => o.MemberId, m => m.MemberId, ( o, m ) => new { o, m } )
        .ToList();

    // 適当ですいません;;
    orders.ForEach( x =>
        {
            Console.WriteLine( String.Format("{0},{1},{2},{3}", x.o.Id, x.o.MemberId, x.m.MemberName, x.m.EMail ));
        });
}

結果
f:id:cer1974:20121212002531p:plain
あふん!
Joinはサポートされていないというつれないエラー。
まあLINQでなくても元々mongoDBはJOINをサポートしていないですから当然の結果かと思います。
※MongoDBRefというクラスを使えばクライアント側でのJOINは可能なようです。

まあこのマスタとの結合はアプリ側でやってしまうのが良いのではないかと

[TestMethod]
public void Member_Reference2()
{
    var orderCollection = GetCollection();
    var memberCollection = GetMemberCollection();

    // 本来はキャッシュしたりする
    var members = memberCollection.FindAll();

    var orders = orderCollection.AsQueryable()
        .Where( x => x.MemberId == 3 ).AsEnumerable()
        .Join( members, o => o.MemberId, m => m.MemberId, ( o, m ) => { o.Member = m; return o; } )
        .ToList();

    // 今度は出力されるはずだからちゃんと書こう
    orders.ForEach( x =>
    {
        Console.WriteLine( 
            String.Format( "memberId:{0},memberName:{1},email:{2}", 
                x.Member.MemberId, x.Member.MemberName, x.Member.EMail ) );
        x.OrderDetails.ToList().ForEach( y => WriteOrderDetail( y ) );
    } );
}

結果
f:id:cer1974:20121212162834p:plain
今度はちゃんと結果を出力することが出来ました。

ログ

Wed Dec 12 16:25:38 [conn4] query advent_sample.orders query: { MemberId: 3 } ntoreturn:0 keyUpdates:0 locks(micros) r:154 nreturned:10 reslen:2438 0ms

ログを見るとMemberIdでの絞り込みしかされていません。Joinはアプリ側でやっていることがわかります。


mongoDBは、正規表現で検索することができます。
会員のメールアドレスがaから始まるco.jpドメインのものを検索します。

var member = memberCollection.AsQueryable()
        .Where( x => Regex.IsMatch( x.EMail, @"^a.*@.*co\.jp$" ) ).ToList();

結果
f:id:cer1974:20121212213147p:plain

ログ

Wed Dec 12 21:29:17 [conn8] query advent_sample.members query: { EMail: /^a.*@.*co\.jp$/ } ntoreturn:0 keyUpdates:0 locks(micros) r:6536 nreturned:1 reslen:110 6ms

データを集計してみる


まずはカウントでも

var orderCount = orderCollection.AsQueryable()
    .Where( x => x.OrderAt >= new DateTime( 2012, 12, 9 ) && x.OrderAt < new DateTime( 2012, 12, 10 ) )
    .Count();
}

結果
f:id:cer1974:20121211235833p:plain

ログ

Tue Dec 11 23:56:36 [conn2] command advent_sample.$cmd command: { count: "orders", query: { OrderAt: { $gte: new Date(1354978800000), $lt: new Date(1355065200000) } } } ntoreturn:1 keyUpdates:0 locks(micros) r:139 reslen:48 0ms

まあこれでも結果はいっしょです。

var orderCount = orderCollection.AsQueryable()
    .Count( x => x.OrderAt >= new DateTime( 2012, 12, 9 ) && x.OrderAt < new DateTime( 2012, 12, 10 ) );

結果
f:id:cer1974:20121212000626p:plain

ログ

Wed Dec 12 00:04:11 [conn3] command advent_sample.$cmd command: { count: "orders", query: { OrderAt: { $gte: new Date(1354978800000), $lt: new Date(1355065200000) } } } ntoreturn:1 keyUpdates:0 locks(micros) r:148 reslen:48 0ms

ログ見るとクエリ的には同じものですね


合計値を求めてみます。

var sum = orderCollection.AsQueryable()
        .Sum( x => x.OrderDetails.Sum( y => y.SubTotal ) );

結果
f:id:cer1974:20121212215907p:plain
あら?例外ですね

Sumは実装されてないみたいですね
mongoDB自体も2.2.0で漸くSumとかできるようになったみたいです。ドライバの方はまだ実装されていないのかな?

var sum = orderCollection.AsQueryable()
    .Select( x => x.OrderDetails.Sum( y => y.SubTotal ) )
    .AsEnumerable().Sum();

これで出来ることは出来るのですが、全件アプリ側に持ってきてからの処理になるので
件数が増えたら死ねますね
※MapReduceという機能で集計はできるはずです。また別途、書ければ書きます。

最後に

mongoDBは、RDBでは難しいことが簡単に出来たりします。(逆に出来ないこともあります)
要件に依っては、RDBよりもmongoDBを使ったほうがうまくいくこともあると思いますので
興味があれば是非、試してみてください。
(プロトタイプとか作るときは、スキーマレスはうれしいですよね)
特にLINQが使えるのでC#erは、すぐに扱えるようになるんではないでしょうか?

なんかLINQメソッドもまだいっぱいあるのに中途半端な記事になってしまった感が否めませんが
詳しく知りたい方は、公式サイトをどうぞ

まあ何にせよ無事、折り返せたのでなによりです。
明日は、@takeshikさんです。よろしくお願いします。

TypeScriptでjQueryプラグイン

TypeScriptで書いていて疑問に思うことがあるんすよね〜
jQueryのプラグインをTypeScriptで扱い方が分からない!

$('#modal').modal();

ってやるとコンパイル通らないんですよね
(当たり前ですけど)


tsファイルがあれば

/// <reference path="hoge.ts"/>

これで良いんですけどね


自分でtsファイルも作れば良いのですが面倒くさいので
tsファイル作らないでやる方法です。

var bs_modal: any = $('#modal');

bs_modal.modal({ keyboard: false });
bs_modal.modal('hide');


通常であればプラグイン自体のオブジェクトをanyで作成すれば
コンパイルエラーにならないんですけど
jQueryプラグインの場合は、jQueryオブジェクトを拡張する形で作られているので
jQueryオブジェクトをanyで受けてそこからプラグインのメソッドを生やす感じで
作ればおk!