Rails 4 - chaining model associations to access associated methods - ruby-on-rails

I have models for User, Profile and Organisation Request. The associations are:
User
has_one :profile, dependent: :destroy
has_one :organisation_request, through: :profile
accepts_nested_attributes_for :organisation_request
Profile
belongs_to :user
belongs_to :organisation
Organisation Request
belongs_to :profile
# belongs_to :user#, through: :profile
belongs_to :organisation
In my user model, I have a method called full_name (which I use to format the presentation of a user's name.
I'm trying to access that full_name method in my organisation_requests model.
I'm trying to do that by writing the following method in my organisation requests model:
def related_user_name
self.profile.user.full_name
end
When I try to use this in my organisation requests index, like this:
<%= link_to orgReq.related_user_name, organisation_request.profile_path(organisation_request.profile.id) %>
I get an error that says:
undefined method `user' for nil:NilClass
When I try to use this idea in the rails console, with:
o = OrganisationRequest.last
OrganisationRequest Load (0.4ms) SELECT "organisation_requests".* FROM "organisation_requests" ORDER BY "organisation_requests"."id" DESC LIMIT 1
=> #<OrganisationRequest id: 2, profile_id: 1, organisation_id: 1, created_at: "2016-08-01 22:48:52", updated_at: "2016-08-01 22:48:52">
2.3.0p0 :016 > o.profile.user.formal_name
Profile Load (0.5ms) SELECT "profiles".* FROM "profiles" WHERE "profiles"."id" = $1 LIMIT 1 [["id", 1]]
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
=> " John Test"
The concept seems to work in the console?
Can anyone see where I've gone wrong?

Don't chain methods, it's a bad practice, it violates the Law of Demeter. The best choice is to use the delegate. So instead of:
def related_user_name
self.profile.user.full_name
end
You can have:
class OrganisationRequest
belongs_to :profile
has_one :user, through: :profile
delegate :full_name, to: :user, allow_nil: true, prefix: true
end
Then you can just call organisation_request.user_full_name and it will go through profile > user and call full_name (and you won't get undefined since the allow_nil: true will "cover" it)
More info about delegate here.

have you checked all of your organisation requests have profile? may be this is not best practice, try to use profile.try(:user).try(:full_name)

Related

How would a 'commentable' polymorphic association work on a 'user' model itself?

I'm learning rails and trying out polymorphic association. I have listed below a couple of simple models for illustration. Model associations seems to works fine as expected. But what if a user (commenter) would like to leave a comment for a another user? I can't seems to get it to work with these configuration. How do I go about doing so?
class User < ApplicationRecord
# username, email, password_digest
has_many :comments, as: :commentable, dependent: :destroy
end
class Project < ApplicationRecord
# title, completed, user_id
has_many :comments, as: :commentable, dependent: :destroy
end
class Comment < ApplicationRecord
# commenter_id, commentable_type, commentable_id, body
belongs_to :commentable, polymorphic: true
end
in console... setup
user1 = User.frist
user2 = User.last
project = Project.first
pro_comment = project.comments.new(body: 'some text')
pro_comment.commenter_id = user1.id
pro_comment.save
user_comment = user2.comments.new(body: 'some text')
user_comment.commenter_id = user1.id
user_comment.save
expected and actual results
Comment.all => successfully lists pro_comment & user_comment
But...
Comment.find_by(commenter_id: 1) => only listed the pro_comment
(what am I doing wrong?)
Also..
user1.comments => returned an empty object... was expecting 2 objects,
as you can see below it's not referencing 'commenter_id' ....
result...
comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE
"comments"."commentable_id" = $1 AND "comments"."commentable_type" = $2
LIMIT $3 [["commentable_id", 1], ["commentable_type", "User"],
["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
I also tried ...
user1.comments.where(commenter_id: 1) >> which returned...
comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE
"comments"."commentable_id" = $1 AND "comments"."commentable_type" = $2
AND "comments"."commenter_id" = $3 LIMIT $4 [["commentable_id", 1],
["commentable_type", "User"], ["commenter_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::AssociationRelation []>
Not sure what I'm doing wrong. Could someone please point me in the right direction.
I thank you for your time.
find_by returns only one record, try Comment.where(commenter_id: 1) instead.
For user1.comments being empty, you are mixing the relationships. You should have 2 relationships: comment belongs to a commentable object (a project or a user) and comments also belongs to a commenter (the user you set as commenter_id).
It makes sense for user1.comments to be empty since the user is the commenter on both comments, it's not the commentable. user2.comments shouldn't be empty, same for project.comments
Try something like this:
class User < ApplicationRecord
has_many :comments_done, class_name: 'Comment', inverse_of: :commenter
has_many :comments, as: :commentable, dependent: :destroy
end
class Comment < ApplicationRecord
belongs_to :commenter, class_name: 'User'
belongs_to :commentable, polymorphic: true
end
(check the guide, I may be missing some config option https://guides.rubyonrails.org/v5.2/association_basics.html#has-many-association-reference)
Now you can use user1.comments_done and user1.comments for comments done by the user and done at the user.

Rails pre-loading association with multiple foreign keys

Let's say I have the following two models, joined separately by the following two joins models:
class Game
has_many :game_plays
has_many :game_views
end
class Person
has_many :game_plays
has_many :game_views
end
# Games that a Person has played
class GamePlay
belongs_to :game
belongs_to :person
end
# Games that a Person has viewed
class GameView
belongs_to :game
belongs_to :person
end
Given a specific GamePlay, I want to get the GameView for the same Person-Game combo, e.g:
game_play = GamePlay.first
game_view = GameView.find_by(game_id: game_play.game_id, person_id: game_play.person_id)
I also need to eager load that association.
I'd love to create an association between GamePlay and GameView, but nothing I've tried has worked so far.
Attempt 1
class GamePlay
belongs_to :game
belongs_to :person
has_one :game_view, -> (gp) { where(game_id: gp.game_id) }, foreign_key: :person_id, primary_key: :person_id
end
This works, but I can't include this:
GamePlay.includes(:game_view).first
# => ArgumentError: The association scope 'game_view' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
Attempt 2
class GamePlay
belongs_to :game
belongs_to :person
def game_view
GameView.find_by(game_id: game_id, person_id: person_id)
end
end
This obviously works, but I can't include this because it isn't defined as an association.
Any thoughts? Thanks!
Rails 5.0.0postgres 9.6.2
How about:
class GamePlay < ApplicationRecord
belongs_to :game
belongs_to :person
has_one :game_view, through: :person, source: :game_views
end
irb(main):002:0> GamePlay.includes(:game_view).find(2)
GamePlay Load (0.2ms) SELECT "game_plays".* FROM "game_plays" WHERE "game_plays"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Person Load (0.2ms) SELECT "people".* FROM "people" WHERE "people"."id" = 1
GameView Load (0.2ms) SELECT "game_views".* FROM "game_views" WHERE "game_views"."person_id" = 1
=> #<GamePlay id: 2, game_id: 1, person_id: 1>
irb(main):008:0> GamePlay.find(2).game_view
GamePlay Load (0.1ms) SELECT "game_plays".* FROM "game_plays" WHERE "game_plays"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
GameView Load (0.2ms) SELECT "game_views".* FROM "game_views" INNER JOIN "people" ON "game_views"."person_id" = "people"."id" WHERE "people"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<GameView id: 2, game_id: 1, person_id: 1>
I had the same challenge, I solved this with a custom version of Rails preloader https://github.com/2rba/smart_preloader
The association is exactly as you described:
class GamePlay
belongs_to :game
belongs_to :person
has_one :game_view, -> (gp) { where(game_id: gp.game_id) }, foreign_key: :person_id, primary_key: :person_id
end
and then explicitly call preloader as:
game_plays =GamePlay.all
ActiveRecord::SmartPreloader.(game_plays, ActiveRecord::CompositeKey.new(:game_view, [:game_id, :person_id])
that is pretty much the same as Rails default GamePlay.preloads(:game_view) behaviour which under the hood calls ActiveRecord::Associations::Preloader. The only difference preloader called explicitly, and preloader is slightly modified to support multikey and polymorphic associations.

Rails 5: add/edit multiple records in "has_many through" relationship

I have classic has_many through relationship where I need to be able to add multiple Companies to particular User. Models look like this:
class Company < ApplicationRecord
has_many :accounts, dependent: :destroy
has_many :users, through: :accounts
end
class Account < ApplicationRecord
belongs_to :company, inverse_of: :accounts
belongs_to :user, inverse_of: :accounts
accepts_nested_attributes_for :company, :user
end
class User < ApplicationRecord
has_many :accounts, dependent: :destroy
has_many :companies, through: :accounts
end
In console I can add single record with this:
[1] pry(main)> user=User.find(7)
[2] pry(main)> user.accounts.create(company_id: 1)
How do I add, edit, delete multiple accounts for user in one query? I need to attach multiple Companies to User, then Edit / Remove if necessary.
So far I tried to implement array part from this tutorial, but somehow it does not work as obviously I'm doing something wrong here:
[4] pry(main)> user.accounts.create(company_id: [1,2])
(0.4ms) BEGIN
User Exists (1.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) AND ("users"."id" != $2) LIMIT $3 [["email", "tester#gmaill.com"], ["id", 7], ["LIMIT", 1]]
(0.6ms) COMMIT
=> #<Account:0x00000005b2c640 id: nil, company_id: nil, user_id: 7, created_at: nil, updated_at: nil>
As I understand I need to create array somehow and then operate with that. I would appreciate any help here. Thank you!
Solution
If anyone needs, I solved my problem a bit differently. I used checkboxes from this tutorial and it works just fine for me.
Here's an example with the bulk_insert gem:
company_ids = [1,2]
user_id = 1
Account.bulk_insert(
values: company_ids.map do |company_id|
{
user_id: user_id,
company_id: company_id,
created_at: Time.now,
updated_at: Time.now
}
end
)

RoR: fetch has_many association through another has_many

I'm following official documentation: http://guides.rubyonrails.org/association_basics.html at section 4.3.3.4
I have following models:
class Nomination < ActiveRecord::Base
belongs_to :festival
has_many :festival_histories, -> { includes :awards }
attr_accessible :name
end
class FestivalHistory < ActiveRecord::Base
has_many :awards
belongs_to :nomination
belongs_to :festival
end
class Award < ActiveRecord::Base
belongs_to :festival_history
belongs_to :case, inverse_of: :awards
has_attached_file :image
attr_accessible :name, :festival_history_id, :image
end
Which looks very similar (for me) to example in documentation.
But when I do in console:
n = Nomination.first
n.festival_histories.awards
I get
NoMethodError: undefined method `awards' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_FestivalHistory:0x000001019cd400>
I've reloaded console, so issue is not there for sure...
There is no problem with documentation:)
As JTG said, you couldn't get awards on all festival_histories, only on specific history.
The difference is:
With include option:
n = Nomination.first
Nomination Load (0.4ms) SELECT "nominations".* FROM "nominations" ORDER BY "nominations"."id" ASC LIMIT 1
n.festival_histories
FestivalHistory Load (25.5ms) SELECT "festival_histories".* FROM "festival_histories" WHERE "festival_histories"."nomination_id" = ? [["nomination_id", 1]]
Award Load (0.7ms) SELECT "awards".* FROM "awards" WHERE "awards"."festival_history_id" IN (1)
n.festival_histories.first.awards
NO QUERY!
Without include option:
n = Nomination.first
Nomination Load (0.4ms) SELECT "nominations".* FROM "nominations" ORDER BY "nominations"."id" ASC LIMIT 1
n.festival_histories
FestivalHistory Load (25.5ms) SELECT "festival_histories".* FROM "festival_histories" WHERE "festival_histories"."nomination_id" = ? [["nomination_id", 1]]
n.festival_histories.first.awards
Award Load (0.7ms) SELECT "awards".* FROM "awards" WHERE "awards"."festival_history_id" = ? [["festival_history_id", 1]]
I think difference is obvious now:)
class Nomination < ActiveRecord::Base
belongs_to :festival
has_many :festival_histories, -> { includes :awards }
has_many :awards, through: :festival_histories
attr_accessible :name
end
Then you can call
Nomination.first.awards
Here's what's going wrong in you console, since festival_histories is an a collection of records, you cannot get the awards for a collection, only an individual record. So instead of
n = Nomination.first
n.festival_histories.awards
You need
n = Nomination.first
n.festival_histories.each { |r| puts r.awards}
to see the awards for each festival_history.
(So yes, how you are include: the :awards for lazy loading is working, and it's not a mistake in the documentation ;))

Rails how to create a new record of a parent model from the child model

I have 2 rails models, a security and stock_quote model, which are as follows
class StockQuote < ActiveRecord::Base
belongs_to :security, class_name: "Security", foreign_key: 'security_id'
end
class Security < ActiveRecord::Base
has_many :stock_quotes, dependent: :destroy
end
In the rails console when i try doing,
a = Security.create(security: "Goldman Sachs", category: "Investment Banking")
b = a.stock_quotes.first
c = b.security.create(security: "Facebook", category: "Tech")
The last query generates the following error
Security Load (0.3ms) SELECT "securities".* FROM "securities" WHERE "securities"."id" = ? LIMIT 1 [["id", 2]]
NoMethodError: undefined method `create' for #<Security:0xbbd2d78>
What I'm i doing wrongly because my associations are correctly defined
Instead c = b.security.create(security: "Facebook", category: "Tech") you need b.create_security(security: "Facebook", category: "Tech"). This similar to
b.security = Security.new;
b.security.save; b.security
Read doc for ActiveRecord::Base#belongs_to

Resources