Modelの定義とリレーション – Ember.js入門(15)

2014.01.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

渡辺です。

前回からember-data編に突入しましたが、一体何時頃にEmber.js入門が収束するのかサッパリ解りません。今回はModelの定義方法について詳しくみていきましょう。まだまだ魔境の入り口でしかないので安心してください(笑)

Modelの定義

Modelは、DS.Modelクラスのサブクラスとして定義します。

App.Entry = DS.Model.extend({
});

DS.ModelクラスはEmber.Objectのサブクラスなので、ModelはObserverなどEmberオブジェクトとしての機能をデフォルトで利用できます。

Modelの属性

Modelの属性を定義する時はDS.attrで型を宣言します。なぜならば、JSONからどのような型を期待してModelに変換するかのヒントを与えなければならないからです。

App.Entry = DS.Model.extend({
  title: DS.attr('string'),
  draft: DS.attr('boolean'),
  postedAt: DS.attr('date'),
  content: DS.attr('string')
});

Modelの属性として定義できる型は、string, number, boolean, dateの4種類です。

デフォルト値

attrメソッドの第2引数にオプションパラメータとしてdefaultValueを指定することができます。defaultValueには固定値、またはfunctionを指定します。

App.Entry = DS.Model.extend({
  title: DS.attr('string'),
  draft: DS.attr('boolean', {defaultValue: true}),
  postedAt: DS.attr('date', {defaultValue: function() { return new Date(); }}),
  content: DS.attr('string')
});

カスタム型

DS.Transformクラスを利用してシリアライズとデシリアライズを実装すれば、カスタム型を利用することができます。今の所、利用したこと無いので割愛します。

Modelの作成

Modelはcreateメソッドでインスタンスを作成出来ません。試しにConsoleで実行してみると、次のようなエラーが表示されてしまいます。

> App.Entry.create();
Error: You should not call `create` on a model. 
Instead, call `store.createRecord` with the attributes you would like to set.

エラーにあるように、storeのcreateRecordを使ってインスタンスを作成します。

storeオブジェクト

storeオブジェクトは、全てのRouteとControllerからthis.storeでアクセスできます。これはEmber.jsが自動的に設定したシングルトンのオブジェクトです。なお、storeにはそれまでロードされたModelがキャッシュされています。

Consoleからstoreを使う場合は、次のような方法でstoreオブジェクトの参照を取得します *1

window.App = Ember.Application.create();
var store = App.__container__.lookup('controller:application').store;

Emberアプリケーションの初期化で作成されるネームスペース(ここではApp)には、containerという特別なプロパティがあり、そのlookupメソッドを利用すればControllerやRouteなどのコンポーネントを参照可能です。上記サンプルではApplicationControllerのインスタンスを参照し、storeオブジェクトを取得しています。

なお、storeオブジェクトはstore:mainでもlookupできるので、次のようにしてもstoreオブジェクトを取得できます。

var store = App.__container__.lookup('store:main');

どちらを利用するかはお好みで。

store.createRecord

storeオブジェクトが取得できたならば、createRecordメソッドを実行することで、Modelのインスタンスを生成できます。createRecordメソッドの第1引数にはModelのlookup名を指定します。

var e = store.createRecord('entry');

デフォルト値が設定されていることは、getメソッドなどを利用すれば確認出来るでしょう。

e.get('draft'); // => true

また、第2引数にオプションを指定することで、Modelの初期値を与えることができます。

var e = store.createRecord('entry', { title: 'Ember.js', content: ''});
e.get('content'); // => ""
e.get('title'); // => "Ember.js"

Modelのリレーション

ember-dataを利用すれば1:N関係等のリレーションを設定することができます。ただし、RESTAdapterを利用した場合にリクエストされるAPIに注意が必要なため、使い所は難しいかもしれません。

なお、現時点ではこの機能を有効活用しているわけでもないため、ノウハウが溜まり次第エントリーにはまとめると思います。

modeling

ONE-TO-ONE

DS.belongsToを使うことで、1:1のリレーションを定義することができます。

App.Site = DS.Model.extend({
  name: DS.attr('string'),
  env: DS.belongsTo('env')
});
App.Env = DS.Model.extend({
  name: DS.attr('string'),
  debug: DS.attr('boolean'),
  host: DS.attr('string'),
  site: DS.belongsTo('site')
});

このサンプルでは、ブログの名前などが設定されるSite modelとデプロイ設定などを定義するEnv modelが1:1のリレーションで相互参照されています。また、ここでは双方向にしてリレーションを設定していますが、次のように単方向のリレーションとしても構いません。

App.Site = DS.Model.extend({
  name: DS.attr('string'),
  env: DS.belongsTo('env')
});
App.Env = DS.Model.extend({
  name: DS.attr('string'),
  debug: DS.attr('boolean'),
  host: DS.attr('string')
});

なお、リレーションされるModelが正しく設定されるかどうかは、RESTAdapterならばJSONレスポンスに強く依存することは忘れないでください。

ONE-TO-MANY

DS.belongsToに加えて、DS.hasManyを利用すれば、ONE-TO-MANYのリレーションを設定することができます。

App.Category = DS.Model.extend({
  name: DS.attr('string'),
  entries: DS.hasMany('entry')
});
App.Entry = DS.Model.extend({
  title: DS.attr('string'),
  postedAt: DS.attr('date'),
  content: DS.attr('string'),
  category: DS.belongsTo('category')
});

このサンプルでは、ブログのエントリー(Entry)にカテゴリ(Category)を1つだけ設定できるとして、CategoryとEntryにONE-TO-MANYのリレーションを定義しています。ONE-TO-ONEと同様に単方向のリレーションとして設定することも可能です。

特にRESTAdapterを利用する場合は、APIに合わせて設定しなければなりません。ActiveRecordのリレーションのように柔軟な設定はできません。何故ならば、ActiveRecordなどのORMであればSQLで実現出来ることであれば可能ですが、ember-dataではREST APIが対応していることに制限が強まるからです。

MANY-TO-MANY

ONE-TO-ONE、ONE-TO-MANYとくれば、当然のようにMANY-TO-MANYのリレーションも設定できます。

App.Tag = DS.Model.extend({
  name: DS.attr('string'),
  entries: DS.hasMany('entry')
});
App.Entry = DS.Model.extend({
  title: DS.attr('string'),
  postedAt: DS.attr('date'),
  content: DS.attr('string'),
  category: DS.belongsTo('category'),
  tags: DS.hasMany('tag')
});

このサンプルでは、ブログのエントリー(Entry)にタグ(Tag)を複数設定できるとして、CategoryとEntryにMANY-TO-MANYのリレーションを定義しています。

まとめ

今回はember-dataにおいてModelを定義する方法について解説しました。

JavaScriptですがデータの型を意識する必要があったりと若干の違和感はあるかもしれませんが、Model自体の定義は非常にシンプルです。ember-dataは非常に魅力的に感じることでしょう。しかし、実際にアプリケーションに組み込もうとすると、リレーションのロードタイミングやAPIの実装など思った以上に考慮すべき項目が多いので注意してください。

また、リレーション機能など、ActiveRecordに似ていると感じた人が多いのではないでしょうか?しかしながら、Modelのリレーションは簡単そうに見えて、APIと合わせて設計していかなければならず、慣れるまではActiveRecordっぽいと思い込んでハマる可能性が高いです。あくまで、バックエンドはRDB+SQLでなくREST APIであるということを忘れずに利用しましょう。

最後にモデルの定義を行うサンプルコード全体を記載します。読み込み後、Consoleなどで色々と試してみてください。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Define Model Ember.js</title>
</head>
<body>
<script type="text/javascript" src="../../ember/1.2.0/jquery-1.10.2.js"></script>
<script type="text/javascript" src="../../ember/1.2.0/handlebars-v1.1.2.js"></script>
<script type="text/javascript" src="../../ember/1.2.0/ember-1.2.0.js"></script>
<script type="text/javascript" src="../../ember/1.2.0/ember-data-1.0.0-beta.4.js"></script>
<script type="text/javascript">
    window.App = Ember.Application.create();
    App.Site = DS.Model.extend({
        name: DS.attr('string'),
        env: DS.belongsTo('env')
    });
    App.Env = DS.Model.extend({
        name: DS.attr('string'),
        debug: DS.attr('boolean'),
        host: DS.attr('string'),
        site: DS.belongsTo('site')
    });
    App.Category = DS.Model.extend({
        name: DS.attr('string'),
        entries: DS.hasMany('entry')
    });
    App.Entry = DS.Model.extend({
        title: DS.attr('string'),
        postedAt: DS.attr('date'),
        content: DS.attr('string'),
        category: DS.belongsTo('category')
    });
</script>
</body>
</html>

脚注