Single Table Inheritance + Relationships - ruby-on-rails

I'm creating a rails 5 application (a sort of job finder that connects recruiters with applicants).
Here is a part of my model configuration:
class User < ActiveRecord::Base
has_and_belongs_to_many :tag
end
class Applicant < User
has_many :experience
has_many :match
end
class Recruiter < User
has_one :company
has_many :offer
end
class Experience < ActiveRecord::Base
belongs_to :applicant, :foreign_key => "user_id"
has_one :company
end
And these are extracts from my schema file:
create_table "users", force: :cascade do |t|
t.string "type", null: false
t.string "login", limit: 40, null: false
t.string "password", limit: 500, null: false
t.bigint "company_id"
t.index ["company_id"], name: "index_users_on_company_id"
t.index ["login"], name: "index_users_on_login", unique: true
end
create_table "experiences", force: :cascade do |t|
t.string "job_name", limit: 100, null: false
t.bigint "company_id"
t.bigint "user_id", null: false
t.text "description"
t.index ["company_id"], name: "index_experiences_on_company_id"
t.index ["job_name"], name: "index_experiences_on_job_name"
t.index ["user_id"], name: "index_experiences_on_user_id"
end
add_foreign_key "users", "companies"
add_foreign_key "experiences", "companies"
add_foreign_key "experiences", "users"
An Experience is attached to the model Applicant through the table user (which contain a type field for the STI), this is why I specified "foreign_key => 'user_id'" in Experience model.
My problem is when I try to access at the first experience of an applicant, I get this error:
PG::UndefinedColumn: ERROR: column experiences.applicant_id does not exist LINE 1: SELECT "experiences".* FROM "experiences" WHERE "experiences...
I hope you can help me.
Thanks!

As stated in the docs:
By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix _id added.
Try doing:
class Applicant < User
has_many :experiences, foreign_key: :user_id
has_many :matches
end
Note that it is conventional to use the plural with the has_many association.
BTW, it's no obvious to me why you're using STI, but I'm sure there are good reasons.

Related

Rails: Self-referential associations have my head spinning

I'm currently working on a small school project that utilizes Ruby on Rails and I'm having some trouble getting my self-referential associations working correctly.
Context
The intended functionality of my web app is for users to post houses/apartments for other users to search through and rent. Since I'm having issues with a specific association, I'm working with a completely stripped down version that only has two models, User and Lease.
What I'm Trying to Accomplish
Ideally, when a person first registers on the site, a User object is created to hold their information such as email and password. A User can then either post a listing or search through listings.
Once a post has been created and another user decides to rent the posted house, a Lease object is created, which holds the ID of the posting User as well as the ID of the renting user, aliased as "landlord_id" and "tenant_id" respectively.
A User should now be identified as either a User, Landlord or a Tenant (or both Landlord and Tenant) based on whether there are any Lease objects with their ID as either a Landlord or a Tenant. This identification will be used to determine whether the User can access other areas of the site.
userFoo.leases
This should give me a list of all Lease objects with which the User's ID is associated, regardless of whether it's as a Landlord or Tenant.
userFoo.tenants
This should give me a list of any User object whose ID is associated with the ID of userFoo as a Tenant through Lease, and the inverse if I ask for landlords.
The Code
User Class
class User < ApplicationRecord
has_many :tenants, class_name: "Lease", foreign_key: "landlord_id"
has_many :landlords, class_name: "Lease", foreign_key: "tenant_id"
end
Lease Class
class Lease < ApplicationRecord
belongs_to :landlord, class_name: "User"
belongs_to :tenant, class_name: "User"
end
Users Table Migration
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest
t.timestamps
end
end
end
Leases Table Migration
class CreateLeases < ActiveRecord::Migration[6.0]
def change
create_table :leases do |t|
t.references :landlord, null: false, foreign_key: {to_table: :users}
t.references :tenant, null: false, foreign_key: {to_table: :users}
t.timestamps
end
end
end
Database Schema
ActiveRecord::Schema.define(version: 2020_10_18_005954) do
create_table "leases", force: :cascade do |t|
t.integer "landlord_id", null: false
t.integer "tenant_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["landlord_id"], name: "index_leases_on_landlord_id"
t.index ["tenant_id"], name: "index_leases_on_tenant_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.string "password_digest"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "leases", "users", column: "landlord_id"
add_foreign_key "leases", "users", column: "tenant_id"
end
What's Wrong?
userFoo.leases
Normally a User would have_many leases by having their ID associated with a lease as "user_id." However, since I'm using "tenant_id" and "landlord_id", this command fails because it can't find "user_id" in the Leases table.
userFoo.tenants
This command gives me a list of all Lease objects where userFoo's ID is associated as "landlord_id" instead of all User objects associated with userFoo's ID as tenants. To retrieve a tenant as is, I have to use the command:
userFoo.tenants.first.tenant
Conclusion
I am having a bit of a hard time understanding these deeper, more complex associations, and I've spent some time trying to find a detailed reference on has_many that covers all the arguments, but all I can really find are small blog posts that reference the "Employees" and "Managers" example on guides.rubyonrails.com . I think one problem is that I'm not sure I'm correctly reflecting my model associations in my table schema.
I'm more than happy to teach myself if someone can point me in the right direction. I'm also open to alternative solutions but only if I can't get the functionality I want out of this setup, because my instructor specifically asked me to try it this way
Thanks in advance for any help! It's much appreciated.
as per your requirement you can try like this:
# app/models/user.rb
class User < ApplicationRecord
has_many :owned_properties, class_name: "Property", foreign_key: "landlord_id"
has_many :rented_properties, class_name: "Property", foreign_key: "tenant_id"
end
Here I have declared two associations with same table but different foreign keys.
# app/models/property.rb
class Property < ApplicationRecord
belongs_to :landlord, class_name: "User"
belongs_to :tenant, class_name: "User"
end
Here I have taken one table by using this user can post one property where landlord is the owner of a house and later you can add tenant who is taking rent to one property.
# db/migrations/20201018054951_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false, index: true
t.string :password_digest, null: false
t.timestamps
end
end
end
Above is your users table migration.
# db/migrations/20201018055351_create_properties.rb
class CreateProperties < ActiveRecord::Migration[6.0]
def change
create_table :properties do |t|
t.references :landlord, foreign_key: {to_table: :users}, null: false
t.references :tenant, foreign_key: {to_table: :users}
t.timestamps
end
end
end
Above is your properties table migration.
# db/schema.rb
ActiveRecord::Schema.define(version: 2020_10_18_055351) do
create_table "properties", force: :cascade do |t|
t.bigint "landlord_id", null: false
t.bigint "tenant_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["landlord_id"], name: "index_properties_on_landlord_id"
t.index ["tenant_id"], name: "index_properties_on_tenant_id"
end
create_table "users", force: :cascade do |t|
t.string "name", null: false
t.string "email", null: false
t.string "password_digest", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email"
end
add_foreign_key "properties", "users", column: "landlord_id"
add_foreign_key "properties", "users", column: "tenant_id"
end
If you want to fetch all the owned properties of a user, use user.owned_properties.
If you want to fetch all rented properties of a user, use user.rented_properties.
^^ Here both the cases you'll get objects of Property class.
If you want to get landlord of a property, use property.landlord.
If you want to get tenant of a property, use property.tenant.
^^ Here both the cases you'll get objects of User class.
If you want you can add other attributes like: name, price, etc to properties table.
I think, this will help you. Thanks :) Happy Coding :)

Rails 5 association with different foreign key

I think I have missed something when creating an association with a foreign_key in Rails.
I have 2 models: Company and Employee. Company has_many employees and Employee belongs_to a company. Company has an attribute called company_code and I should be able to figure out which company the employee works for using the company_code instead of company_id.
At first, I created the models:
rails g model Company company_code:integer:index name
rails g model Employee joined:date:index salary:integer
Then, I generated a migration to add the company_code column to the employees table.
class AddReferenceToEmployee < ActiveRecord::Migration[5.1]
def change
add_column :employees, :company_code, :integer, index: true
add_foreign_key :employees, :companies, column: :company_code
end
end
And, finally I added the foreign key at the model level.
class Company < ApplicationRecord
has_many :employees, foreign_key: :company_code
end
class Employee < ApplicationRecord
belongs_to :company, foreign_key: :company_code
end
However, I'm still not able to create proper association.
company = Company.create(name: 'test', company_code: 123)
company.employees.create(joined: Date.today, salary: 1000)
It creates employee record with company_code = 1 instead of 123.
When I try to create a new instance of employee
company.employees.new
It will generate
#<Employee id: nil, joined: nil, salary: nil, created_at: nil, updated_at: nil, company_code: 1>
What am I missing? Is this the right way to do it?
Bellow is my schema.rb
ActiveRecord::Schema.define(version: 20180828052633) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "companies", force: :cascade do |t|
t.integer "company_code"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["company_code"], name: "index_company_on_company_code"
end
create_table "employees", force: :cascade do |t|
t.date "joined"
t.integer "salary"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "company_code"
end
add_foreign_key "employees", "companies", column: "company_code"
end
class Company < ApplicationRecord
has_many :employees, primary_key: :company_code, foreign_key: :company_code
end
class Employee < ApplicationRecord
belongs_to :company, foreign_key: :company_code, primary_key: :company_code
end

ActiveAdmin No method error

I added a couple of foreign keys to my models and to my tables and it has since broken my use of Active Admin. I'm wondering if anyone knows a work around or a fix to this issue.
schmea.rb
create_table "students", primary_key: "student_id", id: :string, force:
:cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "last_name"
t.string "first_name"
t.string "home_address"
t.string "home_city"
t.string "home_state"
t.string "home_zip"
t.string "school_year_address"
t.string "school_year_city"
t.string "school_year_zip"
t.string "room_number"
t.string "home_phone"
t.string "cell_phone"
t.boolean "new_student"
t.boolean "returning_student"
t.string "athletic_team"
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "fk_rails_148c9e88f4"
end
add_foreign_key "emergency_contacts", "students", primary_key: "student_id"
add_foreign_key "students", "users"
add_foreign_key "vehicles", "students", primary_key: "student_id"
student.rb Students Model
class Student < ApplicationRecord
self.primary_key = :student_id
belongs_to :user
has_one :emergency_contact
has_one :vehicle
end
I'm getting the error, has anyone found a fix for this?
undefined method `emergency_contact_id_eq' for Ransack::Search<class: Student, base: Grouping <combinator: and>>:Ransack::Search
You have defined the assocation wrong.
class Student < ApplicationRecord
self.primary_key = :student_id
belongs_to :user
belongs_to :emergency_contact, class_name: 'User'
has_one :vehicle
end
belongs_to places the foreign key on this table and is exactly what you want. When joining you want to have the id on this table instead of having to look for records where student_id matches this record.
You also need to make sure to add a foreign key column and the correct foreign key constraint:
class AddEmergencyContactIdToStudents < ActiveRecord::Migration[5.0]
def change
add_reference :students, :emergency_contact, foreign_key: false
add_foreign_key :students, :users, column: :emergency_contact_id,
end
end
I would also strongly advise against using non standard primary keys. Prefixing the PK with student_ gives you nothing but headaches and will confuse other developers.

Rails has_and_belongs_to_many only works one way

I am trying to create a many to many relationship between my groups table and my keywords table.
When I am in my controller I cant do Keyword.groups or Group.keywords as I get a no method error. I have checked Group.methods and I only have these related methods
"before_add_for_groups_keywords",
"before_add_for_groups_keywords?",
"before_add_for_groups_keywords=",
"after_add_for_groups_keywords",
"after_add_for_groups_keywords?",
"after_add_for_groups_keywords=",
"before_remove_for_groups_keywords",
"before_remove_for_groups_keywords?",
"before_remove_for_groups_keywords=",
"after_remove_for_groups_keywords",
"after_remove_for_groups_keywords?",
"after_remove_for_groups_keywords=",
"before_add_for_keywords",
"before_add_for_keywords?",
"before_add_for_keywords=",
"after_add_for_keywords",
"after_add_for_keywords?",
"after_add_for_keywords=",
"before_remove_for_keywords",
"before_remove_for_keywords?",
"before_remove_for_keywords=",
"after_remove_for_keywords",
"after_remove_for_keywords?",
"after_remove_for_keywords=",
Where as Keyword.methods gives me these
"before_add_for_keywords_groups",
"before_add_for_keywords_groups?",
"before_add_for_keywords_groups=",
"after_add_for_keywords_groups",
"after_add_for_keywords_groups?",
"after_add_for_keywords_groups=",
"before_remove_for_keywords_groups",
"before_remove_for_keywords_groups?",
"before_remove_for_keywords_groups=",
"after_remove_for_keywords_groups",
"after_remove_for_keywords_groups?",
"after_remove_for_keywords_groups=",
"before_add_for_groups",
"before_add_for_groups?",
"before_add_for_groups=",
"after_add_for_groups",
"after_add_for_groups?",
"after_add_for_groups=",
"before_remove_for_groups",
"before_remove_for_groups?",
"before_remove_for_groups=",
"after_remove_for_groups",
"after_remove_for_groups?",
"after_remove_for_groups=",
My models
has_and_belongs_to_many :keywords
has_and_belongs_to_many :groups
My db schema is the following
create_table "groups", force: :cascade do |t|
t.integer "member_id"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "groups", ["member_id"], name: "index_groups_on_member_id", using: :btree
create_table "groups_keywords", id: false, force: :cascade do |t|
t.integer "group_id"
t.integer "keyword_id"
end
add_index "groups_keywords", ["group_id", "keyword_id"], name: "index_groups_keywords_on_group_id_and_keyword_id", unique: true, using: :btree
create_table "keywords", force: :cascade do |t|
t.string "keyword"
t.string "keyword_hash"
t.datetime "checked_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Associations are defined for instances.
Keyword.groups indeed does not exist (as a class method).
However, Keyword#groups, as in Keyword.first.groups, does work. The reason for that is that Keyword.first returns an instance of Keyword class, which Keyword class itself is not.
A better way to implement a many to many relationship is with the has_many :through idiom:
class Keyword
has_many :keyword_groups
has_many :groups, :through => :keyword_groups
class Group
has_many :keyword_groups
has_many :keywords, :through => :keyword_groups
class KeywordGroup
belongs_to :keyword
belongs_to :group
This will give you flexibility in the future if you need to extend your join model.
When you do Keyword.group(1), it doesn't do what you think it is doing. It just finds all the keywords and groups them by 1.
SELECT * "keywords".* FROM "keywords" GROUP BY 1
Indeed, you can't actually call these kind of method on Class level, associations are accessed through instance level. And as you have defined them has_and_belongs_to_many, you will have to be plural about them, not single.
keyword = Keyword.first
keyword.groups # Not 'group', and on instance level

Could not find the source association(s) :character in model Relation

I have two models called User and Relations. Relation stores the relationships between users, and there are different types of relationships. At the moment, I am trying to implement two types: friends and friend requests.
Here are my class associations:
class Relation < ActiveRecord::Base
belongs_to :owner, class_name: "User"
validates :owner, presence: true
validates :character, presence: true
end
class User < ActiveRecord::Base
has_many :characters, dependent: :destroy
has_many :relations, foreign_key: "owner", dependent: :destroy
has_many :friends, -> { where reltype: "friend" }, through: :relations, source: "character"
has_many :reverse_relations, foreign_key: "character", class_name: "Relation", dependent: :destroy
has_many :friend_requests, -> { where reltype: "freq" }, through: :reverse_relations, source: :owner
.
.
.
end
In IRB, I am trying to use the User.friends array this is supposed to generate, and this error is thrown:
Loading development environment (Rails 4.0.0)
irb(main):001:0> user = User.find(1)
User Load (12.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
=> #<User id: 1, name: "melv", email: "melv#mx.org", password_digest: "$2a$10$g3FjyVP9ya/L.j1iWAzYH.YFOjYOyxUGp3KIt6ajic
Jf...", verified: nil, reg_ip: nil, last_ip: nil, character_limit: 3, characters: nil, created_at: "2013-10-31 19:10:36"
, updated_at: "2013-11-02 11:05:14", remember_token: "4e883d6ec84b5c142882cc084c14bc101a06350f", playchar: 0>
irb(main):002:0> user.friends
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) :character in model
Relation. Try 'has_many :friends, :through => :relations, :source => <name>'. Is it one of :owner?
<...stack trace...>
irb(main):003:0>
Am I doing something wrong?
My schema, as requested:
ActiveRecord::Schema.define(version: 20131102102750) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "characters", force: true do |t|
t.integer "accountid"
t.string "username"
t.integer "roundles"
t.integer "gems"
t.integer "rank"
t.integer "tier"
t.datetime "tiertime"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "permission_ranks", force: true do |t|
t.string "label"
t.integer "badge"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "relations", force: true do |t|
t.integer "owner"
t.integer "character"
t.string "reltype"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "relations", ["character"], name: "index_relations_on_character", using: :btree
add_index "relations", ["owner", "character"], name: "index_relations_on_owner_and_character", unique: true, using: :btree
add_index "relations", ["owner"], name: "index_relations_on_owner", using: :btree
create_table "tickets", force: true do |t|
t.string "title"
t.text "desc"
t.integer "sender"
t.integer "assigned"
t.text "addinfo"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", force: true do |t|
t.string "name"
t.string "email"
t.string "password_digest"
t.boolean "verified"
t.string "reg_ip"
t.string "last_ip"
t.integer "character_limit"
t.integer "characters"
t.datetime "created_at"
t.datetime "updated_at"
t.string "remember_token"
t.integer "playchar"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["name"], name: "index_users_on_name", unique: true, using: :btree
add_index "users", ["remember_token"], name: "index_users_on_remember_token", using: :btree
end
OK, I think the problem is with naming. It's not a good idea naming association and column with same name. If it's possible start with renaming columns on relation:
owner => owner_id
character => character_id
Alternatively You can rename associations:
owner => user_owner
character => user_character
Another thing that I see is Your separate characters table. You seem like want to reference characters table from Your Relation model (correct me if I am wrong), but never do it.
belongs_to :user_character, foreign_key: "character"
Now to Your main question. If I understood Your correctly You want to get list of characters, using friends association defined in User. In this case the line should look like this:
has_many :friends, -> { where reltype: "friend" }, through: :relations, source: "user_character", class_name: "Character"
UPDATED
Author explained that he really wants to reference User model through friends association.
# relation.rb
belongs_to :user_character, foreign_key: "character", class_name: "User"
# user.rb
has_many :friends, -> { where reltype: "friend" }, through: :relations, source: "user_character"
UPDATE2
There was a problem with association condition, in particular case You need to specify table explicitly:
has_many :friends, -> { where("relations.reltype" => "friend") }, through: :relations, source: "user_character"

Resources