3
votes

In my Rails application, I am implementing a survey-like form which allows users to fill out answers to a dynamically changeable set of questions. Currently, I'm planning to support three different "types" of questions: Yes/No, Rating (on a 1-5 scale), and Text. For each of these question types, I need slightly different behavior for the corresponding Answer model (to accommodate different methods, validation requirements, etc). I am currently trying to implement these different behaviors using single table inheritance, like so:

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

The problem is that with single-table inheritance the chosen class is normally determined by a field in the database for each record. (By default, this field is "type", but you can change this by setting inheritance_column).

In my case though, the type of the answer should always match the type of its corresponding question, making the need to manage an extra database field like this awkward and redundant. So rather than rely strictly on what's in the database, I want to determine programmatically what class should be used for a given record. 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

Is this possible? If so, how?

2

2 Answers

3
votes

inheritance_column does not determine the Class, it stores the name of the type column, which then determines what Class to use for the given record.

So by changing the inheritance_column value, you can store what you would usually store in the type column in another column.

This would not help you to dynamically determine where the Class name is stored, because it just points to another column which then determines the Class by the value in that column.

See also: http://apidock.com/rails/ActiveRecord/ModelSchema/ClassMethods/inheritance_column


Single-Table-Inheritance (STI) is specifically meant for the case that you want to persist models which are somewhat alike, but which persist different attributes depending on type. In other words: You shouldn't use STI unless you want to persist different things in the model.

If you just want different behavior depending on the kind of question, you could do this: depending on the kind of question, you extend the Answer instance with the behavior you like by calling extend on the necessary Ruby module:

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

and in your ./app/models/concerns you have different files containing the modules which define the behavior you want to add to the Answer class depending on kind of question:

 # 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

To beautify the code some more, you could put those answer-behaviors into a sub-directory './concerns/answer_behavior/', and wrap them in a module 'AnswerBehavior'.

2
votes

This is a great resource for single table inheritance in Rails


STI

From what you're asking, it seems there are several things you need to consider, starting with how STI's work

STI (Single Table Inheritance) is a way for your application to pull data from a single table, whilst using different "classes" to differentiate that data. The best way to describe why this is effective is to give you the low-down on Rails' object orientated nature:

enter image description here

Everything you do in Rails needs to be based around objects. These are built from your models (which take the data from your database). The draw of an STI relationship is that you'll be able to build objects around specific types of data you want.

For example:

#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

--

Usage

In reference to your question directly - the problem you have is that you're not using the STI-based models correctly. They should work just like any other model, except will draw their data from a single table (hence their name):

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

Calling the type of the model should have no bearing as long as you use the files / settings that you have in the model to make it work as required.

--

In my case, rather than rely strictly on what's in the database, I want to determine programmatically what class should be used for a given record. Is this possible? If so, how?

I think you're using STI's wrong if this is the functionality you're trying to achieve. I could be wrong - but surely you'd want to call the models up front (as demonstrated above), allowing you to manipulate their objects as you wish?


Update

Judging from what you've written, you may be better using just an Answer model, setting a specific attribute to determine which question originally sent the request:

#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

This will allow you to set the question type as you wish