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?
source to share
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.
source to share
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 :
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.
source to share