試して学ぶ Backbone.js入門2
はじめに
前回の「試して学ぶ Backbone.js入門1」では、Backbone.jsの中心となるModelとCollectionについて、その基本的な利用方法を説明しました。今回はBackbone.jsの優れた機能である非同期RESTful JSONインタフェースによる永続化について説明します。
今回のソースの全体は
こちらとこちらで確認することができます。
Backbone.jsのRESTful JSONインタフェースによる永続化
Backbone.jsは、ajaxを利用したRESTful JSONインタフェースによるサーバ側への永続化機能を標準で備えています。具体的にはModelクラスのsave, fetch, destroyメソッド、 Collectionクラスのcreate, fetchメソッドを利用することで、サーバ側へのデータの永続化と、サーバ側のデータとの同期を行うことができます。これらの機能を実際に動かしながら確認していきましょう。
補足
この機能を試すには、サーバ側で動作するRESTful JSONインタフェースを持つアプリケーションが必要になります。実際に動かして試せるように、Node.js+MongoDBによる簡単なCRUD機能を持つ
Memoアプリケーション
を用意しましたので、ここからの説明ではこのアプリケーションを利用して説明します。
Model単独での永続化
ModelをCollectionに保持せず、Modelオブジェクトを単独で永続化する場合について説明します。
まずは、Modelを定義しましょう。コードは以下のようになります。
1 2 3 4 5 6 7 | var Memo = Backbone.Model.extend({ urlRoot: "/memo" , idAttribute: "_id" , defaults: { "content" : "" } }); |
idAttribute属性はid識別子としてどの属性を使うかを指定します。今回はサーバ側がMongoDBであり、MongoDBの識別子である_idをそのまま識別子として使用したいので、このように指定しています。urlRoot属性は、このModelが対応づくリソースのURLについて、そのrootとなるパスを指定します。
Modelにはデータを永続化するための関連メソッドとして、save、fetch、destroyがあります。urlRoot属性に指定したリソースのURLとHTTPメソッドに対応する処理は以下のようになります。urlRoot属性と、Modelオブジェクトのidによって、サーバ側にリクエストする際のURLが決定されます。この処理はajaxによる非同期リクエストになります。
Backbone.jsのRESTful JSONインタフェース概要図
(HTTPメソッド) | (URL) | (処理) | (対応するメソッド) |
GET | /memo | 一覧取得 | idが未設定のModelによるfetch(または Collectionによるfetch) |
POST | /memo | 新規作成 | idが未設定のModelによるsave(または Collectionによるcreate) |
GET | /memo/:id | 参照 | idが設定済みのModelによるfetch |
PUT | /memo/:id | 更新 | idが設定済みのModelによるsave |
DELETE | /memo/:id | 削除 | idが設定済みのModelによるdestroy |
それでは、インスタンスを生成し、永続化処理を試してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var memo = new Memo(); console.log( "Before save: " + JSON.stringify(memo)); console.log( "isNew(): " + memo.isNew()); memo.save({content: "Acroquest" }, { success: function () { console.log( "After save(post) memo: " + JSON.stringify(memo)); console.log( "After save(post) memo.isNew(): " + memo.isNew()); } }); console.log( "After save: " + JSON.stringify(memo)); console.log( "isNew(): " + memo.isNew()); |
memoオブジェクトを生成した後、saveメソッドを呼び出すことで、サーバ側に保存を行います。クライアント側でmemoオブジェクトを生成した場合、通常はid識別子は設定されません(isNew()がtrueの状態)。この状態でsaveメソッドを実行すると、上記の表の通りサーバ側には/memoというURLに対してHTTPのPOSTリクエストが送信されます。
saveメソッドは非同期で実施されます。saveメソッドの第2引数のoptionsハッシュにてsuccessコールバックを指定することで、サーバ側にて保存された結果を確認することができます。コンソール出力の結果を見るとその様子が分かります。
コンソール出力結果
1 2 3 4 5 6 7 8 | Before save: { "content" : "" } isNew(): true After save: { "content" : "Acroquest" } isNew(): true After save(post) memo: { "content" : "Acroquest" , "__v" :0, "date" : "2013-02-16T14:00:55.158Z" , "_id" : "511f9117e32557404b000001" } After save(post) memo.isNew(): false |
コードを書いた順番とコンソール出力される結果が非同期処理のために入れ替わっていることが分かります。また、successコールバックが呼ばれるタイミングまでは、memoオブジェクトにはクライアント側で設定したcontentプロパティしか存在していませんが、successコールバックが呼ばれるタイミングで、_idプロパティ、dateプロパティなど、サーバ側で設定されたプロパティが反映されることが分かります。isNewメソッドの結果は、id識別子である_idプロパティが設定されると、falseとなります。
それでは次に、今サーバ側に保存したデータを取得してみましょう。id識別子が設定されている状態(isNew()がfalseの状態)にてfetchメソッドを実行すると、上記の表の通りにサーバ側には/memo/:idというURLに対してHTTPのGETリクエストを送信します。
1 2 3 4 5 6 7 8 9 10 11 | // 確認用に一度別の値に変えておく memo.set({content: "Acro" }); console.log( "Befroe fetch memo: " + JSON.stringify(memo)); // サーバ側のデータを同期する memo.fetch({ success: function () { console.log( "After fecth memo: " + JSON.stringify(memo)); } }); |
コンソール出力結果
1 2 3 | Befroe fetch memo: { "content" : "Acro" , "__v" :0, "date" : "2013-02-16T15:11:33.534Z" , "_id" : "511fa1a55871772e4c000001" } After fecth memo: { "content" : "Acroquest" , "__v" :0, "date" : "2013-02-16T15:11:33.534Z" , "_id" : "511fa1a55871772e4c000001" } |
memoオブジェクトのcontentプロパティを変更してみましょう。id識別子が設定されている状態(isNew()がfalseの状態)にてsaveメソッドを実行すると、上記の表の通りサーバ側には/memo/:idというURLに対してHTTPのPUTリクエストが送信されます。
1 2 3 4 5 6 7 | console.log( "Befroe save(put) memo: " + JSON.stringify(memo)); memo.save({content: "Acroquest Technology" }, { success: function () { console.log( "After save(put) memo: " + JSON.stringify(memo)); } }); |
コンソール出力結果
1 2 3 | Befroe save(put) memo: { "content" : "Acroquest" , "__v" :0, "date" : "2013-02-16T15:20:22.535Z" , "_id" : "511fa3b6377656564c000001" } After save(put) memo: { "content" : "Acroquest Technology" , "__v" :0, "date" : "2013-02-16T15:20:22.801Z" , "_id" : "511fa3b6377656564c000001" } |
次は、memoオブジェクトを削除してみましょう。id識別子が設定されている状態(isNew()がfalseの状態)にてdestroyメソッドを実行すると上記の表の通り、サーバ側には/memo/:idというURLに対してHTTPのDELETEリクエストが送信されます。
1 2 3 4 5 6 7 | console.log( "Before delete memo: " + JSON.stringify(memo)); memo.destroy({ success: function () { console.log( "After delete memo: " + JSON.stringify(memo)); } }); |
コンソール出力結果
1 2 3 | Before delete memo: { "content" : "Acroquest Technology" , "__v" :0, "date" : "2013-02-16T15:20:22.801Z" , "_id" : "511fa3b6377656564c000001" } After delete memo: { "content" : "Acroquest Technology" , "__v" :0, "date" : "2013-02-16T15:20:22.801Z" , "_id" : "511fa3b6377656564c000001" } |
この処理によって、サーバ側のデータは削除されるのですが、コンソールの出力結果を確認すると、クライアント側ではmemoオブジェクト自身は削除されていないことが分かります(後述しますが、対象のModelがCollectionに保存されている場合は、Collectionからは削除されます)。
余談:jQueryのDeferredオブジェクトを使ってajaxの非同期リクエストを順次実行する
さて、POST、GET、PUT、DELETEの各処理の書き方を説明してきましたが、そのままコードを書くと、少し問題があります。実際に動作させてみると分かりますが、save、fetch、destroyメソッドは内部ではajaxの非同期リクエストとなるため、これまでの処理を続けてコードとして書くと、例えばPOST処理によってサーバ側にてデータの保存が完了する前に、DELETEリクエストを送信してしまいます。
各処理が完了してから次の処理を実行したい場合、ひとつの方法としてはsuccessコールバックの中に次の処理を書くことで期待した動作になります。しかしこの書き方では、POST、GET、PUT、DELETEと順次実行しようとすると、ネストが深くなり、コードとしての見通しが良くありません。
そこで、jQueryの
Deferredオブジェクト
を使ってみましょう。Deferredオブジェクトとは、簡単に言うと非同期処理を扱いやすくするライブラリで、非同期リクエストの処理結果を待ち、成功した場合、失敗した場合の処理を簡単に書くことができます。実は、jQueryを内部で利用するBackbone.jsは、save, fetch, destroyなどのajaxを利用した非同期リクエストを行うメソッド(内部的にはBackbone.sync)が戻り値としてDeferredオブジェクトを返すため、このDeferredオブジェクトを利用することができます。
では、これまでに説明したPOST、GET、PUT、DELETEの各処理をDeferredオブジェクトのpipe、doneメソッドを使って順次実行するように書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | var memo = new Memo(); console.log( "Before save: " + JSON.stringify(memo)); console.log( "isNew(): " + memo.isNew()); memo.save({content: "Acroquest" }, { success: function () { console.log( "After save(post) memo: " + JSON.stringify(memo)); console.log( "After save(post) memo.isNew(): " + memo.isNew()); } }).pipe( function () { memo.set({content: "Acro" }); console.log( "Befroe fetch memo: " + JSON.stringify(memo)); return memo.fetch({ success: function () { console.log( "After fecth memo: " + JSON.stringify(memo)); } }); }).pipe( function () { console.log( "Befroe save(put) memo: " + JSON.stringify(memo)); return memo.save({content: "Acroquest Technology" }, { success: function () { console.log( "After save(put) memo: " + JSON.stringify(memo)); } }); }).done( function () { console.log( "Before delete memo: " + JSON.stringify(memo)); return memo.destroy({ success: function () { console.log( "After delete memo: " + JSON.stringify(memo)); } }); }); console.log( "After save: " + JSON.stringify(memo)); console.log( "isNew(): " + memo.isNew()); |
save、fetchの戻り値であるDeferredオブジェクトのpipeメソッドの引数として次の処理をchainできるようにDeferredオブジェクトを返すコールバック関数を渡しています。最後はdoneメソッドを利用しています。このように書くことで、ネストが深くならずにコードを書くことができます。コンソール出力の結果は以下の通りです。期待するとおりに動作していることが分かります。
コンソール出力結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | Before save: { "content" : "" } isNew(): true POST http: //localhost:3000/memo After save: { "content" : "Acroquest" } isNew(): true After save(post) memo: { "content" : "Acroquest" , "__v" :0, "date" : "2013-02-16T15:20:22.535Z" , "_id" : "511fa3b6377656564c000001" } After save(post) memo.isNew(): false Befroe fetch memo: { "content" : "Acro" , "__v" :0, "date" : "2013-02-16T15:20:22.535Z" , "_id" : "511fa3b6377656564c000001" } GET http: //localhost:3000/memo/511fa3b6377656564c000001 After fecth memo: { "content" : "Acroquest" , "__v" :0, "date" : "2013-02-16T15:20:22.535Z" , "_id" : "511fa3b6377656564c000001" } Befroe save(put) memo: { "content" : "Acroquest" , "__v" :0, "date" : "2013-02-16T15:20:22.535Z" , "_id" : "511fa3b6377656564c000001" } PUT http: //localhost:3000/memo/511fa3b6377656564c000001 After save(put) memo: { "content" : "Acroquest Technology" , "__v" :0, "date" : "2013-02-16T15:20:22.801Z" , "_id" : "511fa3b6377656564c000001" } Before delete memo: { "content" : "Acroquest Technology" , "__v" :0, "date" : "2013-02-16T15:20:22.801Z" , "_id" : "511fa3b6377656564c000001" } DELETE http: //localhost:3000/memo/511fa3b6377656564c000001 After delete memo: { "content" : "Acroquest Technology" , "__v" :0, "date" : "2013-02-16T15:20:22.801Z" , "_id" : "511fa3b6377656564c000001" } |
ModelとCollectionによる永続化
Modelオブジェクトは通常Collectionに格納して使います。ここではCollectionの永続化に関連するメソッドのcreateとfetchについて説明します。
まずは、ModelとModelを格納するCollectionを定義しましょう。コードは以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 | var Memo = Backbone.Model.extend({ idAttribute: "_id" , defaults: { "content" : "" } }); var MemoList = Backbone.Collection.extend({ model: Memo, url: "/memo" }); |
Modelに定義していたurlRoot属性はCollectionに格納する場合は、Collection側でurl属性を定義することで不要になります。そして、Collectionには格納するModelを定義するためのmodel属性を定義します。
それではModelオブジェクトを生成してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 | var memoList = new MemoList(); console.log( "Before collection.length: " + memoList.length) var memo = memoList.create({content: "Acro1" }, { success: function () { console.log( "After create model: " + JSON.stringify(memoList)); console.log( "After create collection.length: " + memoList.length) } }); console.log( "After model: " + JSON.stringify(memo)) console.log( "After collection.length: " + memoList.length) |
コンソール出力結果
1 2 3 4 5 6 7 | Initial memoList.length: 0 After memo: { "content" : "Acro1" } After memoList.length: 1 After create memoList: [{ "content" : "Acro1" , "__v" :0, "date" : "2013-02-18T16:54:57.437Z" , "_id" : "51225ce1995ed7e453000001" }] After create memoList.length: 1 |
Collectionのcreateメソッドは、Modelをサーバ側に保存してCollectionに要素を追加します。1点注意すべき点は、createメソッドの戻り値はDeferredオブジェクトではなく、Modelオブジェクトになるという点です。Deferredオブジェクトを使いたい場合は、次のようにCollectionを関連付けたModelオブジェクトを生成して、saveメソッドで保存し、Collectionのaddメソッドで要素を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | var memo = new Memo({content: "Acro2" }, {collection: memoList}); memo.save( null , { success: function () { console.log( "After create memoList: " + JSON.stringify(memoList)); console.log( "After create memoList.length: " + memoList.length); } }); memoList.add(memo); console.log( "After add memo: " + JSON.stringify(memo)); console.log( "After add memoList.length: " + memoList.length); |
コンソール出力結果
1 2 3 4 5 6 7 | Initial memoList.length: 0 After add memo: { "content" : "Acro2" } After add memoList.length: 1 After save memoList: [{ "content" : "Acro2" , "__v" :0, "date" : "2013-02-18T17:58:01.327Z" , "_id" : "51226ba9f888a3e254000001" }] After save memoList.length: 1 |
次はfetchです。これはModelの時と同じように、以下のように書くことができます。
1 2 3 4 5 6 | memoList.fetch({ success: function () { console.log( "After fetch memoList: " + JSON.stringify(memoList)); console.log( "After fetch memoList.length: " + memoList.length); } }); |
Collectionに格納したModelの値の変更と削除は、先に説明した通り、Modelのsaveメソッド、destroyメソッドを使います。最終的なコードは以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | var memo = new Memo({content: "Acro2" }, {collection: memoList}); memo.save( null , { success: function () { console.log( "After save memoList: " + JSON.stringify(memoList)); console.log( "After save memoList.length: " + memoList.length); } }).pipe( function () { return memoList.fetch({ success: function () { console.log( "After fetch memoList: " + JSON.stringify(memoList)); console.log( "After fetch memoList.length: " + memoList.length); } }); }).pipe( function () { var tempMemo = memoList.find( function (item){ return item.get( "content" ) === "Acro2" ; }); return tempMemo.save({content: "Acro3" }, { success: function () { console.log( "After save memoList: " + JSON.stringify(memoList)); console.log( "After save memoList.length: " + memoList.length); } }); }).done( function () { var tempMemo = memoList.find( function (item){ return item.get( "content" ) === "Acro3" ; }); return tempMemo.destroy({ success: function () { console.log( "After destroy memoList: " + JSON.stringify(memoList)); console.log( "After destroy memoList.length: " + memoList.length); } }); }); memoList.add(memo); console.log( "After add memo: " + JSON.stringify(memo)); console.log( "After add memoList.length: " + memoList.length); |
コンソール出力結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | Initial memoList.length: 0 POST http: //localhost:3000/memo After memo: { "content" : "Acro2" } After memoList.length: 1 After save memoList: [{ "content" : "Acro2" , "__v" :0, "date" : "2013-02-18T18:19:43.493Z" , "_id" : "512270bf82a8845e55000001" }] After save memoList.length: 1 GET http: //localhost:3000/memo After fetch memoList: [{ "date" : "2013-02-18T18:19:43.493Z" , "content" : "Acro2" , "_id" : "512270bf82a8845e55000001" , "__v" :0}] After fetch memoList.length: 1 PUT http: //localhost:3000/memo/512270bf82a8845e55000001 After save memoList: [{ "date" : "2013-02-18T18:19:43.731Z" , "content" : "Acro3" , "_id" : "512270bf82a8845e55000001" , "__v" :0}] After save memoList.length: 1 DELETE http: //localhost:3000/memo/512270bf82a8845e55000001 After destroy memoList: [] After destroy memoList.length: 0 |
次回
今回は第2回目として、ModelとCollectionのRESTful JSONインタフェースによる永続化について説明しました。次回はBackbone.Eventについて取り上げます。
著者: 村田賢一郎
Acroquest Technology 株式会社勤務。Javaによるミッションクリティカルな集中監視システムのフレームワーク開発、およびライフラインを支えるシステム開発に携わる。非同期処理、メッセージング、HAなどが本業である傍ら、Webによる新しいUI表現、開発手法に興味があり、あれこれ模索している。