无法获得STI / ActiveRecord模型来做我期望的事情

时间:2013-05-11 20:01:27

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

我正在尝试构建一个简单的调查/问卷调查应用程序。调查有Questions;大多数问题由单个内容字段(问题本身)组成,调查接受者将在自由文本响应中写入。 (还有一些与此讨论无关的其他字段。)但是,用户还可以创建MultipleChoiceQuestionsLikertQuestions(例如,以1-5的比例回答)。 (如果是MultipleChoiceQuestions,则会有另一个名为Answer的模型,以便MultipleChoiceQuestion has_many Answers)。据我所知,这是我的设计选择:

1)继承问题:

class Question < ActiveRecord::Base
   attr_accessible :id, :content
end

class MultipleChoiceQuestion < Question
   attr_accessible :type
end

class LikertQuestion < Question
   attr_accessible :type, :min, :max, :label_min, label_max
end

2)使用带有共享属性和方法的模块/ mixin:

module Question
  @content, @id
  def method1
  end
end

class MultipleChoiceQuestion < ActiveRecord::Base
  include Question
end

class LikertQuestion < ActiveRecord::Base
  include Question
  attr_accessible :type, :min, :max, :label_min, label_max
end

这似乎是一个明确的继承案例,所以我选择了1.从那以后,我无法让它工作。单表继承似乎很简单,因此我在其架构中为每个MultipleChoiceQuestion提供了LikertQuestiontype:string。以下是每个模式(来自db / schema.rb):

  create_table "questions", :force => true do |t|
    t.integer  "parent"
    t.string   "type"
    t.string   "content"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
    t.integer  "survey_id"
  end

 create_table "multiple_choice_questions", :force => true do |t|
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
    t.string   "type"
  end

  create_table "likert_questions", :force => true do |t|
    t.integer  "min"
    t.integer  "max"
    t.string   "label_min"
    t.string   "label_max"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
    t.string   "type"
  end

如果我实现上面的选项1,那么MultipleChoiceQuestion和LikertQuestion实际上并不包含schema.rb中指定的任何唯一字段;相反,它们只有来自Question的继承字段。请参阅控制台输出:

1.9.3p392 :001 > Question
 => Question(id: integer, parent: integer, content: string, created_at: datetime, updated_at: datetime, survey_id: integer)
1.9.3p392 :002 > LikertQuestion
 => LikertQuestion(id: integer, parent: integer, content: string, created_at: datetime, updated_at: datetime, survey_id: integer)
1.9.3p392 :003 > MultipleChoiceQuestion
 => MultipleChoiceQuestion(id: integer, parent: integer, content: string, created_at: datetime, updated_at: datetime, survey_id: integer)
1.9.3p392 :004 > LikertQuestion.new(:min => 3)
ActiveRecord::UnknownAttributeError: unknown attribute: min

StackOverflow上有人说问题应该是一个抽象类。但如果我补充一下 self.abstract_class = true到Question.rb,然后我得到以下内容:

1.9.3p392 :001 > Question
 => Question(abstract)
1.9.3p392 :002 > LikertQuestion
 => LikertQuestion(id: integer, min: integer, max: integer, label_min: string, label_mid: string, label_max: string, created_at: datetime, updated_at: datetime, type: string)
1.9.3p392 :003 > MultipleChoiceQuestion
 => MultipleChoiceQuestion(id: integer, created_at: datetime, updated_at: datetime, type: string)
1.9.3p392 :004 > LikertQuestion.new(:content => "foo")
ActiveRecord::UnknownAttributeError: unknown attribute: content

LikertQuestionMultipleChoiceQuestion仅显示 其唯一字段,并且不会从父级继承字段。

1)我在这里缺少什么?无论继承是否是最佳解决方案,我必须忽略一些明显的东西。

2)我应该使用模块方法而不是继承吗?正如我所提到的,继承看起来很简单:LikertQuestionMultipleChoiceQuestion实际上是Questions种。如果我使用模块方法,我就无法说出像survey.questions()survey.questions.build()这样的东西,并且可能是其他方便的东西。在这种情况下,Rails的热点是做什么的?我会做任何事情。

StackOverflow上的帖子没有提供关于子类与mixin的优缺点的全面讨论。

使用Ruby 1.9.3(虽然考虑切换到2.0),Rails 3.2.3。

1 个答案:

答案 0 :(得分:3)

你确实遗漏了一些明显的东西。你知道STI代表什么吗? 单表继承。您正在制作几个表,然后尝试使用STI。

如果您的表格相同或非常相似(可能是1个不同的字段),您应该只使用STI。它主要用于您想要子类化,然后提供区分行为的方法。例如,也许所有用户共享相同的属性,但其中一些属于管理员。您可以在users表中添加type字段,然后可能会出现以下内容:

class Admin < User
  def admin?
    true
  end
end

class NormalUser < User
  def admin?
    false
  end
end

(这显然是一个非常简单的例子,并且可能不会保证它自己的STI。)

就抽象类而言,如果你有几个表都应该从超类继承行为,这是一个很好的决定。看起来你的情况可能有意义;但是,重要的是要注意抽象类没有表。将abstract_class声明为真的重点是,在尝试查找不存在的表时,ActiveRecord不会感到困惑。没有它,ActiveRecord将假设您正在使用STI并尝试查找问题表。在您的情况下,您确实有一个问题表,因此将其声明为抽象类并没有多大意义。

另外一件事,你问“我应该使用模块方法而不是继承吗?”。使用模块实际上是Ruby中的一种继承形式。当你包含一个模块时,它就像超类一样被插入到祖先链中的类中(然而,模块是在超类之前插入的)。我确实认为某种形式的继承是正确的方法。在这种情况下,因为它们都是问题的类型,所以制作一个抽象的问题超类对我来说是有意义的。因为这些问题没有共享许多属性,所以在我看来,将它们存储在单独的表中是最好的解决方案。当你有几个不同的字段时,STI并不是一个好的做法,因为它会在你的数据库中导致很多null

为了清楚模块,我认为最好在几个不相关的模型共享某种形式的共同行为时完成。我多次使用的一个例子是Commentable模块的概念(使用ActiveSupport :: Concern)。仅仅因为可以评论几个模型并不一定能保证超类,因为模型不相关 - 它们并不真正来自某种父对象。这是模块有意义的地方。在您的情况下,超类是有意义的,因为您的两个模型都是问题类型,因此它们都来自通用Question基类似乎是合适的。