例えば、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に対してset
やget
メソッドで取得する属性が実際に格納されている場所です。つまり、上記の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
メソッドを呼び出しています。同様にeach
、reduce
、find
などのunderscoreのメソッドが定義されており、これらは全てCollectionに追加されているModelの配列に対して呼び出されます。
##まとめ
Modelの一対多を表現する場合は、リファレンスを属性として保持するのではなく、リファレンスを返すメソッドを実装し、内部ではCollectionを使うのが、意図されている実装方法なのではないかと思います。