#154 Polymorphic Association
- Download:
- source codeProject Files in Zip (191 KB)
- mp4Full Size H.264 Video (13.8 MB)
- m4vSmaller H.264 Video (9.32 MB)
- webmFull Size VP8 Video (25.7 MB)
- ogvFull Size Theora Video (20 MB)
多态关联早已经不是一个Rails的新特性了,早在Rails 1.1的就被引入进来。不过它可能会让一些没有经验的Rails开发者感到困惑,所以,这一次我们将用多态关联作一个简单的示范来说明它到底该怎么用。
以上的页面来自一个拥有三个model(articles, photos 和events)的站点。现在,我们想要让用户能够为articles、photos或者events添加评论(comment)。如果我们只要简单的为其中一个添加评论功能,假设是文章(article),我们就需要创建一个叫Comment
的model,并在Article
和Comment
之间建立一个has_many
/ belongs_to
关联。在这种没有使用的多态关联(polymorphic associations)的情况下,我们就必须为各个model建立3个不同的comment model,继而我们的代码会有很多重复的部分。这种情况下我们就该使用多态关联,它允许我们只要建一个Comment model(清洁了很多,保证了Dry),然后让每一条comment知道自己是跟具体的哪个model关联的就行了。
创建Comment Model
下面我们要做的第一件事就是创建Comment
model。就用普通的创建model的方式去创建,但是需要有一点小小的不同。如果我们仅仅只为文章(Article
)创建comment我们只需要使用一个叫做article_id
的integer(整数型)字段来存储外键(foreign key);但是现在的情况是我们需要更高层的抽象,所有我们需要添加model的对象都有一个共同点:允许评论;因此我们将外键的名字定为:commentable_id
。还有,每一条comment需要知道自己关联的model,所以我们就需要另一条字段去存储它,命名为:commentable_type
,这个字段将保存关联model的类名。
script/generate model Comment content:text commentable_id:integer commentable_type:string
一旦我们创建好了model并且运行了相应的migration后,我们就得为article, photo和event这三个model添加关联关系了。
class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true end
我们使用belongs_to
commentable
代替了belongs_to
其他 model的做法,而且声明它是一个多态关联关系。现在,只要是has_many commentables
的其他model都可以有评论了。具体来说,我们在Article
, Photo
和Event
类中我们这样定义关系:
has_many :comments, :as => :commentable
这将告诉所有的类,它们通过commentable关系多态关联了很多评论。
选出评论(及view??没明白不过也没有影响啦)
现在关系已经在我们的model之间建立好了,我们现在要做的就是关注controller和view了。我们需要为Comment
model建立一个controller。
script/generate controller Comments
我们可以像使用其他的关系一样的对待comment model,举例来说,要取得article的评论我们就可以用article.comments
来获取。
获取正确的评论
如果我们使用嵌套资源(nested resources)的话就会遇到问题了。比如使用/articles/1/comments
查看某一article的所有comment的时候,要想让这样的请求成功我们就要去修改routes.rb
文件配置,这样我们的comment就会被当成相应的model的嵌套资源(nested resources)对待了。(译注:nested resources翻译成“嵌套资源”似乎有点不妥,应该是“被嵌套的资源”,但这么翻译的整句估计更难理解,所以,如果你还不能明白这段意思,还请对照英文版吧)
map.resources :articles, :has_many => :comments map.resources :photos, :has_many => :comments map.resources :events, :has_many => :comments
如果我们通过URL(/articles/1/comments
)访问一篇文章article的所有评论(comment), CommentsController
里的index
action将会被执行,
def index @comments = Comment.all end
无论我们为一个Article
,Photo
或者Event
获取所有评论的时候调用的都是同一个index
action,所以,我们需要做点什么让index
action知道返回哪一些comments。其中一种方式是迭代传入action的所有参数(parameter),找到其中类似<parent_resource>_id
形式的那个,我们就知道了到底是哪一个model在查询评论(comment)。我们可以这样写一个方法加在CommentsController
中。
def find_commentable params.each do |name, value| if name =~ /(.+)_id$/ return $1.classify.constantize.find(value) end end nil end
以上的方法迭代所有传入的参数,并查找其中以_id
结尾的那一个。如果我们找的是第一篇文章(Article)的所有评论(comment),那么就应该有一个叫article_id
的参数并且它的值为1
(译注:第一篇文章嘛当然是1)。
如果方法发现了一个匹配的参数,它将会调用classify
方法将得到的_id
前的名字从表明转变为model名(即“articles”
或“article”
将会转为“Article”),然后对classify返回的字符串调用contantize
尝试找到一个常量(constant)与之匹配(译注:字面上是前面这么翻译,很不好懂,本质上的意思是通过前面转换来的字符串找到相应的类(Class),这样后面才好调用find方法)。最后使用这个类调用find
方法查找value值对应的数据,也就是我们要帮它找到所有评论的那个“被评论的对象”。在我们的例子里,将返回第一篇文章(Article
)。
我们现在可以用以下这个index action方法找到对应的所有评论(comment)了。
def index @commentable = find_commentable @comments = @commentable.comments end
添加评论
在index页面列出所有评论的同时,我们也希望用户能够添加评论。而我们的view的代码就是以下这样的。
<h1>Comments</h1> <ul id="comments"> <% @comments.each do |comment| %> <li><%= comment.content %></li> <% end %> </ul> <h2>New Comment</h2> <% form_for [@commentable, Comment.new] do |form| %> <ol class="formList"> <li> <%= form.label :content %> <%= form.text_area :content, :rows => 5 %> </li> <li><%= submit_tag "Add comment" %></li> </ol> <% end %>
第一部分的view代码渲染出一个无序的评论列表。下面跟着的是添加评论的form。而这个form的提交对象是一个嵌套资源(nested resource),我们使用一个数组表示,数组的第一个元素是我们将从find_commentable
方法得到的commentable
对象,第二个元素这是一个新的评论对象。
当这个form被提交的时候将会调用CommentsController
的create
action。
def create @commentable = find_commentable @comment = @commentable.comments.build(params[:comment]) if @comment.save flash[:notice] = "Successfully saved comment." redirect_to :id => nil else render :action => 'new' end end
这里第一件需要注意的是我们又调用了find_commentable
方法来获取当前评论的记录;将会是一篇Article
,一张Photo
或者一个Event
;之后我们就可以使用build
创建评论了。当我们保存了这个model后,返回index
页面会有问题,因为不知道我们评论是嵌套在哪一个资源内的。所以我们使用一个有帮助的hack技巧重定向给id
为nil
的记录。这样我们就返回到当前(嵌套资源内)的index action然后显示出正确的页面来。
我们评论完后正确的返回了index action。
我们的评论现在开始正常工作了。我们添加了一条评论并且返回了正确的页面。这功能在对Photo
和Event
model也同样能见效。
给读者的练习
接下来我们将要做的是把刚刚对index
和create
action做的改动套用到CommentsController
的其他的action上去,就留给读者们自己实践吧。
给英语困难的同学参考用,英文好的同学还是建议看原版教程。如有错误,欢迎指正,发邮件到joey.d.darko@gmail.com