I have a relationship model in which two Users can enter into a Trade for the exchange of two Items.
class User < ActiveRecord::Base
has_many :owned_items, class_name: "Item"
has_many :trades_received, class_name: "Trade", through: :owned_items, source: :trades
has_many :trades
has_many :wanted_items, class_name: "Item", through: :trades, source: :item
end
class Item < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: :user_id
has_many :trades, dependent: :destroy
has_many :trade_requesters, through: :trades
has_many :trade_recipients, through: :trades
end
class Trade < ActiveRecord::Base
belongs_to :trade_requester, class_name: "User"
belongs_to :trade_recipient, class_name: "User"
belongs_to :wanted_item, class_name: "Item", foreign_key: :wanted_item_id
belongs_to :collateral_item, class_name: "Item", foreign_key: :collateral_item_id
end
The migration on my Trades table looks like this:
create_table :trades do |t|
t.belongs_to :trade_requester
t.belongs_to :trade_recipient
t.belongs_to :wanted_item
t.belongs_to :collateral_item
end
The stack trace leads to a helper method I'm using to list all Trade requests. That line says #trades = current_user.trades_received.requested.count, and then on down to the model association on User where has_many :owned_items, class_name: "Item". From my understanding, it looks like the trades_received method, which is called through: :owned_items and source: :trades should be referencing the :wanted_item_id foreign key in the migration. But it is not. It works if I create a migration to add item_id, but a Trade needs two items, and so I've split it up into the two wanted_item and collateral_item associations. How do I set that User association up so that it references the Item that is being requested by another User? Should Items has_many :trades, the way I have it, or should Items belongs_to :trades?
Full error:
PG::UndefinedColumn: ERROR: column trades.item_id does not exist
LINE 1: ...LECT COUNT(*) FROM "trades" INNER JOIN "items" ON "trades"."...
^
: SELECT COUNT(*) FROM "trades" INNER JOIN "items" ON "trades"."item_id" = "items"."id" WHERE "items"."user_id" = $1 AND "trades"."approved" IS NULL
tldr: I need to track a bunch of complex has_many :through associations, I don't think my data model is correct, and need help understanding why. Thank you.
You're setting up two has_many :through relationship between User and Item, with Trade as the join table for both. You got some relationship confused. Here is a setup based on your migration:
class User < ActiveRecord::Base
has_many :received_trades, class_name: "Trade", foreign_key: "trade_recipient"
has_many :requested_trades, class_name: "Trade", foreign_key: "trade_requester"
has_many :collateral_items, through: :received_trades
has_many :wanted_items, through: :requested_trades
end
class Item < ActiveRecord::Base
has_many :collateral_items, class_name: "Trade", foreign_key: "collateral_item"
has_many :wanted_items, class_name: "Trade", foreign_key: "wanted_item"
has_many :trade_requesters, through: :wanted_items
has_many :trade_recipients, through: :collateral_items
end
class Trade < ActiveRecord::Base
belongs_to :trade_requester, class_name: "User"
belongs_to :trade_recipient, class_name: "User"
belongs_to :wanted_item, class_name: "Item"
belongs_to :collateral_item, class_name: "Item"
end
##migration
create_table :trades do |t|
t.belongs_to :trade_requester
t.belongs_to :trade_recipient
t.belongs_to :wanted_item
t.belongs_to :collateral_item
end
Some explanation:
Item has_many :collateral_item ## item_id in table collateral_items
Item has_many :collateral_item, class_name: "Trade", foreign_key: "collateral_item"
##collateral_item_id in trades table.
Ok, well. The problem is here:
has_many :trades, dependent: :destroy
And in your Trade model:
belongs_to :wanted_item, ...
belongs_to :collateral_item, ..
Rails cannot handle this automatically.
You need to do one of this steps (depending on what you need in your app):
If you need separate associations:
class User < ActiveRecord::Base
has_many :trades_received, class_name: "Trade", through: :owned_items, source: :wantable_trades
end
class Item < ActiveRecord::Base
has_many :wanted_trades, class_name: 'Trade', inverse_of: :wanted_item, dependent: :destroy
has_many :collateral_trades, class_name: 'Trade', inverse_of: :collateral_item, dependent: :destroy
end
If you need all trades as single association:
Well, you will have a pain in the ass :) In this case you should either select associations manually, or rethink your data model.
Related
I'm trying to create a Referral program on a Rails app and I struggle with the relationships.
My Referral model is pretty simple : godfather_id, godson_id, state
Both godfather and godson ids references an User, which can have many godsons but only one godfather.
class Referral < ApplicationRecord
belongs_to :user
belongs_to :godson, class_name: 'User'
end
The issue comes in my User model. I wan't to be able to do user.godsons to get an array of godsons Users and user.godfather to get the godfather User.
I tried a few things and I think those two where the closest to what I need to do (User model simplified for the example).
class User < ApplicationRecord
has_many :referrals
has_many :godson, -> { where(godfather_id: id) }, through: :referrals
has_one :godfather, -> { where(godson_id: id) }, through: :referrals
end
class User < ApplicationRecord
has_many :godson_relations, class_name: 'Referral', foreign_key: 'godson_id'
has_many :godsons, through: :godson_relations
has_one :godfather_relation, class_name: 'Referral', foreign_key: 'godfather_id'
has_one :godfather, through: :godfather_relations
end
I'm really unsure about how to materialize this relationship, any help will be appreciated 🙏
To make an actual self-referential assocation you would just add a column on the users table that points back to the same table:
class AddGodfatherToUsers < ActiveRecord::Migration[6.1]
def change
add_reference :users, :godfather, null: true,
foreign_key: { to_table: :users }
end end
class User
belongs_to :god_father,
class_name: 'User',
optional: true,
inverse_of: :god_children
has_many :god_children,
class_name: 'User',
foreign_key: :god_father_id
inverse_of: :god_father
end
If you must store Referalls as a separate table you were kind of on the right track but you got the foreign keys backwards:
class Referral < ApplicationRecord
# you better be explicit here or its going to get extremely confusing
belongs_to :godfather, class_name: 'User'
belongs_to :godson, class_name: 'User'
end
class User < ApplicationRecord
has_many :referrals_as_godfather,
class_name: 'Referral', foreign_key: 'godfather_id'
has_one :referral_as_godson,
class_name: 'Referral',
foreign_key: 'godfather_id'
has_many :godsons, through: :referrals_as_godfather
has_one :godfather, through: :referral_as_godson
end
It should be noted that has_one in no way guarentees that a user can have only one referral (and thus one godfather). It just adds a LIMIT 1 to the query. You would have to enforce that with a uniqueness constraint and validations.
Wondering about a relationship I have and not sure wheter this is due to cause some issues in the future.
I have the following relationships with Users and Leases.
class User < ApplicationRecord
has_one :lease, foreign_key: "tenant_id"
has_many :leases, foreign_key: "landlord_id"
end
and
class Lease < ApplicationRecord
belongs_to :tenant, class_name: "User"
belongs_to :landlord, class_name: "User"
end
and I'm trying to convert the relationship with the tenant and the lease to has_many, but I don't know how to approach this the right way.
I got this to work with
class User < ApplicationRecord
has_many :leases_as_landlord, class_name: "Lease", foreign_key: "tenant_id"
has_many :leases_as_tenant, class_name: "Lease", foreign_key: "landlord_id"
end
and
class Lease < ApplicationRecord
belongs_to :tenant, class_name: "User", inverse_of: :leases_as_tenant
belongs_to :landlord, class_name: "User", inverse_of: :leases_as_landlord
end
but I don't like calling User.leases_as_landlord and User.leases_as_tenant. What I would like to do is just call User.leases to return the leases in which the User is either the landlord or the tenant.
You can add instance method:
class User < ApplicationRecord
has_many :leases_as_landlord, class_name: "Lease", foreign_key: "tenant_id"
has_many :leases_as_tenant, class_name: "Lease", foreign_key: "landlord_id"
def leases
leases_as_landlord.or(leases_as_tenant)
end
end
It will also return ActiveRecord_AssociationRelation and you can chain other ActiveRecord method on it.
Also I would recommend to follow Rails Convention and name your has_many associations in the plural.
class User < ApplicationRecord
has_many :landlord_leases, class_name: 'Lease', foreign_key: :tenant_id
has_many :tenant_leases, class_name: 'Lease', foreign_key: :landlord_id
def leases
landlord_leases.or(tenant_leases)
end
end
I'm trying to create an app where a user chooses volunteers to complete their task. The way that volunteers are considered participants is through the selected boolean attribute placed on the TaskVolunteer join table. Unfortunately when I try to find the participants of a particular class I get the following error:
task = Task.create
task.participants
SQLite3::SQLException: no such column: users.selected
Models
class User < ActiveRecord::Base
has_many :owned_tasks, class_name: "Task", foreign_key: :owner_id
has_many :task_volunteers, as: :volunteer
has_many :volunteered_tasks, through: :task_volunteers
end
class TaskVolunteer < ActiveRecord::Base
# task_id, volunteer_id, selected (boolean)
belongs_to :task
belongs_to :volunteer, class_name: "User", foreign_key: :volunteer_id
end
class Task < ActiveRecord::Base
# owner_id
has_many :task_volunteers
has_many :volunteers, through: :task_volunteers, source: :volunteer
has_many :participants, -> {where(selected: true)}, through: :task_volunteers, source: :volunteer
belongs_to :owner, class_name: "User"
end
The error is caused by a faulty foreign_key option in TaskVolunteer.
belongs_to :volunteer, class_name: "User", foreign_key: :volunteer_id
foreign_key here refers to the column on the users table not on tasks_volunteers. You can just remove the foreign key option.
class TaskVolunteer < ActiveRecord::Base
# task_id, volunteer_id, selected (boolean)
belongs_to :task
belongs_to :volunteer, class_name: "User"
end
Added
I have to say though by altering the naming a bit and using an enum to denote status you could cut the code and cognitive complexity quite dramatically.
class User < ActiveRecord::Base
has_many :participations, foreign_key: :participant_id
has_many :owned_tasks, class_name: "Task", as: :owner
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: 'User'
has_many :participations
has_many :participants, through: :participations, source: :participant
# Dynamically generates relations such as 'selected_participants'
Participation.statuses.keys.each do |status|
has_many "#{status}_participants".to_sym,
-> { where(participations: { status: status.to_sym }) },
through: :participations,
source: :participant
end
end
class Participation < ActiveRecord::Base
belongs_to :task
belongs_to :participant, class_name: "User"
enum status: [:interested, :selected]
end
The enum macro gives you stuff like:
user.participations.selected
participation.selected?
I'm trying to create a join table between 2 models that has 3 columns. The models are called User and Dare. The join table is called DaresUsers. And what I want to include in the join table is an author_id, accepter_id, dare_id.
Each dare will only have one author but will have many accepters because more than one user can accept the same dare. Should I use a has_many through relationship and if so what would I declare in my models? My confusion here is because the User model is referenced by the join table in two respects: the author_id and accepter_id.
Thanks for any help!
Try this:
class Dare < ActiveRecord::Base
belongs_to :author, class_name: 'User'
has_many :dare_user_relations, dependent: :destroy
has_many :accepters, through: :dare_user_relations
end
class DareUserRelation < ActiveRecord::Base
belongs_to :accepter, class_name: 'User'
belongs_to :dare
end
class User < ActiveRecord::Base
has_one :dare, foreign_key: 'author_id', dependent: :destroy
has_many :dare_user_relations, dependent: :destroy
has_many :dares, through: :dare_user_relations, foreign_key: 'accepter_id'
end
or w/o a model:
class Dare < ActiveRecord::Base
belongs_to :author, class_name: 'User'
has_and_belongs_to_many :accepters, class_name: 'User', association_foreign_key: 'accepter_id'
end
class User < ActiveRecord::Base
has_one :dare, foreign_key: 'author_id', dependent: :destroy
has_and_belongs_to_many :dares, foreign_key: 'accepter_id'
end
My belong_to Item -> User relationship works; however, how do I setup the corresponding relationship in my User model (has many User -> Item)?
#item.rb
belongs_to :update_user, foreign_key: :item_updated_at_user_id, class_name: "User"
belongs_to :delete_user, foreign_key: :item_deleted_at_user_id, class_name: "User"
#user.rb
has_many :update_items, class_name: "Items", inverse_of: :update_user
has_many :delete_items, class_name: "Items", inverse_of: :delete_user
The associations should look as below:
class Item < ActiveRecord::Base
belongs_to :update_user, foreign_key: :item_updated_at_user_id, class_name: "User", inverse_of: :update_items
belongs_to :delete_user, foreign_key: :item_deleted_at_user_id, class_name: "User", inverse_of: :delete_items
end
class User < ActiveRecord::Base
has_many :update_items, foreign_key: :item_updated_at_user_id, class_name: "Item", inverse_of: :update_user
has_many :delete_items, foreign_key: :item_deleted_at_user_id, class_name: "Item", inverse_of: :delete_user
end
class_name: "Items" should be class_name: "Item"(Note: Model names are Singular)
Specify foreign key option on both side of association.
Also, Its best to specify inverse_of option on both sides of association.