Rails Model 中的 respond_to? 对于 render 的影响 / 利用 method_missing 完成 hash 字段在 form_for 中的使用。

jzlikewei · 2015年08月11日 · 最后由 jzlikewei 回复于 2015年08月16日 · 2725 次阅读

需求是这样的。 我需要储存一些 metadata,所以搞了个序列化的 information 作为储存。然后呢,为了用着方便,通过 method_missing 完成了幽灵方法。这样我就可以通过类似 s.info_location 来访问 s.infomation['location'],同时也方便在 form_for 中使用。 代码如下,其余代码均由 scaffold 生成:

class Series < ActiveRecord::Base
  validates :name, :uniqueness => true
  serialize :information, JSON
  def method_missing(name,* args)
    super unless respond_to? name
    attribute = name.to_s
    attribute = attribute[5..attribute.length]
    if attribute[-1]=='='
      self.information[attribute.chop]=args[0]
    else
      self.information[attribute]
    end
  end
   def respond_to?(method)
     method_str= method.to_s
     method_str.include? 'info_' || super
   end
  def to_s
    name
  end
end

结果出现的情况是,渲染/series/2/edit 页面时,却错误的渲染成了 new 的页面,如下图所示:

将 Series.rb 修改为下面代码后,正常渲染。(既,删掉对 respond_to? 的复写)

class Series < ActiveRecord::Base
  has_many :episodes
  has_many :hobbies
  validates :name, :uniqueness => true
  serialize :information, JSON
  def method_missing(name,* args)
    attribute = name.to_s
    super unless attribute.include? 'info_'
    attribute = attribute[5..attribute.length]
    if attribute[-1]=='='
      self.information[attribute.chop]=args[0]
    else
      self.information[attribute]
    end
  end
  # def respond_to?(method)
  #   method_str= method.to_s
  #   method_str.include? 'info_' || super
  # end
  def to_s
    name
  end
end

最后完成效果是: views/series/_form.html.erb

<%= form_for(@series) do |f| %>
  <% if @series.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@series.errors.count, "error") %> prohibited this series from being saved:</h2>

      <ul>
      <% @series.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :description %><br>
    <%= f.text_field :description %>
  </div>
  <div class="field">
    <%= f.label :info_location %><br>
    <%= f.text_field :info_location %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

controllers/series_controller.rb

class SeriesController < ApplicationController
  before_action :set_series, only: [:show, :edit, :update, :destroy]

  def index
    @series = Series.all
  end

  def show
  end

  def new
    @series = Series.new
  end

  def edit
  end

  def create
    @series = Series.new(series_params)

    respond_to do |format|
      if @series.save
        format.html { redirect_to @series, notice: 'Series was successfully created.' }
        format.json { render :show, status: :created, location: @series }
      else
        format.html { render :new }
        format.json { render json: @series.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @series.update(series_params)
        format.html { redirect_to @series, notice: 'Series was successfully updated.' }
        format.json { render :show, status: :ok, location: @series }
      else
        format.html { render :edit }
        format.json { render json: @series.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @series.destroy
    respond_to do |format|
      format.html { redirect_to series_index_url, notice: 'Series was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_series
      @series = Series.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
   ######################################
   #  add :info_xxx to access information['xxx']               # 
   ######################################
    def series_params
      params.require(:series).permit(:name, :description, :info_location)
    end
end

既,在 form_for 中可以直接像普通属性一样通过 info_xxx 来访问 information 这个 hash 中的变量。

麻烦楼主能把下面的代码运行一下么?

series.respond_to?(:info_location)
series.method(:info_location)
series.info_location

#1 楼 @flemon

s=Series.first
  Series Load (0.1ms)  SELECT  "series".* FROM "series"  ORDER BY "series"."id" ASC LIMIT 1
 => #<Series id: 2, name: "Over Lord", description: "装逼神作", information: {"location"=>"日本s", "start_time"=>"2015-07"}, created_at: "2015-08-10 17:10:53", updated_at: "2015-08-11 08:07:58">
2.2.0 :002 > s.respond_to?(:info_location)
 => true
2.2.0 :003 > s.method(:info_location)
NameError: undefined method `info_location' for class `Series'
    from (irb):3:in `method'
    from (irb):3
2.2.0 :004 > s.info_location
 => "日本s"
2.2.0 :005 >

删掉 respond_to? 之后,唯一的区别就是。s.respond_to?(:info_location) 的返回值为 false

@jzlikewei 神速回复啊

恩,我就是想提醒一下,你只 override method_missing 会造成逻辑混淆,你知道怎么回事,别人不一定知道,或者要找好久才能找到原因,我一般会同时 override 两个方法:method_missing 和:respond_to_missing 以避免此逻辑混淆

#3 楼 @flemon respond_to_missing 是干嘛用的呢?

@jzlikewei :respond_to_missing 是 ruby 1.9.2 出台,用来解决 即使 override 了:respond_to?还是不能把所自定义的方法使用起来像个 ruby 方法 (i.e. 无法满足method()的调用)。它即实现了respond_to?又满足了method()的调用。 具体的代码可寻此处

  • 1 @jzlikewei 这个问题我们在 ruby 的群里说过:)
def respond_to?(method)
     method_str= method.to_s
     method_str.include? 'info_' || super
end

这段代码的执行顺序是先执行或,然后执行 include?,所以当 method 中不含有'info_'时 respond_to?返回都是 false。

操作符的优先级:Ruby operators

  • 2 至于为何 respond_to 返回 false 后 form 会不同,form_helper.rb 中:
def apply_form_for_options!(record, object, options) #:nodoc:
....
   action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
....
end

object.respond_to?(:persisted?)来判断 object 是否是持久化的来生成相应的 form

#6 楼 @mueven 谢谢,解决疑惑了。

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