Custom method or association for chained model associations - ruby-on-rails

I have three models : User, Product and Transaction.
Here are the associations :
app/models/transaction.rb
# A transaction has a `current` boolean that is true when the transaction is currently happening, and nil else.
belongs_to :seeker, class_name: "User", foreign_key: "seeker_id"
belongs_to :product
app/models/user.rb
has_many :owned_products, class_name: "Product",
foreign_key: "owner_id",
dependent: :destroy
has_many :transactions, foreign_key: "seeker_id",
dependent: :destroy
has_many :requested_products, through: :transactions, source: :product
has_many :active_transactions, -> { where current: true },
class_name: 'Transaction',
foreign_key: "seeker_id"
has_many :borrowed_products, through: :active_transactions, source: :product
app/models/product.rb
belongs_to :owner, class_name: "User",
foreign_key: "owner_id"
has_many :transactions, dependent: :destroy
has_many :seekers, through: :transactions,
source: :seeker
has_one :active_transaction, -> { where current: true },
class_name: 'Transaction'
has_one :borrower, through: :active_transaction,
source: :seeker
I want to create a method that allows me to do the following :
user.owned_products.available # returns every product owned by the user that has a transaction with current:true.
user.owned_products.lended # returns every product owned by the user that has no transaction with current.true
Is this possible ? If not, I would do an association link like user.available_products and user.lended_products but I don't know how, because I must use conditions from both models in order to make an association in a third, like this :
app/models/user.rb
has_many :available_products, -> { where borrower: nil },
class_name: "Product",
foreign_key: "owner_id"
And I get this error message :
ActionView::Template::Error:
SQLite3::SQLException: no such column: products.borrower: SELECT COUNT(*) FROM "products" WHERE "products"."owner_id" = ? AND "products"."borrower" IS NULL
Any hint ?

Create a scope
scope :available, where(:current => true).joins(:transactions)
now you can say
user.owned_products.available
This is not tested. But this will give you an idea of how to go ahead.
Here is a reference for scopes.

Related

Self-Referential Has Many Through with Custom Foreign Keys in Rails

I have a User model and a relationship table called ParentsChildren.
I'm trying to create two relationships on the User model so that User#children returns all of a users children and User#parents returns all of a users parents.
I've managed to get this working before, but I'm doing something wrong right this time, and I'm not sure what it is exactly.
class ParentsChildren < ApplicationRecord
self.table_name = 'parents_children'
belongs_to :parent_user, class_name: 'User'
belongs_to :child_user, class_name: 'User'
end
class User
has_many :parent_relationships, class_name: 'ParentsChildren', foreign_key: :parent_user_id
has_many :child_relationships, class_name: 'ParentsChildren', foreign_key: :child_user_id
has_many :children, through: :parent_relationships, class_name: 'User', source: :child_user
has_many :parents, through: :child_relationships, class_name: 'User', source: :parent_user
end
# => uninitialized constant ParentsChildren::ChildUser
Figured it out. The key was to drop 'User' as the class name for has_many :parents and has_many :users. It's inferred through the given sources.
class User
has_many :parent_relationships, foreign_key: :child_user_id,
class_name: 'ParentsChildren'
has_many :children, through: :parent_relationships,
source: :parent_user
has_many :child_relationships, foreign_key: :parent_user_id,
class_name: 'ParentsChildren'
has_many :parents, through: :child_relationships,
source: :child_user
end

why does column trades.item_id not exist?

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.

How to use :inverse_of with multiple associations?

Does anyone know what I should put in the empty fields below to make this relationship work out of the box? I am very close to making this work as all associations work flawlessly. The only issue is that I cannot save a user with fruits attached to it as, currently, there is no :inverse_of.
I need the :inverse_of pointing in the right direction so that I can save a user with fruits instead of having to save the user first and then attach fruits to it later.
Thank you!
UPDATED AFTER COMMENTS:
The User model:
class User < ApplicationRecord
has_many :bought_fruits_users, -> { bought },
class_name: 'FruitsUser', inverse_of: :buyer
has_many :bought_fruits, through: :bought_fruits_users,
class_name: 'Fruit', source: :bought_fruit
has_many :sold_fruits_users, -> { sold },
class_name: 'FruitsUser', inverse_of: :seller
has_many :sold_fruits, through: :sold_fruits_users,
class_name: 'Fruit', source: :sold_fruit
end
The middle-table model:
class FruitsUser < ApplicationRecord
belongs_to :seller, foreign_key: :user_id,
class_name: 'User', inverse_of: :sold_fruits_users
belongs_to :buyer, foreign_key: :user_id,
class_name: 'User', inverse_of: :bought_fruits_users
belongs_to :bought_fruit, foreign_key: :fruit_id,
class_name: 'Fruit', inverse_of: :buying_fruits_users
belongs_to :sold_fruit, foreign_key: :fruit_id,
class_name: 'Fruit', inverse_of: :selling_fruits_users
scope :bought, -> { where(type_of: 'bought') }
scope :sold, -> { where(type_of: 'sold') }
end
The Fruit model:
class Fruit < ApplicationRecord
has_many :buying_fruits_users, -> { bought },
class_name: 'FruitsUser', inverse_of: :bought_fruit
has_many :buying_users, through: :bought_fruits_users,
class_name: 'User', source: :buyer
has_many :selling_fruits_users, -> { sold },
class_name: 'FruitsUser', inverse_of: :sold_fruit
has_many :selling_users, through: :sold_fruits_users,
class_name: 'User', source: :seller
end
Still can't save it:
u = User.new [OK]
u.needs << Fruit.sample [OK]
u.valid? [false]
u.errors [:bought_fruits_users=>["is invalid"]]

Trying to 'alias' a polymorphic has_many relationship

I have a User model:
class User < ActiveRecord::Base
has_many :tracks, dependent: :destroy
has_many :tracked_locations, through: :tracks, source: :tracking, source_type: 'Location'
and a Track model (think of it as 'following'):
class Track < ActiveRecord::Base
belongs_to :user
belongs_to :tracking, polymorphic: true
end
The idea here is I will have many models to track / follow so I am using polymorphism. For example I have a Location model:
class Location < ActiveRecord::Base
has_many :tracks, :as => :tracking, :dependent => :destroy
has_many :users, through: :tracks
Now in the console Location.first.users works fine along with User.first.tracked_locations.
Now I will be adding another polymorphic relationship along the lines of Flagged. The user can 'flag' another model with a note etc. So if I add has_many :users, through: :flagged to the Location model for example I need to differentiate between tracking users and flagged users.
I tried:
has_many :tracking_users, through: :tracks, source: :tracking, source_type: 'User'
but I get:
NoMethodError: undefined method `evaluators_for' for #<Location:0x007ff29e5409c8>
Can I even do this or am I missing something simple here?
UPDATE
Based on the answer below I figured it out:
has_many :tracking_users, through: :tracks, class_name: "User", foreign_key: "user_id", source: :user
I'm not 100% on this, but you could try:
has_many :tracking_users, through: :tracks, class_name: "User", foreign_key: "user_id", source: :user
Or you could also just create a class method and do it by hand.
def self.tracking_users
user_ids = tracks.collect(&:user_id)
User.where(id: user_ids)
end
edit: Had a brainfart, changed the "source" up there to :user. That tells what table to actually do the lookup in with the other attribute you've provided. of course it wouldn't be in :tracks

Rails set role based relationship between belong_to and has_many for multiple primary keys

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.

Resources