実践入門 Ember.js

第7回 データの永続化(Ember Data)

この記事を読むのに必要な時間:およそ 11 分

前回はショッピングカートを作成しつつ,Ember.jsの汎用的な機能を解説しました。今回は,Ember.jsの永続化層を扱うためのライブラリであるEmber Dataを解説します。

Ember DataはEmber.jsとは別のライブラリですが,Ember.jsのコアチームによって開発されている公式ライブラリです。ただ,APIがまだ安定しきっておらず今後変更される可能性があるため本稿執筆時点ではベータ版という位置付けです。

まだ発展途上のライブラリですが,サーバのデータをクライアントサイトで変更して保存する場合や既存のAPIに対してのクライアントをEmber.jsで作成する場合に大活躍するため,本連載でも取り扱うことにします※1)⁠

※1
  • 発展途上であるため,APIが変更される可能性が十分あります。大きな変更であれば,新しいバージョンがリリースされた際にEmber.js 公式ブログのリリースノートで変更履歴が解説されます。Ember Dataを利用する場合常にこのリリースノートを確認してください。

  • 環境準備

    まずは環境準備です。前回の記事を参考に必要なファイルを準備してください。また,今回はこれに加えてEmber Dataを読み込む必要があります。Ember.js公式サイトのベータ版のダウンロードページからEmber Dataをダウンロードして,HTMLに読み込み用のscriptタグをを追加します。

    本稿での対象バージョンはこちらです。

    準備ができたらHTMLをブラウザで表示してください。ブラウザの開発者コンソールにEmber Dataのバージョンが表示されていれば準備完了です。

    DEBUG: -------------------------------
    DEBUG: Ember      : 1.10.0
    DEBUG: Ember Data : 1.0.0-beta.15
    DEBUG: jQuery     : 2.1.3
    DEBUG: -------------------------------

    それでは実際にEmber Dataに触れてみましょう。

    データを扱う

    DS.Model

    Ember Dataでデータを扱う際はDS.Modelを継承したクラスを作成します。まずはブログ記事を表すPostと,記事に対してのコメントを表すCommentを作成しましょう。

    App.Post = DS.Model.extend({
      title: DS.attr('string'),
      body: DS.attr('string'),
    
      comments: DS.hasMany('comment')
    });
    
    App.Comment = DS.Model.extend({
      text: DS.attr('string'),
    
      post: DS.belongsTo('post')
    });
    データとモデルの対応付け

    データと対応させたいプロパティにはDS.attr()を指定します。こうすることで,データとモデルのプロパティがマッピングが必要であることをEmber Dataに知らせています。

    型変換

    DS.attr()の引数に形名を指定すると,JSONとして渡ってくるデータをモデルにマッピングする際に自動で型変換が行われます。利用可能な値は次の4つです※2)⁠

    • string
    • number
    • boolean
    • date
    ※2
  • 利用可能な型変換の種類を増やしたい場合は公式サイトの説明を参照してください。

  • 特にJSONで表現できない日付Date型を扱いたい場合はdateを指定してください。データの型変換が不要な場合(特に,配列やオブジェクトを扱いたい場合)には引数は省略できます。引数を省略した場合,型変換は行われずデータとして渡された値がそのままモデルのプロパティに設定されます。

    データの形式

    この例では,データは次のような形式であることを想定しています。

    // post
    {
      "id": ...,
      "title": ...,
      "body": ...
    }
    
    // comment
    {
      "id": ...,
      "text": ...
    }

    このデータのプロパティのうち"id"は自動でマッピングされるため,それ以外のプロパティにDS.attr()を指定します。

    また,モデルを作成する際にはモデル同士の関連を指定できます。今回の例ではDS.hasMany()DS.belongsTo()を使って一対多の関連を定義しました。DS.hasMany()/DS.belongsTo()の引数には,モデル名を与えます。モデル名はモデルのクラスを小文字にしてハイフン区切りにしたものです。

    クラス モデル名
    Post post
    Comment comment
    TopicCategory topic-category

    このモデル名は,関連の指定以外にもあらゆる場面で利用します。

    次はデータを読み込む部分を解説します。

    DS.Store

    Ember Dataでは,REST APIなどから取得したデータはモデルにマッピングされDS.Storeによって管理されます。DS.Storeはクライアントサイドのデータベースのようなもので,取得したデータはこのストアにキャッシュされます。

    外部サーバからデータを取得する方法は後述するので,まずここでは手元でデータを用意する方法を解説します※3)⁠

    ※3
    今回初登場ですが,RoutebeforeModel()メソッドはmodel()メソッドの前で実行されるメソッドです。model()より前に行いたい処理がある場合,ここに記述します。一方model()のあとに実行したい処理がある場合,afterModel()メソッドを利用できます。
    App.ApplicationRoute = Ember.Route.extend({
      beforeModel: function() {
        this.store.push('post', {
          id: 1,
          title: 'はじめての Ember.js',
          body: 'これから Ember.js を始めようという方向けの記事です。'
        });
    
        this.store.push('post', {
          id: 2,
          title: '公式サイトの歩き方',
          body: 'http://emberjs.com/ の解説です。'
        });
    
        this.store.push('comment', {
          id: 1,
          text: 'はじめまして',
          post: 1
        });
    
        this.store.push('comment', {
          id: 2,
          text: '入門しました。',
          post: 1
        });
    
        this.store.push('comment', {
          id: 3,
          text: '詳しい説明を知りたいときはまず参考にします。',
          post: 2
        });
      }
    });

    では,DS.Storeのインスタンスにアクセスする方法を説明します。Ember DataはControllerRoutestoreというプロパティを定義し,ここにDS.Storeのインスタンスを設定します。これにより,ControllerRouteの中ではthis.storeでストアにアクセスできます。

    そしてストアにデータを読み込むにはストアのpush()メソッドを利用します。push()メソッドの第一引数はモデル名,第二引数はデータの本体です。

    データが関連を持っている場合,関連名をキーとして関連先データのIDを指定します。

    さて,これで準備完了です。画面に表示して動作を確認してみましょう。

    ルーティングを作成します。

    App.Router.map(function() {
      this.resource('posts', {path: '/'});
      this.resource('post', {path: '/post/:id'});
    });
    
    App.PostsRoute = Ember.Route.extend({
      model: function() {
        return this.store.all('post');
      }
    });
    
    App.PostRoute = Ember.Route.extend({
      model: function(params) {
        return this.store.find('post', params.id);
      }
    });

    ここではストア経由でモデルを取得しています。モデルを取得するにはいくつかの方法があります。

    DS.Store#all()

    ストアに読み込み済みのモデル全件を取得します。第一引数にはモデル名を指定します。

    DS.Store#find()

    モデル名とIDを指定してモデルを取得します。もし該当するモデルがストアに存在しなければ,ストアはサーバに対してHTTPリクエストを発行してデータを取得します※4)⁠ここまでのサンプルではサーバを用意していないのでリクエストは失敗します。

    ※4
    HTTPリクエストを発行するというのは,デフォルトのアダプターであるDS.RESTAdapterの挙動です。このアダプタを変更することによって,サーバを変更したり,サーバではなくlocalStorageにデータを取得するよう設定できます。これについては後述します。

    次はテンプレートを作成します。

    <script type="text/x-handlebars" data-template-name="posts">
      <h1>ブログ記事一覧</h1>
    
      <ul>
        {{#each post in model}}
          <li>{{link-to post.title "post" post}}</li>
        {{/each}}
      </ul>
    </script>
    
    <script type="text/x-handlebars" data-template-name="post">
      <h1>{{model.title}}</h1>
    
      <pre>
        {{model.body}}
      </pre>
    
      <ul>
      {{#each comment in model.comments}}
        <li>{{comment.text}}</li>
      {{/each}}
      </ul>
    
      {{link-to "戻る" "posts"}}
    </script>

    この状態でブラウザを表示すると,コメントがブログ記事に関連付けられているのが確認できます。

    著者プロフィール

    佐藤竜之介(さとうりゅうのすけ)

    株式会社えにしテック所属。JavaScriptとRubyが好きなウェブ系プログラマ。オープンソースソフトウェアに強い関心がありEmber.jsにも数多くのパッチを送っている。

    ブログ:http://tricknotes.hateblo.jp/
    Twitter:@tricknotes

    コメント

    コメントの記入