Rails unidirectional inheritance: defining a class programmatically

In my Rails application, I use a poll-like form that allows users to fill out responses to a dynamically changing set of questions. Currently I plan to support three different types of questions: Yes / No, Rating (on a scale of 1-5) and Text. For each of these types of questions, I need slightly different behavior for the corresponding response model (to accommodate different methods, validation requirements, etc.). I am currently trying to implement these different behaviors using unidirectional table inheritance, for example:

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 class selected is usually determined by a field in the database for each record. (By default this is a field "type"

, but you can change this by setting inheritance_column

).

In my case, however, the answer type must always match the type of the corresponding question, which makes it inconvenient and redundant to manage an additional database field like this. So instead of strictly relying on what's in the database, I want to define programmatically which class should be used for a given record. For example:.

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

      

Is it possible? If so, how?

+3


source to share


2 answers


inheritance_column

doesn't define a class, it stores the column name type

, which then determines which class to use for a given record.

So, by changing the value inheritance_column

, you can keep what you would normally store in a column type

in another column.

It won't help you dynamically determine where the class name is stored as it just points to another column, which then identifies the class by the value in that column.

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


Single Inheritance (STI) is specifically intended for the case where you want to keep models that are somewhat similar, but which retain different attributes depending on the type. In other words: you shouldn't use STI if you don't want to store different things in the model.



If you just want different behavior depending on the type of question, you can do this: depending on the type of question, you extend the Answer instance with the behavior you like by calling extend

on the desired 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 
  # 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 yours ./app/models/concerns

, you have different files containing modules that define the behavior you want to add to the answer class, depending on the question type:

 # 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 your code even better, you can put these answers in the "./concerns/answer_behavior/" subdirectory and wrap them in the "AnswerBehavior" module.

+3


source


This is a great resource for single table inheritance in Rails


STI

From what you are asking, it seems that you need to consider a few things, starting with how the STI works

STI (Single Page Inheritance) is a way for your application to extract data from a single table using different "classes" to distinguish the data. The best way to describe why this is effective is to give you low rendering level object oriented :

enter image description here

Everything you do in Rails should be object-based. They are built from your models (which take data from your database). The attraction of the STI relationship is that you will be able to create objects around the 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

      

-

Using



Regarding your question directly - the problem is that you are not using STI based models correctly. They should work just like any other model, except that they will pull data from the same table (hence their name):

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

      

The type

model call should not depend as long as you use the files / settings you have in the model to make it work as needed.

-

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

I think you are using STI incorrectly if this is the functionality you are trying to achieve. I could be wrong - but surely you would like to name the models in front (as shown above), allowing you to manipulate your objects as you wish?


Update

From what you wrote, you might be better off using just the model Answer

by 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 ask the question type of your choice.

+2


source







All Articles