Search code examples
ruby-on-railsruby-on-rails-4relationshipsjoinsingle-table-inheritance

Rails single through table for multiple models


Whenever i try to add any of the user's role (say admin) and grade in user_grade table it is giving errors other roles must exist (teacher, student, guardian). I don't know how to fix this as this is bit complex relationship structure. or can anyone suggest any better way of doing this?

class Admin < User

  # can post many posts
  has_many :posts, dependent: :destroy , foreign_key: 'user_id'

  # admin post can have many tags
  has_many :post_tags, dependent: :destroy, foreign_key: 'user_id'
  has_many :tags , through: :post_tags

  # can have many grades to see other grades posts
  has_many :user_grades, foreign_key: 'user_id'
  has_many :grades, through: :user_grades

end

class Teacher < User

  # can post many posts
  has_many :posts, dependent: :destroy , foreign_key: 'user_id'

  # teacher post can have many tags
  has_many :post_tags, dependent: :destroy, foreign_key: 'user_id'
  has_many :tags , through: :post_tags

  # can teach many grades
  has_many :user_grades, dependent: :destroy, foreign_key: 'user_id'
  has_many :grades , through: :user_grades


end

class Student < User


  # a student can only in particular grade
  has_one :user_grade , dependent: :destroy, foreign_key: 'user_id'
  has_one :grade , through: :user_grade

end

class Guardian < User

  # parents can have many children in different classes
  has_many :user_grades, dependent: :destroy, foreign_key: 'user_id'
  has_many :grades, through: :user_grades


end

class Grade < ApplicationRecord

  has_many :user_grades, dependent: :destroy
  has_many :admins, through: :user_grades

  has_many :teachers, through: :user_grades

  has_many :students , through: :user_grades

  has_one :guardian, through: :user_grade

end


class UserGrade < ApplicationRecord

  belongs_to :grade
  belongs_to :admin
  belongs_to :teacher
  belongs_to :student
  belongs_to :guardian


end

Database:

ActiveRecord::Schema.define(version: 20180410102940) do

  create_table "grades", force: :cascade do |t|
    t.integer "cls"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "post_tags", force: :cascade do |t|
    t.integer "user_id"
    t.integer "post_id"
    t.integer "tag_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "posts", force: :cascade do |t|
    t.integer "user_id", null: false
    t.string "title"
    t.text "content"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "roles", force: :cascade do |t|
    t.string "name"
    t.string "resource_type"
    t.integer "resource_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
    t.index ["name"], name: "index_roles_on_name"
    t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id"
  end

  create_table "tags", force: :cascade do |t|
    t.string "tag_name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "user_grades", force: :cascade do |t|
    t.integer "user_id"
    t.integer "grade_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer "sign_in_count", default: 0, null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string "current_sign_in_ip"
    t.string "last_sign_in_ip"
    t.string "confirmation_token"
    t.datetime "confirmed_at"
    t.datetime "confirmation_sent_at"
    t.string "unconfirmed_email"
    t.integer "failed_attempts", default: 0, null: false
    t.string "unlock_token"
    t.datetime "locked_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "type"
    t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
    t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
  end

  create_table "users_roles", id: false, force: :cascade do |t|
    t.integer "user_id"
    t.integer "role_id"
    t.index ["role_id"], name: "index_users_roles_on_role_id"
    t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id"
    t.index ["user_id"], name: "index_users_roles_on_user_id"
  end

end

I used rolify and cancancan gem here for user's role.


Solution

  • Try adding optional parameter to belongs_to relation

    class UserGrade < ApplicationRecord
       belongs_to :grade
       belongs_to :admin, optional: true
       belongs_to :teacher, optional: true
       belongs_to :student, optional: true
       belongs_to :guardian, optional: true
    end
    

    Rails 5 makes belongs_to association required by default. We can avoid validation on belongs_to relation by adding optional: true.

    OR

    You can turn off this behaviour for all models by keeping the value of belongs_to_required_by_default to false.

    # file => config/application.rb
    config.active_record.belongs_to_required_by_default = false