0
votes

I'm new to rails and I'm having some problems specifying a couple of entities and relationships in my database.

Let's assume the following entities.

Racer Entity

Where the migration file looks like this:

class CreateRacers < ActiveRecord::Migration
    def self.up
        create_table :racers, options: "ENGINE=InnoDB" do |t|
        t.string :email, limit: 60, null: false
        end
        add_index :racers, :email, unique: true
        execute "ALTER TABLE racers MODIFY id INT UNSIGNED AUTO_INCREMENT;"
    end

    def self.down
        drop_table :racers
    end
end

The model file looks like this:

class Racer < ActiveRecord::Base
    attr_accessible :email
    validates_uniqueness_of :email

    before_save { |racer| racer.email = email.downcase }

    # Email validation
    VALID_EMAIL_REGEX = /([\w+.]+)@[a-z\d\-.]+\.[a-z]+\z/i
    validates :email, presence: true, length: {maximum: 60}, format: { with: VALID_EMAIL_REGEX }

    has_many :tracks, dependent: :delete_all
end

Track Entity

The migration file looks like this:

class CreateTracks < ActiveRecord::Migration
    def self.up
        create_table :tracks, options: "ENGINE=InnoDB" do |t|
            t.column :user_id, 'integer unsigned', null: false
            t.string :description, limit: 250, null: false
            t.string :image, null: true
        end
        execute "ALTER TABLE tracks MODIFY id INT UNSIGNED AUTO_INCREMENT;"
        add_foreign_key :tracks, :racers
    end

    def self.down
        remove_foreign_key :tracks, :racers
        drop_table :tracks
    end
end

And the model file looks like this:

class Track < ActiveRecord::Base
    attr_accessible :description, :image

    validates :description, presence: true, length: {maximum: 250}

    belongs_to :racer
    validates_presence_of :racer_id
    has_many :stops, dependent: :delete_all
end

Stop Entity

The migration file looks like this:

class CreateStops < ActiveRecord::Migration
    def self.up
        create_table :stops, options: "ENGINE=InnoDB" do |t|
            t.column :track_id, 'integer unsigned', null: false
            t.column :coordinates, :point, null: false
            t.string :name, limit: 30, null: true
            t.column :sequence_order, 'integer unsigned', null: false
        end
        execute "ALTER TABLE stops MODIFY id INT UNSIGNED AUTO_INCREMENT;"
        add_foreign_key :stops, :tracks
    end

    def self.down
        remove_foreign_key :stops, :tracks
        drop_table :stops
    end
end

And the model file looks like this:

class Stop < ActiveRecord::Base
    attr_accessible :coordinates, :name, :sequence_order

    validates :name, presence: true, length: {maximum: 30}
    validates :coordinates, presence: true
    validates :spot_sequence_order, presence: true

    belongs_to :track
    validates_presence_of :track_id
    has_one :challenge, dependent: :delete_all
end

Challenge, Puzzle, Quiz, QuizOption entities (where the problem is)

Has seen above the Stop entity has_one challenge and I want that Challenge to be, among other things, a Quiz or a Puzzle. The Challenge belongs_to a Stop. So far I have the following migrations:

class CreatePuzzles < ActiveRecord::Migration
    def self.up
        create_table :puzzles, options: "ENGINE=InnoDB" do |t|
            t.string :image_path, null: false
            t.int :ver_split, null: false, default: 4
            t.int :hor_split, null: false, default: 4
        end
        execute "ALTER TABLE puzzlies MODIFY id INT UNSIGNED AUTO_INCREMENT;"
    end

    def self.down
        drop_table :quizzes
    end
end
class CreateQuizzes < ActiveRecord::Migration
    def self.up
        create_table :quizzes, options: "ENGINE=InnoDB" do |t|
            t.string :question, null: false
        end
        execute "ALTER TABLE quizzes MODIFY id INT UNSIGNED AUTO_INCREMENT;"
    end

    def self.down
        drop_table :quizzes
    end
end

And the following models

class Puzzle < ActiveRecord::Base
    attr_accessor :image_path, :ver_split, hor_split

    validates :image_path, presence: true, allow_blank: false
    validates :ver_split, allow_blank: false
    validates :hor_split, allow_blank: false

    belongs_to :stop
end
class Quiz < ActiveRecord::Base
    attr_accessor :question

    validates :question, presence: true, length: { maximum: 255 }, allow_blank: false

    belongs_to :spot
    has_many :quiz_options
end

The Quiz has several answers, where one or more are correct.

class CreateQuizOptions < ActiveRecord::Migration
    def self.up
        create_table :quiz_options do |t|
            t.column :quiz_id, 'integer unsigned', null: false
            t.string :option, null: false
            t.boolean :is_correct, null: false, default: false
        end
        add_foreign_key :quiz_options, :quizzes
        execute "ALTER TABLE quiz_options MODIFY id INT UNSIGNED AUTO_INCREMENT;"
    end

    def self.down
        remove_foreign_key :quiz_options, :quizzes
        drop_table :quiz_options
    end
end
class QuizOption < ActiveRecord::Base
    attr_accessor :option, :is_correct

    validates :option, presence: true, length: { maximum: 255 }
    validates_inclusion_of :is_correct, in: [true,false]

    belongs_to :quiz
    validates_presence_of :quiz_id
end

The Question

How should I specify my migrations,models and controllers to accomplish this goal?

I have found a couple of examples of STI and Polymorphic-Associations but I don't know which one to apply and how to apply them to this case.

First I have tried to use STI and declare all the necessary fields in the Challenge table and then the Quiz and Puzzle model inherit from Challenge model. The problem is that I don't know where to put the has_many :quiz_options.

Then I have tried to use Polymorphic-Associations explained here and here but I honestly can't understand how to adapt it to this specific case.

Thanks in advance.

Edit: I forgot to say that I'm using MySQL. I also have some gems to manage Spatial data types(rgeo, activerecord-mysql2spatial-adapter) and foreign_keys(foreigner).

1

1 Answers

0
votes

Ok, I think I solved my own problem by applying Single Table Inheritance.

The result is not pretty but it works and due to the lack of proper information on other solutions I will stick to it by now.

So let's get back to my example.

The Racer, Track and Stop entity stay as they are, no changes there.

The Challenge migration will look like this:

class CreateChallenges < ActiveRecord::Migration
    def self.up
        create_table :challenges, options: "ENGINE=InnoDB" do |t|
            t.column :stop_id, 'integer unsigned', null: false
            t.string :type
            # Common attributes to all challenges
            t.string :description, null: false
            # Puzzle attributes
            t.string :image_path
            t.int :vert_split
            t.int :hor_split
            # Quiz attributes
            t.string :question
        end
        add_foreign_key :challenges, :stops
        execute "ALTER TABLE quizzes MODIFY id INT UNSIGNED AUTO_INCREMENT;"
    end

    def self.down
        remove_foreign_key :challenges, :stops
        drop_table :quizzes
    end
end

In this table I just added all the necessary fields to specify both Quizzes and Puzzles.

Now the models will look like this:

class Challenge < ActiveRecord::Base
    validates_presence_of :spot_id, :description

    attr_accessible :description, :image_path

    # Validate description attribute
    validates :description, length: { maximum: 255 }, allow_blank: false
    # Validates image type
    VALID_IMAGE_TYPE_REGEX = /^[\/]?([\w]+[\/])*([\w]+\.(png|jpg))$/
    validates :image_path, length: { maximum: 255 }, allow_blank: true, format: { with: VALID_IMAGE_TYPE_REGEX }

    # Validates type attribute
    validates_inclusion_of :type, in: %w( Quiz Puzzle )

    # Associations
    belongs_to :stop
    has_many :quiz_options
end
class Puzzle < Challenge
    validates_presence_of :image_path
    attr_accessor :image_path, :ver_split, hor_split

    validates :image_path, allow_blank: false
    validates :ver_split, allow_blank: false
    validates :hor_split, allow_blank: false
end
class Quiz < Challenge
    validates_presence_of :question
    attr_accessor :question

    validates :question, length: { maximum: 255 }, allow_blank: false
end

Now to finish everything, the QuizOption migration and model will look like this:

class CreateQuizOptions < ActiveRecord::Migration
    def self.up
        create_table :quiz_options do |t|
            t.column :challenge_id, 'integer unsigned', null: false
            t.string :option, null: false
            t.boolean :is_correct, null: false, default: false
        end
        add_foreign_key :quiz_options, :challenges
        execute "ALTER TABLE quiz_options MODIFY id INT UNSIGNED AUTO_INCREMENT;"
    end

    def self.down
        remove_foreign_key :quiz_options, :challenges
        drop_table :quiz_options
    end
end
class QuizOption < ActiveRecord::Base
    validates_presence_of :challenge_id, :option
    attr_accessor :option, :is_correct

    validates :option, length: { maximum: 255 }
    validates_inclusion_of :is_correct, in: [true,false]

    belongs_to :challenge
end

This way everything works as I wanted.