Rails单表继承:以编程方式确定类

时间:2014-08-20 15:49:41

标签: ruby-on-rails single-table-inheritance

在我的Rails应用程序中,我正在实现类似调查的表单,允许用户填写动态可更改的问题集的答案。目前,我计划支持三种不同的“类型”问题:是/否,评级(1-5级)和文本。对于每种问题类型,我需要对相应的答案模型略有不同的行为(以适应不同的方法,验证要求等)。我目前正在尝试使用单表继承实现这些不同的行为,如下所示:

class Question < ActiveRecord::Base
  validates_presence_of :question_type

  # ...
end

class Answer < ActiveRecord::Base
  belongs_to :question

  validates_presence_of :answer

  # ...
end

class YesNoAnswer < Answer
  validates :answer, inclusion: {in: %w(Yes No N/A)}

  # ...
end

class RatingAnswer < Answer
  validates :answer, numericality: { only_integer: true }, inclusion: {in: 1..5}

  def answer
    self[:answer].to_i
  end

  # ...
end

class TextAnswer < Answer
  validates :answer, length: { minimum: 2 }

  # ...
end

问题在于,对于单表继承,所选类通常由数据库中每个记录的字段确定。 (默认情况下,此字段为"type",但您可以通过设置inheritance_column来更改此字段。

在我的情况下,答案的类型应始终与其相应问题的类型相匹配,因此需要管理额外的数据库字段,如此笨拙和冗余。因此,我不想严格依赖数据库中的内容,而是想以编程方式确定应该为给定记录使用哪个类。 E.g:

class Answer < ActiveRecord::Base
  # I want a value on the associated question to determine this record's type.
  # Simply defining a type method as shown here doesn't work though.
  def type
    question.try(:question_type)
  end
end

这可能吗?如果是这样,怎么样?

2 个答案:

答案 0 :(得分:3)

inheritance_column不确定类,它存储type列的名称,然后确定用于给定记录的类。

因此,通过更改inheritance_column值,您可以存储通常存储在另一列type列中的内容。

这无法帮助您动态确定类名存储的位置,因为它只指向另一列,然后通过该列中的值确定Class。

另请参阅:http://apidock.com/rails/ActiveRecord/ModelSchema/ClassMethods/inheritance_column


单表继承(STI)特别适用于您希望保留模型的情况,这些模型有些相似,但根据类型保留不同的属性。 换句话说:除非你想在模型中保留不同的东西,否则你不应该使用STI。

如果您只是想要根据问题的类型采取不同的行为,您可以这样做: 根据问题的类型,通过在必要的Ruby模块上调用extend,使用您喜欢的行为扩展Answer实例:

class Question < ActiveRecord::Base
  validates_presence_of :kind
  has_many :answers
  # ... contains an attribute 'kind' to determine the kind of question
end

class Answer < ActiveRecord::Base
  belongs_to :question
  validates_presence_of :answer

  # when a new instance of Answer is created, it automatically extends itself's 
  # behavior from the given Ruby Module, which is conveniently stored as a Rails Concern
  def initialize
    case question.kind
    when 'yes_no'
      self.class.send(:extend, YesNo)
    when 'rating'
      self.class.send(:extend, Rating)
    when 'text'
      self.class.send(:extend, Text)
    else
      # ...
    end
  end

end

并且在./app/models/concerns中,您有不同的文件,其中包含根据问题类型定义要添加到Answer类的行为的模块:

 # file concerns/yes_no.rb
 module YesNo
  validates :answer, inclusion: {in: %w(Yes No N/A)}
 end

 # file concerns/rating.rb
 module Rating
  validates :answer, numericality: { only_integer: true }, inclusion: {in: 1..5}  

  def answer
   self[:answer].to_i
  end
  # ...
end

# file concerns/text.rb
module Text
  validates :answer, length: { minimum: 2 }
end

为了更好地美化代码,您可以将这些答案行为放入子目录&#39; ./ concerns / answer_behavior /&#39;中,并将它们包装在模块&#39; AnswerBehavior&#39; ;

答案 1 :(得分:2)

这是single table inheritance in Rails

的绝佳资源

<强> STI

根据您的要求,您似乎需要考虑几件事情,从STI的工作方式开始

STI(单表继承)是一种应用程序从单个表中提取数据的方法,同时使用不同的“类”来区分这些数据。描述为什么这样做有效的最好方法是给你降低Rails的object orientated性质:

enter image description here

你在Rails中所做的一切都需要基于对象。这些是根据您的模型(从数据库中获取数据)构建的。 STI关系的绘制是您将能够构建围绕所需的特定数据类型的对象。

例如:

#app/models/answer.rb
Class Answer < ActiveRecord::Base
   #fields id | type | question_id | other | attributes | created_at | updated_at
   belongs_to :question
end

#app/models/question.rb
Class Question < ActiveRecord::Base
  has_many :answers
end

#app/models/yes_no.rb
Class YesNo < Answer
   ... custom methods here ...
end

-

<强>用法

直接参考您的问题 - 您遇到的问题是您没有正确使用基于STI的模型。他们 应该像其他模型一样工作,除了从单个表中绘制数据(因此他们的名字):

#app/controllers/questionnaires_controller.rb
Class QuestionnairesController < ApplicationController
   def create
       @question = YesNo.new(questionnaire_params)
       @question.save
   end
end

只要您使用模型中的文件/设置使其按要求运行,调用模型的type就不会有任何影响。

-

  

在我的情况下,我不想严格依赖数据库中的内容,而是想以编程方式确定应该为给定记录使用哪个类。这可能吗?如果是这样,怎么样?

如果这是你想要实现的功能,我认为你正在使用STI的错误。我可能是错的 - 但你肯定想要预先调用模型(如上所示),允许你按照自己的意愿操纵它们的对象?


<强>更新

根据您所写的内容,可以更好地仅使用Answer模型,设置特定属性以确定最初发送请求的问题:

#app/models/answer.rb
Class Answer < ActiveRecord::Base
   before_create :set_question_type

   private

   def set_question_type
      self.question_type == "yes/no" unless question_type.present?
   end
end

这将允许您根据需要设置问题类型