Rails 如何在 controller 为 model 中的实例动态地创建 model?

kevin · 2011年11月28日 · 最后由 huacnlee 回复于 2012年02月04日 · 4900 次阅读

借助 ruby-china,问一个一直困扰我的问题。

例如,我有个 surveys,每个 survey 可能包含量若干个不同类型的 question (可能是 email,可能是普通的 string,可能是 checkbox,也有可能是 ranking)。

现在想在 surveys_controller.rb 中创建一个方法,并且希望通过/surveys/id/publish 来建立相应的 model:

def publish
    @survey = surveys.find(params:id)
    class survey.id.to_s
        include Mongoid::Document
        include Redis::Search
        embeded_in response
        survey.questions.each do |question|
            field :question.id.to_s, :type => question.type, :default => question.default_value
            validate_presence_of question.id.to_s if question.required?
        end
    end
end

代码完全是我瞎扯的,关键是向各位大侠请教!

你还没明白不应该这样弄么

what_ever = Class.new # 生成一个类

看了几次,还是不太十分明白你的需求,不知是不是你要这个:

假设你可以获得了某个和 model 名字一样的字符串

str = 'user'
user = str.classify.constantize.find(id)

没看懂你的需求..

@Rei 能不能详细点呢?哪怕再多三五行代码示例。

@Rei 这段代码如何?是从 thundersurvey 上复制来的一段,但原来是基于 MongoMapper 的,移植到 Mongoid 一直移植不成功。

def user_klass
    klass ||= Class.new
    klass.send(:include, Mongoid::Document)
    klass.send(:include, Mongoid::Timestamps)
    klass.collection_name = self._id.to_s
    klass.key "created_at", DateTime
    klass.class_eval <<-METHOD
      def id
        self._id.to_s
      end
      def persisted?
        !new_record?
      end
    METHOD

    klass.instance_eval <<-NAME
      def name
        'Response'
      end
    NAME

    self.questions.each do |question|
      klass.key "q#{question.id}", String
      klass.validates_presence_of "q#{question.id}".to_sym, :message => I18n.t('activemodel.errors.messages.blank') if question.required_question
      klass.validates_uniqueness_of "q#{question.id}".to_sym, :message => I18n.t('activemodel.errors.messages.taken') if question.unique

      if question.input == 'check' || question.input == 'radio'
        klass.class_eval <<-METHOD
          alias_method :old_q#{question.id}=, :q#{question.id}=
          def q#{question.id}=(choices)
            if !choices.is_a?(Array)
              self.old_q#{question.id}= choices
              return
            end

            if choices.include?('_other')
              choices.delete('_other')
              other_options = choices.detect {|c| c.is_a?(Hash)}
              choices << other_options['other']
            end

            choices.reject! {|c| c.is_a?(Hash) || c.blank?}
            self.old_q#{question.id}= choices.join("\n")
          end
        METHOD
      end
    end
    klass
  end

你的需求是如何保存问卷格式和问卷结果,而不是如何动态生成类。

具体效果可参考:http://www.hujoy.com/surveys/ ,任何一个 survey 预览,可以发现现在这段代码,无法创建相应的存放调查结果的 response 实例。

问卷格式已经没问题,主要是问卷结果。原来 thundersurvey 实现是通过创建 survey.id.to_s 为名称的类。觉得这种方式值得选择。 如果拆解成不同类别问题用不同 model,倒是相对容易实现。

不要想类,想文档。

用户提交的是文档,数据库储存的是文档,到底需要类做什么呢?

有点混淆类和文档,明白只需文档即可。但关键是如何才能动态的创建文档呢,另外,针对不同 survey,每个文档具有不同的 field,不同的 field,类别和一些 validation 也不同。

你要做的可能是设计 validation 这块,创建问卷的时候保存校验信息,然后储存答卷的时候调用校验。

如果没有校验,mongodb 尽可以随意储存不同的 field 的文档。

> db.answers.insert({name: 'Rei', age: 24})
> db.answers.insert({weather: 'cold', temperature: [-1, 4]})
> db.answers.find();
{ "_id" : ObjectId("4ed3b3ad665202a0ac36e109"), "name" : "Rei", "age" : 24 }
{ "_id" : ObjectId("4ed3b3ea665202a0ac36e10a"), "weather" : "cold", "temperature" : [ -1, 4 ] }

不知道 MongoDB 的 hash 类型字段能否解决你的问题?

基于 hash 的存储,方便是方便,但总感觉这种方式的存储,没有固定 field 形式的存储性能好。维护也困难很多。 如果有我两个 survey,我希望针对这两个 survey,分别有不同的存储文档:

# survey 1: (named with survey.id.to_s)
:field name, :type => string
:field email, :type => string
:field question1.id, :type => integer
:field question2.id, :type => bollean

# survey 2: (named with survey.id.to_s)
:field score :type => integer
:field question1.id :type => string

这样的话,如何才能实现?

class Servey
  include Mongoid::Document
  embed_many :columns
  has_many :answers

  def check_validate(answer)
    columns.each do |column|
      column.errors.add(column.name, "some message") unless column.check_validate?(answer[column.name])
    end
  end
end

class Column
  field :name
  embed_in :servey

  def check_validate?(value)
    raise "Not implement yet."
  end
end

class StringColumn < Column
  def check_validate?(value)
    #do nothing
  end
end

class RangeColumn < Column
  field :max, :type => Integer
  field :min, :type => Integer

  def check_validate?(value)
    value <= max && value >= min
  end
end

class CheckColumn < Column
  field :values, :type => Array

  def check_validate?(value)
    values.include?(value)
  end
end

class Answer
  include Mongoid::Document
  belongs_to :servey
  attr_protected :servey_id

  validates :servey_validate
  def servey_validate
    servey.check_validate(self)
  end
end

# controller

def create
  @answer = @survey.answers.new params[:answer] # DYNAMIC FIELDS http://mongoid.org/docs/documents/dynamic.html
  if @answer.save
    #...
  else
    #...
  end
end

#15 楼 @Rei 话说有了回帖插图的那个功能,一下子有趣多了,哈哈哈 睡觉啦

@Rei, 非常感谢,晚上回去实现一下。

呵呵,不好,暴露了我的马夹。现在有没有会员合并功能呀,前阵子第三方帐号有时不给力,注册了两个马夹。kevin, kevinhua 和 hujoy 都是我。

登陆 -> 个人设置 -> 删除帐号

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