【问题标题】:Rails single-table inheritance: determining class programmaticallyRails 单表继承:以编程方式确定类
【发布时间】:2014-08-24 05:01:15
【问题描述】:

在我的 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 来更改此字段)。

但在我的例子中,答案的类型应该始终与其对应问题的类型相匹配,这样就需要管理一个额外的数据库字段,这样既尴尬又多余。因此,我不想严格依赖数据库中的内容,而是想以编程方式确定给定记录应该使用哪个类。例如:

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

这可能吗?如果有,怎么做?

【问题讨论】:

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


    【解决方案1】:

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

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

    这不会帮助您动态确定类名的存储位置,因为它只是指向另一列,然后由该列中的值确定类。

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


    Single-Table-Inheritance (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
    

    为了进一步美化代码,您可以将这些回答行为放入子目录“./concerns/answer_behavior/”中,并将它们包装在模块“AnswerBehavior”中。

    【讨论】:

    • 是的,我知道这一点。事实上,我在我的问题中链接到了同一个文档页面。我的问题中是否有任何内容让您相信我的想法并非如此?
    • 感谢您更新和澄清您的问题 - 请参阅更新后的答案
    • 有趣的想法。与继承相比,这似乎有点尴尬,但它确实解决了问题中给出的问题。
    • 这是元编程 - 动态扩展行为 ;-)
    【解决方案2】:

    这是single table inheritance in Rails 的绝佳资源


    性传播感染

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

    STI(单表继承)是您的应用程序从单个表中提取数据的一种方式,同时使用不同的“类”来区分这些数据。描述其为何有效的最佳方式是向您介绍 Rails 的object orientated 性质:

    您在 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
    

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

    【讨论】:

    • 嗯,我想也许我没有很好地传达我的问题;似乎没有人明白我在问什么。也许我应该更具体地说明我的确切问题,而不是在更抽象的层次上讨论单表继承。在我编辑的时候……
    • 我已经完成了我的编辑。希望它能更好地说明我要解决的实际问题是什么。
    • 嗯,我仍然认为您对 STI 感到困惑,但让我为您写一个更新
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-12
    • 1970-01-01
    • 2011-08-29
    相关资源
    最近更新 更多