Related
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 :)
Issue is I can't find why reference column id can't be inserted when create new record.
I have 3 table shop_plan, shop and app
Below is tables schema:
create_table "shop_plans", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "shops", force: :cascade do |t|
t.string "url"
t.bigint "plan_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["plan_id"], name: "index_shops_on_plan_id"
end
create_table "apps", force: :cascade do |t|
t.bigint "shop_id"
t.binint "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["app_id"], name: "index_apps_on_shop_id"
end
add_foreign_key "shops", "shop_plans", column: "plan_id"
add_foreign_key "apps", "shops"
And below is Model
class ShopPlan < ApplicationRecord
has_many :shop
end
class Shop < ApplicationRecord
belongs_to :shop_plan, class_name: 'ShopPlan', foreign_key: :plan_id
has_many :app
end
class App < ApplicationRecord
belongs_to :shop, class_name: 'Shop', foreign_key: :shop_id
end
There will be 1 default record added in seed.db for table shop_plan
ShopPlan.create(name: 'Basic')
ShopPlan and Shop are linked by plan_id column in Shop
Shop and App are linked by shop_id column in App
I pre-insert some value when user access index:
#basic_plan
#basicPlan = ShopPlan.where(name: "Basic").first
# if new shop registered, add to database
unless Shop.where(url: #shop_session.url).any?
shop = Shop.new
shop.url = #shop_session.url
shop.plan_id = #basicPlan.id
shop.save
end
This insert works well, however, when i run 2nd insert:
#shop= Shop.where(url: #shop_session.url).first
unless App.where(shop_id: #shop.id).any?
app = App.new
app.shop_id = #shop.id,
app.amount = 10
app.save
end
error occurs as somehow app.shop_id will not add in my #shop.id and it will return will error: {"shop":["must exist"]}
I even try hard-code app.shop_id =1 but it does not help and when I add in optional: true to app.db model, it will insert null
Appreciate if anyone can help point out why I get this error
EDIT: #arieljuod to be clear
1) I have to specific exact column class due to between Shop And Shop_Plan, i'm using a manual plan_id instead of using default shopplans_id columns.
2) I have update 1 column inside App and all that unless is just to do checking when debugging.
First of all, like #David pointed out, your associations names are not right. You have to set has_many :shops and has_many :apps so activerecord knows how to find the correct classes.
Second, you don't have to specify the class_name option if the class can be infered from the association name, so it can be belongs_to :shop and belongs_to :shop_plan, foreign_key: :plan_id. It works just fine with your setup, it's just a suggestion to remove unnecesary code.
Now, for your relationships, I think you shouldn't do those first any? new block manually, rails can handle those for you.
you could do something like
#basicPlan = ShopPlan.find_by(name: "Basic")
#this gives you the first record or creates a new one
#shop = #basicPlan.shops.where(url: #shop_session.url).first_or_create
#this will return the "app" of the shop if it already exists, and, if nil, it will create a new one
#app = #shop.app or #shop.create_app
I have found out the silly reason why my code does not work.
It's not because as_many :shops and has_many :app and also not because my code when creating the record.
It just due to silly comma ',' when creating new record in App at app.shop_id = #shop.id,, as I was keep switching between Ruby and JavaScript. Thank you #arieljuod and #David for your effort
Perhaps I am approaching this all wrong, I have the following models setup:
The User model has many Questions, a Question belongs_to a User
This means I can easily do: u = User.last.questions and see all of the questions associated with the user last added.
Following this, I have a Payment model that belongs_to a Question. A Question has_one payment meaning I can easily do u = User.last.questions.last.payment, getting the payment details from the question last added by the last added user (daft example perhaps!).
Now the bit that is confusing me
I have a model called PaymentOption which is used to populate a drop down list, for different prices, the PaymentOption belongs_to a Payment and a Payment has_one PaymentOption
This is all working correctly, but the only way for me to find out the details of the payment option, such as the amount, would be to do something like this:
payment_amount = PaymentOption.find(User.last.questions.last.payment.payment_option_id).amount
Ideally I would do something like this:
amount = User.last.questions.last.payment.payment_option.amount
By doing it in the way I have, it is as good as doing two seperate queries, one to get the payment option ID and another to find a detail relating to that particular payment option. Surely there is a better way to approach this?
Thanks in advance
EDIT 1
To clarify my answer, I have included the schema for Payments and PaymentOptions
create_table "payment_options", force: :cascade do |t|
t.integer "amount"
t.text "description"
t.string "title"
t.boolean "user_accessible", default: true
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "payments", force: :cascade do |t|
t.string "email"
t.string "token"
t.integer "question_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "payment_option_id"
end
add_index "payments", ["payment_option_id"], name: "index_payments_on_payment_option_id"
add_index "payments", ["question_id"], name: "index_payments_on_question_id"
I think here is you problem:
I have a model called PaymentOption which is used to populate a drop
down list, for different prices, the PaymentOption belongs_to a
Payment and a Payment has_one PaymentOption
You said you've setup Payment has_one PaymentOption and PaymentOption belongs_to Payment. But in the schema described in the end Payment has a payment_option_id.
In this case, Payment belongs_to PaymenteOption and not the opposite. See this example in the Rails Guide. Account has supplier_id so Supplier has one Account and Account belongs_to Supplier.
Your models should be like this:
class Payment < ActiveRecord::Base
belongs_to :payment_option
end
class PaymentOption < ActiveRecord::Base
has_one :payment # Or even has_many :payments
end
schema.rb:
ActiveRecord::Schema.define(version: 20150324012404) do
create_table "groups", force: :cascade do |t|
t.string "title"
t.integer "teacher_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "groups_students", id: false, force: :cascade do |t|
t.integer "group_id"
t.integer "student_id"
end
add_index "groups_students", ["group_id"], name: "index_groups_students_on_group_id"
add_index "groups_students", ["student_id"], name: "index_groups_students_on_student_id"
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.datetime "created_at"
t.datetime "updated_at"
t.boolean "admin", default: false
t.string "type"
t.integer "group_id"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
group.rb:
class Group < ActiveRecord::Base
belongs_to :teacher
has_and_belongs_to_many :students
end
student.rb:
class Student < User
has_and_belongs_to_many :groups
end
I could have set a simple belongs_to and a has_many relationship between the student and group models, but I want students to be able to belong to more than one group, so I set up a HABTM association and corresponding join table.
I think I that right?
The question is, how do I, in the console, set a Student to belong to more than one group?
I have setup a User with 'type: Student' and I have two Groups. So...
In the console I do:
student = Student.first
Then, I want to set 'student' to belong to both Groups, but I don't know how to do this.
To set it to belong to one group I can do:
student.update_attributes(group_id: 1)
But how do make it belong to both groups? It would have two group_id's wouldn't it? I don't know how to set this.
If you need to see any of the other files, it's the 'handcode' branch here:
https://github.com/Yorkshireman/sebcoles/tree/handcode
The answers others have already provided are correct. But if you're working with id's you can also do something like this
student = Student.first
student.group_ids = 1,2,3,4
You don't need to set group_id for the User, the association is handled by the join table and the HABTM statement. You should remove group_id from the users table in the schema.
From memory you should be able to do something like this:
student = Student.first
groups = Group.all
student.groups << groups
student.save
See the active record guide on HABTM associations - specfically 4.4.1.3
Instead of habtm, just use the normal through and your life becomes easy. Make sure an id is generated for the association table (remove id:false)
create_table "group_students", force: :cascade do |t|
t.integer :group_id, nil:false
t.integer :student_id, nil:false
end
class Group < ActiveRecord::Base
has_many :group_students, dependent: :destroy, inverse_of :group
has_many :students, through :group_students
end
class Student < ActiveRecord::Base
has_many :group_students, dependent: :destroy, inverse_of :student
has_many :groups, through: :group_students
end
class GroupStudent < ActiveRecord::Base
belongs_to :group,
belongs_to :student
validates_presence_of :group, :student
end
Group.last.students << Student.last
or..
Student.last.groups << Group.last
Student.last.groups = [Group.find(1), Group.find(2)]
etc....
Ok, so it took me 3 days of all kinds of pain to work this out.
There was nothing wrong with my original code, except that I needed to remove the group_id from the user table.
roo's answer was correct, except that using 'group' as a variable name in the console confused Rails. This had led me to believe there was something wrong with my code, but there wasn't. You learn the hard way.
So, Students can be pushed into Groups like this:
To push a student into one group:
student = Student.first
OR
student = Student.find(1)
(or whatever number the id is)
group1 = Group.first
OR
group1 = Group.find(1)
student.groups << group1
To push into multiple groups (which was the original goal of this whole debacle:
student = Student.first
OR
student = Student.find(1)
allclasses = Group.all
student.groups << allclasses
To view your handywork:
student.groups
Works beautifully. The only problem I can see with my code is that it's possible to push the same student into a group twice, resulting in two duplicates of that student in one group. If anyone knows how to prevent this happening, I'm all ears.
I have a scenario where I need to have multiple has_one relationships and be able to access them all at once through a relationship.
Currently I have an STI table, we'll call it Animals.
There are subclasses: Lion, Meerkat, Boar
And they all need to relate to multiple Group objects.
So a Lion can be in multiple groups, but a Group may only have one Lion related.
I want to limit and refer to specifically one Lion, Meerkat and Boar.
I've tried a simple bridge table, has_many :animals, through: :associated_animals. But there doesn't seem to be an easy way to pull out group.lion. I've also tried doing multiple has_one :lion/:meerkat/:boar relationships, but there was no obvious way to do group.animals.
Is there an STI way to get this functionality through default rails associations or do I have to do a combination of both the has_one :lion and has_many :animals?
Have you tried it like this:
app/models/animal.rb
class Animal < ActiveRecord::Base
belongs_to :group
end
class Lion < Animal; end
class Meerkat < Animal; end
Of course you can split out the Lion and Meerkat classes to separate ruby files
app/models/group.rb
class Group < ActiveRecord::Base
has_many :animals
has_one :lion
has_one :meerkat
end
after you run your migrations your schema should look something like this:
db/schema.rb
ActiveRecord::Schema.define(version: 20150309234835) do
create_table "animals", force: :cascade do |t|
t.string "name"
t.string "type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "group_id"
end
create_table "groups", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
Now you should be able to write queries using group:
g = Group.find(1)
g.animals
g.lion
Lion.first.group
Animal.last.group