LoginSignup
20
20

More than 5 years have passed since last update.

Modelの一対多を実装する

Last updated at Posted at 2012-03-16

例えば、Blogテーブルにuser_idカラムがあり、これがUserテーブルへの外部キー制約になっているような場合、WebフレームワークではBlogモデルにuserフィールドを持たせて、そのフィールドにメッセージを送ると、参照しているUserモデルのインスタンスを返してくれるようになっている場合が多いと思います。
このような関係をBackbone.jsで実現しようと思い、つい以下のように書きたくなってしまうかも知れません。

var User = Backbone.Model.extend({});
var author = new User({id: $(".user_id").html()});
var Blog = Backbone.Model.extend({});
var entry = new Backbone({text: $(".text").html(), user_id: author.id, user: author});

こうすれば確かに entry.get("user")author へのリファレンスを得ることができます。
僕も以前はこのような書き方をしたことがあったのですが、どうやらこれはBackbone.js的には正しくない使い方のようです。その理由はModelのtoJSONメソッドの実装を見てみると分かります。

// Backbone.js 0.5.3 から改変して抜粋
_.extend(Backbone.Model.prototype, {
  toJSON: function () {
    return _.clone(this.attributes);
  }
});

ここでattributesはModelに対してsetgetメソッドで取得する属性が実際に格納されている場所です。つまり、上記のentryオブジェクトのattributesは以下のようになっているわけです

entry.attributes == {text: $(".text").html(), user_id: author.id, user: author}

ここではModelであるはずのauthorに対して、再帰的にtoJSONを呼び出すという処理を行なっていませんね。
Backbone.syncでModelをWebサーバど同期するためにRESTful APIを呼び出す際にModelオブジェクトにtoJSONメソッドを呼び、その返り値をJSON.stringifyしたものをAPIに投げるという処理を行なっています。
authorも同様にattributesなどを持っているので、これではUserテーブルにattributesカラムがあるかのようにAPIを呼び出していることになってしまいます。実際、scaffoldで作ったAPIに対してこのようなリクエストを発行してもRailsはエラーを出します。
回避方法は大きく分けて二通りあります。

toJSONをオーバーライドする

BlogのtoJSONメソッドをオーバーライドしてuserを返り値から取り除けば回避できます。

var Blog = Backbone.Model.extend({
  toJSON: function () {
    var json = _.clone(this.attributes);
    delete json.user;
    return json;
  }
});

これでも動きますが、次のおそらく意図しているであろう使い方の方が適当だと思います。

メソッドにする

そもそもの悪の元凶は、userをattributesに入れてしまったことです。UserインスタンスをIDと紐付けて管理するオブジェクトを用意して、それに問い合わせるようにしてみます。

var users = {};
users[author.id] = author;
var Blog = Backbone.Model.extend({
  getUser: function () {
    return users[this.get("user_id")];
  }
});
var entry = new Blog({user_id: author.id});
entry.getUser(); // => author

Collectionを使う

Backbone.jsで複数のModelを管理するなら、Collectionを使うほうが適当だと思われます。

var UserList = Backbone.Collection.extend({
  model: User
});
var users = new UserList([author]);
var Blog = Backbone.Model.extend({
  userList: users,
  getUser: function () {
    return this.userList.get(this.get("user_id"));
  }
});
var entry = new Blog({user_id: author.id});
entry.getUser(); // => author

User側から参照しているBlogの一覧を取得する

Userインスタンスに対して、このユーザが作ったBlogの一覧を取得したいことが考えられます。これもBlogのCollectionを作るのがいいと思います。

var BlogList = Backbone.Collection.extend({
  model: Blog
});
var blogs = new BlogList([entry]);
var User = Backbone.Model.extend({
  blogList: blogs,
  getBlogs: function () {
    return this.blogList.filter(function (blog) { return blog.get("user_id") == this.id }, this);
  }
});

余談ですが、ここCollectionインスタンスにfilterメソッドを呼び出しています。同様にeachreducefindなどのunderscoreのメソッドが定義されており、これらは全てCollectionに追加されているModelの配列に対して呼び出されます。

まとめ

Modelの一対多を表現する場合は、リファレンスを属性として保持するのではなく、リファレンスを返すメソッドを実装し、内部ではCollectionを使うのが、意図されている実装方法なのではないかと思います。

20
20
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20