RoR: fetch has_many association through another has_many - ruby-on-rails

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 ;))

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.

Filter by a field of a join table using JSONAPI Resources

I have three tables:
Observation
Subcategory
Category
with the following structure:
class Observation < ApplicationRecord
translates :name
belongs_to :subcategory
end
class Subcategory < ApplicationRecord
belongs_to :category
has_many :observations
end
class Category < ApplicationRecord
has_many :subcategories
end
I want to be able to filter with JSONAPI::Resource the observations based on a category_id
class ObservationResource < JSONAPI::Resource
attributes :name, :subcategory_id
filter :subcategory_id, :test
filter :test, apply: ->(records, value, _options) {
records.where('subcategories.category_id = ?', value[0])
}
end
If I try to do the request:
/observations?include=subcategory,subcategory.category&filter[subcategory.category_id]=1
I get:
subcategory.category_id is not allowed
If I try to do the request:
/observations?include=subcategory,subcategory.category&filter[test]=1
I get an exception coming from Postgres when trying to execute:
SQL (6.4ms) SELECT DISTINCT "observations"."id", "observations"."id"
AS alias_0 FROM "observations" LEFT OUTER JOIN
"observation_translations" ON
"observation_translations"."observation_id" = "observations"."id"
WHERE (subcategories.category_id = '1') ORDER BY "observations"."id"
ASC LIMIT $1 OFFSET $2 [["LIMIT", 1000], ["OFFSET", 0]]
Because there's no join on the table subcategories
If I add a default scope to include the subcategories all the queries work well but the last one fails, when trying to count the records:
SELECT COUNT(*) FROM "observations" WHERE (subcategories.category_id = '1')
How can I do this properly?
Ok, the solution was to use a ´joins`:
filter :test, apply: ->(records, value, _options) {
records.joins(:subcategory).where('subcategories.category_id = ?', value[0])
}

Rails 4 eager loading with multiple tables

I'm trying to eager load results with 5 tables:
class Meeting < ActiveRecord::Base
has_many :bookings
end
class Booking < ActiveRecord::Base
belongs_to :user
belongs_to :meeting
belongs_to :role
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :group_memberships
has_many :users, through: :group_memberships
end
class GroupMembership < ActiveRecord::Base
belongs_to :group
belongs_to :user
end
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, through: :group_memberships
end
Inside MeetingController.rb:
def index
#meetings = Meeting.where(year: #year)
end
I'm trying to use includes(:bookings) but according to bullet-gem the eager loading is not working. And inside my view I'm calling this line:
#meetings.each do |meeting|
meeting.bookings.find_by(group: #group, role: #role)
end
I'm getting multiple 52 lines of this code which clearly means that I'm not eager loading any data:
Booking Load (0.1ms) SELECT "bookings".* FROM "bookings" WHERE "bookings"."meeting_id" = ? AND "bookings"."user_id" = 8 AND "bookings"."group_id" = 2 LIMIT 1 [["meeting_id", 207]]
Rendered shared/_book.html.haml (1.0ms)
Booking Load (0.1ms) SELECT "bookings".* FROM "bookings" WHERE "bookings"."meeting_id" = ? AND "bookings"."group_id" = 2 AND "bookings"."role_id" = 4 LIMIT 1 [["meeting_id", 208]]
Booking Load (0.0ms) SELECT "bookings".* FROM "bookings" WHERE "bookings"."meeting_id" = ? AND "bookings"."user_id" = 8 AND "bookings"."group_id" = 2 LIMIT 1 [["meeting_id", 208]]
Rendered shared/_book.html.haml (1.0ms)
Booking Load (0.1ms) SELECT "bookings".* FROM "bookings" WHERE "bookings"."meeting_id" = ? AND "bookings"."group_id" = 2 AND "bookings"."role_id" = 4 LIMIT 1 [["meeting_id", 209]]
bullet.log:
2014-03-29 15:30:48[WARN] user: karlingen
localhost:3000http://localhost:3000/meetings/host
Unused Eager Loading detected
Meeting => [:bookings]
Remove from your finder: :include => [:bookings]
Any ideas what I should be doing?
Try the following:
#meetings = Meeting.includes(:bookings).where(year: #year)
Also, how are you passing in the #year?
Meeting.includes(:bookings).where(year: #year).references(:bookings)
will work if you are using rails 4.

Why is the before filter giving me an error

Here is my data structure
class User < ActiveRecord::Base
has_many :companies, :through => :positions
has_many :positions
class Company < ActiveRecord::Base
has_many :positions
has_many :users, :through => :positions
class Position < ActiveRecord::Base
belongs_to :company
belongs_to :user
attr_accessible :company_id, :user_id, :regular_user
end
class Position < ActiveRecord::Base
belongs_to :company
belongs_to :user
attr_accessible :company_id, :user_id, :regular_user
before_save :set_regular_user
def set_regular_user
if self.user.is_admin?
self.regular_user = false
else
self.regular_user = true
end
end
end
everytime i run
#user.companies << Company.last
I get ActiveRecord::RecordNotSaved: ActiveRecord::RecordNotSaved
but if i remove my before filter everthing works out perfect and it saves as expected
#user.companies << Company.last
Company Load (0.2ms) SELECT `companies`.* FROM `companies` ORDER BY `companies`.`id` DESC LIMIT 1
(0.1ms) BEGIN
SQL (0.2ms) INSERT INTO `positions` (`company_id`, `created_at`, `regular_user`, `updated_at`, `user_id`)
VALUES
(263, '2012-07-25 14:44:15', NULL, '2012-07-25 14:44:15', 757)
Any ideas what i am missing ....this question is based on this earlier question
Callbacks need to return true in order to continue, false cancels the operation. In your function, the value of the if statement may be false: self.regular_user = false The return value of a ruby function is the last statement.
Just add a return true to the end.
As #DGM says, it is good practice for callbacks to always return true at the end (or false at some point in the flow if they should prevent the code continuing). Otherwise it can be the source of some very odd bugs further down the line (speaking from experience :) ).
I suspect it is the if branch returning the false. Hopefully if you just add true as the last statement in the callback it should work.

Resources