rails_多态关联

什么是多态关联

假如有三个模型,分别是 用户, 产品, 图片。图片为用户所有,也为产品所有。我们可以创建两个 picture 的模型,如下

rails g modle picture_user user_id:integer name:string url:string

rails g modle picture_product product_id:integer name:string url:string

class PictureUser < ApplicationRecord
  belongs_to :user
end

class PictureProduct < ApplicationRecord
  belongs_to :product
end

这样我们就可以使用user.pricturesproduct.pictures来分别获得用户下和产品下的图片了。但是我们发现,两个图片模型除了外键不一样,其他字段都是一样的,那么有没有一种办法只创建一个 picture 模型,同时属于 user 和 product 呢,这种既属于一个模型又属于另外一个模型(可以是很多个)的关联就是多态关联。

多态关联的实现

为了能同时使用 user.picturesproduct.pictures 来获得各自的图片,我们就需要对 picture 模型做一些修改,使其能够标识一张图片是属于 user 还是属于 product ,当然外键是必不可少的。我们还需要一个外键对应类的名称,如下:

rails g modle picture pictureable_id:integer pictureable_type:string name:string url:string

class Picture < ApplicationRecord
  belongs_to :pictureable, polymorphic: true
end

class User < ApplicationRecord
  has_many :pictures, as: :pictureable
end

class Product < ApplicationRecord
  has_many :pictures, as: :pictureable
end

pictureable 相当于一个接口,凡是拥有图片的模型都可以像 User 那样使用关联。

可以使用user.pictures.create(name: 'user_0', url: 'https://dcc.com')来创建一条关联对象,创建之后我们发现在 picture 表中多了一条记录:

id: 1, pictureable_type: 'User', pictureable_id: 1, name:"user_0", url:'https://dcc.com'

pictureable_type: ‘User’ 就是所属对象的标识,这样才可以使用 user.pictures 进行查询。由此我们知道,多态关联中,xxxable_type, xxxable_id字段是必不可少的。

-————————————————————————————

下面是关于多态 view 页面使用的讲解原文

什么是多态

Rails 模型中的关系有一对一,一对多还有多对多,这些关联关系都比较直观,除此之外 Rails 还支持多态关联,所谓的多态关联其实可以概括为一个模型同时与多个其它模型之间发生一对多的关联。并且在实际的应用中这种关系也十分普遍,比如可以应用到站内消息模块,评论模块,标签模块等地方,下图就是多态关系下的评论模块的 E-R 图。

通过 E-R 图,我们能直观的看到系统中的事件,文章以及照片都可以被用户评论,并且这些评论都被存储在一张叫 comments 表中。Ok,现在我们已经搞清楚了多态的含义,下面继续看下 Rails 中是如何实现多态关联的。

Rails 中实现多态的步骤

这里我们通过将 Rails Guides 中给出的例子线性化(转化为详细步骤)来说明这个问题。

Step 1: 通过 Migration 创建表

执行下面命令来生成 Migration 文件

rails g model picture name:string imageable_id:integer  imageable_type:string

生成的 Migration 文件如下:

class CreatePictures < ActiveRecord::Migration
  def change
    create_table :pictures do |t|
      t.string  :name
      t.integer :imageable_id
      t.string  :imageable_type
      t.timestamps null: false
    end

    add_index :pictures, :imageable_id
  end
end

其中需要特别关注, imageable_idimageable_type 两个字段,前者用来存储相关联内容的外键键值,后者则用来存储相关联内容的类型名。后面在通过模型查找关联内容的时候,可以通过这两个值来定位到要找到的内容。特别是后者 imageable_type 的存在是多态实现的关键。

Step 2: 修改各 Model 得关联关系

按照上面 E-R 图和代码修改模型结构,因为 Employee,Product 分别与 Picture 是一对多的关系,所以用到了 has_many 与 belongs_to 方法,再使用 polymorphic 与 as 来指明是多态关联。

Step 3: Controller 中应用

上面的两步完成后就能在 Controller 中通过多态关联关系进行相互访问了,并且通过关联关系创建的新评论 Rails 也会自动帮你设置 commentable_id 与 commentable_type 两个字段的值。

event = Event.create name: "event1"
event1= event.comments.create content: “comment1”
event1.commentable_type #=> “Event”

Done!到此就算完整应用到了多态关联关系,后续需要处理的就是如何来组织代码让多态关系更加灵活便捷的被你操作,不过这个就应该是另一篇文章的内容了。:)

刚开始看 Rails Guide 的时候对多态的表关联真的是一头雾水。后来自己写了一个博客应用的时候用到了 acts_as_commentable 这个 gem,它就是用到了多态表的关联,然后我又看了 Terry 在 railscasts china 上的 视频 ,对多态的理解就深了很多。

理解什么是多态

一般表的关联有一对一,一对多,多对多,这些都是非常好理解的,然后对于多态的表关联可能稍微有点不好理解。其实多态关键就是一个表关联到多个表上。就如 Comment(评论)表吧,一个 Topic 应该有 Comment(一个帖子应该有许多的评论),除此之外 Micropost(微博)也可能有很多的 Comment。然后一个网站中既有 Topic 的论坛功能,又有 Micropost 的功能,我们怎么处理 Comment 表呢?当然我们可以建两个独立的表比如 TopicComment 和 MicropostComment,再分别关联到 Topic 和 Micropost 上,但这不是一种好的选择,我们可以只建一个表,然后去关联这两个表,甚至多个表。这也就实现了多态的能力。

一个例子

1.首先我们先生成一个 Comment 的 model,假设已经有 Topic 和 Micropost 这两个 model 了

rails g model comment content:text commentable_id:integer comment_type:string

2.然后我们 会得到一个 migration

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.text :content
      t.integer :commentable_id
      t.string  :commentable_type
      t.timestamps
    end
  end
end

也可以通过 t.references 来简化上面的

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.text :content
      t.references :commentable, :polymorphic => true #这里指明了多态,这样会生成comment_id和comment_type这两个字段的,如上
      t.timestamps
    end
  end
end

多态魔法就在这里,commentable_typle 字段用于指明 comment 所关联的表的类型,如 topic 或 micropost 等,而 comment_id 用于指定那个关联表的类型对象的 id。如:可以把一个 comment 关联到第一篇 topic 上,那么 comment_type 字段为 topic,而 comment_id 为对应 topic 对象的 id 1,同理这样就可以关联到不同表了,从而实现多态的关联。

3,数据迁移 rake db:migrate 就能生成我们要的表了

4,对 model 进行操作从而现实表的关联

####comment model
class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true
end

看到没有,这里的 comment belongs_to 没有写 topic,micropost 等,而写了 commentable,因为 commentable 中有 type 和 id 两个字段,可以指定任何其他 model 对象的,从而才能实现多态,如果这里写 belongs_to topic 的话就没办法实现多态了。然后我们看看 topic 和 mocropost 的 model 该如何写。

class Topic < ActiveRecord::Base
  has_many :comments, :as => :commentable
end
class Micropost < ActiveRecord::Base
  has_many :comments, :as => :commentable
end

看到这里的 as 了吗?as 在这我们可以解释为:作为(我的理解,可能这种理解补科学,哈哈),也就是说 Topic 有许多的 comments,但是它是通过将自己作为 commentable,实现的。Micropost 同理。

然后就是 controller 和 views 中(如 form 表单)的设计了,这也是我刚学的时候,最头疼这个了,因为对 params 参数通过表单到 controller 的传递没掌握好。

在写这些之前,我们先看看如何写路由吧,因为一个 topic 有多个 comments,Micropost 同理。所以我们可以这样写

resources :topics do
  resources :comments
end

resources :microposts do
  resources :comments
end

然后我们通过命令 rake routes 就可以得到相应的路由了如:

          topic_comments GET    /topics/:topic_id/comments(.:format)          comments#index
                                    POST   /topics/:topic_id/comments(.:format)          comments#create
       new_topic_comment GET    /topics/:topic_id/comments/new(.:format)      comments#new
      edit_topic_comment GET    /topics/:topic_id/comments/:id/edit(.:format) comments#edit
           topic_comment GET    /topics/:topic_id/comments/:id(.:format)      comments#show
                                    PUT    /topics/:topic_id/comments/:id(.:format)      comments#update
                                   DELETE /topics/:topic_id/comments/:id(.:format)      comments#destroy

这些待会我们会用到。

然后我们再来分析 controller 和 views 之间的参数传递。我们通过完整的创建 comment 的过程进行说明

(1)首先页面上肯定有一个创建 comment 的连接或按钮(假设创建 comment 的表单和 topic show 页面不在统一页面上),代码应该是这样的:

<%= link_to "发表评论", new_topic_comment_path%>

(2)点击这个链接后,通过路由来到 controller 中的 new 方法(同时会将对应的 topic 相关的参数传给 controller)

def new
  @topic = Topic.find(parmas[:id]) #找到comment属于的topic
  @comment = @topic.comments.build #建立这个关系
end

(3)经过这个方法(action)后,页面来到了 comments/new.html.erb,在这个页面中有一个评论的表单,大概是这样的

 <%= form_for([@comment.commentable, @comment]) do |f| %>
  ......
<%end%>

这个表的参数是一个数组,[email protected]

@comment,如果没有关联的化,[email protected],

[email protected]

��,还有一个就是 commentable,这里也就是 topic。

还记得 new 中的 @comment = @topic.comments.build 的吗,这里就暂时将对应的 topic 对象写入 commentable(注意:只是暂时建立关系,还没有写入数据库),[email protected]

@topic。

(4)然后你填完表单后,按提交按钮后,表单中的参数(包括 commentable,@post 的 id 等信息),一起来到 controller 的 create 方法中

def create
  Topic.find(parmas[:topic_id]).comments.create(parmas[:comment])
  ......
end

这样就真正创建了一个新的 comment。micropost 同理。

其实多态讲的也差不多了,但在提一个地方

**重要知识点:**假设一个 comment 已经建立了,它的 commentable_type 是:topic.comment_id 是 1。如果我们得到了这个 id 为 1 的 topic,@topic,那么我们怎么得到它的 comments 呢?是的很简单,直接 @topic.comments 就 ok 了。但是反过来呢,我们得到了这个 comment,@comment,我们如何得到对应的 topic 的信息呢?我以前刚学的时候,就用了@comment.topic ,呵呵,没错,得到的是一串错误,正确一概是 @comment.commentable

关于多态我们已经讲的差不多了。

补充:上面的例子 comment 的表单是独立在 comments/new.html.erb 中的,但是一般的应用 comment 的表单是在 topics/show.html.erb 中,也就是上面一个 topic,topic 下有一个 comment 表单。这样的话在 controller 中我们就不需要 new 这个方法了,那么我们在哪建立关系呢?

@comment = @topic.comments.build #建立这个关系

我们就在表单的 <%= form_for ...%> 前面写 <@comment = @topic.comments.build>