Rails 如何在 Mongoid 下做出 active_record 下的 STI (Single Table Inheritance) 逻辑?

suffering · 2012年03月22日 · 最后由 datty258 回复于 2013年12月12日 · 5193 次阅读

碰到个蛋疼的要求!类似于在 active_record 下使用的 STI(Single Table Inheritance) 逻辑。 换个说法:Mongoid inheritance,如何利用父类的 controller 与 view 创建对象到指定子类? Model 如下: Models

#1
class Category
  include Mongoid::Document
  field :name
  has_many :products
  has_many :pages
end

#2
class Pagecategory < Category
end

#3
class Prodcategory < Category
end

#4
class Page
  include Mongoid::Document
  field :name
  field :content
  belongs_to :category
end

#5
class Product
  include Mongoid::Document
  field :name
  field :description
  belongs_to :category
end




要求可以使用已有的 categories 的 controller 与 views 直接创建 Pagecategory 与 Prodcategory 的实例。(不新建任何 controller 与 views)。 可以直接在新建 product 时选择其类。模式如下: <%= simple_form_for @Product do |f|%> <%= f.input :category_id, :collection => Prodcategory.all %> <%= f.input :name %>

<%= f.submit %> <% end %> 问题变成了如何在创建category时,将其自由创建到Pagecategory或Prodcategory?(不让新建controller与view) 这相当于是让 categories 多态。但是,有这种搞法吗? 有没有好的解决方案?

@_@ 一沉到底了?

ActiveRecord 和 Mongoid,inhert 都不影响 id 阿,只要你关联好 id 就可以读到对应的对象

#2 楼 @Rei ,如何设置这个_type?在 form 中添加:_type 字段? 我自己有做过一个方案,用的就是这个:_type. 要求是达到了。但是,觉得代码相当丑陋。既然 mongoid 做了相应的设置,应该有些简单明白的解决方案吧。

#4 楼 @suffering 是说创建 category 的时候?就是填 _type 阿,另外做个校验只允许已有的类型,不然写进脏数据会抛错。

#5 楼 @Rei ,我帖上我的解决方案吧。我是先用这个方案解决了问题。连帖子都写好了。准备帖在分享栏的。但是想想觉得太丑陋了。于是才发帖问有没有通用的解决方案的:

Mongoid + Rails STI(Single Table Inheritance)

Models

class Category
  include Mongoid::Document
  field :name
  has_many :products
  has_many :pages
end

class Pagecategory < Category
end

class Prodcategory < Category
end

class Page
  include Mongoid::Document
  field :name
  field :content
  belongs_to :category
end

class Product
  include Mongoid::Document
  field :name
  field :description
  belongs_to :category
end




Prodcategory 继承自 Category, 创建 Prodcategory 实例后,会将其在座存储于 Category 表中。同时在 Category 表中添加 _type 对象。

mongoid.org 的描述如下:An additional attribute _type is stored in order to make sure when loaded from the database the correct document is returned. 详情参见http://mongoid.org/docs/documents/inheritance.html

Prodcategory.create(name: 'product category 1')在 mongodb 中实际保存在 Category 表中,其_type 为'Prodcategory'.

这时,没有必要专门去创建 prodcategories_controller.rb 与 pagecategories_controller.rb 以及相应的 views。 所要做的事情是对 routes.rb , views/categories/_form.html.erb 及 controllers/categories_controller.rb 进行一些修改。

修改后的 routes.rb

resources :pages
  resources :products
  resources :categories
  resources :prodcategories, :controller => "categories",:_type => "Prodcategory"
  resources :pagecategories, :controller => "categories",:_type => "Prodcategory"




修改后的 views/categories/_form.html.erb

<%= simple_form_for(@category) do |f| %>
<%= f.error_notification %>
  <div class="form-inputs">
    <% if f.object.new_record? %>
      <%= f.object._type = params[:_type] %>
    <% end %>
    <%= f.input :_type, :collection => [['Default Category','Category'],['Product Category','Prodcategory'],['Page Category','Pagecategory']], :include_blank => false %>
    <%= f.input :name %>
  </div>
  <div class="actions">
    <%= f.button :submit %>
  </div>
<% end %>


进行了部分修改的 controllers/categories_controller.rb

def update
  @category = Category.find(params[:id])
  _s = @category.class.to_s.downcase #更改params upload的hash名.
  respond_to do |format|
    if @category.update_attributes(params[_s]) #upload it!
      format.html { redirect_to @category, notice: 'Category was successfully updated.' }
      format.json { head :no_content }
    else
      format.html { render action: "edit" }
      format.json { render json: @category.errors, status: :unprocessable_entity }
    end
  end
end




在原来的基础上添加了:_type 字段的下拉选框

<%= simple_form_for @category do |f| %>
<%= f.error_notification %>
  <div class="form-inputs">
    <% if f.object.new_record? %>
      <%= f.object._type = params[:_type] %>
    <% end %>
    <%= f.input :_type, :collection => [['Default Category','Category'],['Product Category','Prodcategory'],['Page Category','Pagecategory']], :include_blank => false %>
    <%= f.input :name %>
  </div>
   <%= f.button :submit %>
<% end %>




Done!

举例来说: 这时,通过prodcategories/new即可直接创建 Prodcategory 的实例。 新建的实例可以通过 Prodcategory 来查找,访问。 如新建 product 时,可以使用这样的 Form:

<%= simple_form_for(@product) do |f| %>
  <%= f.error_notification %>
  <div class="form-inputs">
    <%= f.input :category_id, :collection => Prodcategory.all, :include_blank => false %>
    <%= f.input :name %>
    <%= f.input :description %>
  </div>
  <div class="form-actions">
    <%= f.button :submit %>
  </div>
<% end %>



#6 楼 @suffering 建议还是分开,不要用继承

有个公司项目早前也用了单表继承,后来随着需求变动,显示逻辑增加了,各种复杂的 join 查询就出来了,还是得拆开。

我觉得用 mongodb 和用 rdbms 时的建模思路大不相同,有时候最好不要照搬 rdbms 那套。

使用继承是出于这样一种考虑: 我个人在做些小公司的项目时,经常会碰到的事情是 products,supports,news,serivces 之类的 resources 都会有其分类。 category 只有一个 name 字段,一个 position 字段,为每一种资源新建一个 scaffold,如为 products 新建 ProductCategory.这样会成生大量的重复的 MVC 文件,实在是不可忍。 因此,出于 rubist 的本能,选择的是将它们全部 blongs_to 到 category。这时就会发现查找时会有困难。比如说,新建 products 页面时,一般会提供一个下拉框给用户选择 product 所属的类。这个下拉框里的可选 categories list 应当如何查找生成?

同时还有多级分类的问题。比如说,产品要分为一级分类,其下有二级分类,再往下....。当然,这样相当不好。但是往往会碰到这样的要求。 于是只有去对 category 做 self join...

....呃,说着说着我自己都觉得有点乱了。:)

#4 楼 @suffering #6 楼 @suffering 为什么我在 Controller 里直接设置_type 的值,如 “@exchange._type = "Gift"” 会出现报错 “没有_type 方法”,但是我在控制台下给_type 直接赋值就没有问题,这个怎么理解?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号