msn email google-talk twitter tumblr flickr

asciicasts翻译:154-多态关联

多态关联早已经不是一个Rails的新特性了,早在Rails 1.1的就被引入进来。不过它可能会让一些没有经验的Rails开发者感到困惑,所以,这一次我们将用多态关联作一个简单的示范来说明它到底该怎么用。

E154I01.png

以上的页面来自一个拥有三个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添加关联关系了。

   1      class Comment < ActiveRecord::Base     
   2        belongs_to :commentable, :polymorphic => true    
   3      end

我们使用belongs_to commentable代替了belongs_to 其他 model的做法,而且声明它是一个多态关联关系。现在,只要是has_many commentables的其他model都可以有评论了。具体来说,我们在Article, Photo和Event类中我们这样定义关系:

   1      has_many :comments, :as => :commentable 

这将告诉所有的类,它们通过commentable关系多态关联了很多评论。

选出评论

现在关系已经在我们的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翻译成“嵌套资源”似乎有点不妥,应该是“被嵌套的资源”,但这么翻译的整句估计更难理解,所以,如果你还不能明白这段意思,还请对照英文版吧)

   1      map.resources :articles, :has_many => :comments    
   2      map.resources :photos, :has_many => :comments    
   3      map.resources :events, :has_many => :comments

如果我们通过URL(/articles/1/comments)访问一篇文章article的所有评论(comment), CommentsController里的index action将会被执行

   1      def index     
   2        @comments = Comment.all     
   3      end

无论我们为一个Article,Photo或者Event获取所有评论的时候调用的都是同一个index action,所以,我们需要做点什么让index action知道返回哪一些comments。其中一种方式是迭代传入action的所有参数(parameter),找到其中类似_id形式的那个,我们就知道了到底是哪一个model在查询评论(comment)。我们可以这样写一个方法加在CommentsController中。

   1      def find_commentable     
   2        params.each do |name, value|     
   3          if name =~ /(.+)_id$/     
   4            return $1.classify.constantize.find(value)     
   5          end    
   6        end    
   7        nil    
   8      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)了。

   1      def index     
   2        @commentable = find_commentable     
   3        @comments = @commentable.comments     
   4      end
添加评论

在index页面列出所有评论的同时,我们也希望用户能够添加评论。而我们的view的代码就是以下这样的。

   1      <h1>Comments</h1>     
   2  
   3      <ul id="comments">     
   4          <% @comments.each do |comment| %>     
   5              <li><%= comment.content %></li>     
   6          <% end %>     
   7      </ul>     
   8  
   9      <h2>New Comment</h2>     
  10      <% form_for [@commentable, Comment.new] do |form| %>     
  11          <ol class="formList">     
  12              <li>     
  13              <%= form.label :content %>     
  14              <%= form.text_area :content, :rows => 5 %>     
  15              </li>     
  16              <li><%= submit_tag "Add comment" %></li>     
  17          </ol>     
  18      <% end %>

第一部分的view代码渲染出一个无序的评论列表。下面跟着的是添加评论的form。而这个form的提交对象是一个嵌套资源(nested resource),我们使用一个数组表示,数组的第一个元素是我们将从find_commentable方法得到的commentable对象,第二个元素这是一个新的评论对象。

当这个form被提交的时候将会调用CommentsController的create action。

   1      def create     
   2        @commentable = find_commentable     
   3        @comment = @commentable.comments.build(params[:comment])     
   4        if @comment.save     
   5          flash[:notice] = "Successfully saved comment."    
   6          redirect_to :id => nil    
   7        else    
   8          render :action => 'new'    
   9        end    
  10      end

这里第一件需要注意的是我们又调用了find_commentable方法来获取当前评论的记录;将会是一篇Article,一张Photo或者一个Event;之后我们就可以使用build创建评论了。当我们保存了这个model后,返回index页面会有问题,因为不知道我们评论是嵌套在哪一个资源内的。所以我们使用一个有帮助的hack技巧重定向给id为nil的记录。这样我们就返回到当前(嵌套资源内)的index action然后显示出正确的页面来。

E154I02.png

我们评论完后正确的返回了index action。

我们的评论现在开始正常工作了。我们添加了一条评论并且返回了正确的页面。这功能在对Photo和Event model也同样能见效。

给读者的练习

接下来我们将要做的是把刚刚对index和create action做的改动套用到CommentsController的其他的action上去,就留给读者们自己实践吧。

供英语困难的同学参考用,英文好的同学还是建议看原版教程。如有错误,欢迎指正,发邮件到joey.d.darko@gmail.com

本篇是我参加蜗牛同学组织的asciicasts中文翻译计划完成的第二篇.目前已经发布在asciicast.com.