Rails 4 eager loading with multiple tables - ruby-on-rails

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.

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.

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

Create join model record with accepts_nested_attributes_for

I have following models
class User < ActiveRecord::Base
has_many :project_users, dependent: :destroy
has_many :projects, through: :project_users
end
class ProjectUser < ActiveRecord::Base
belongs_to :user
belongs_to :project
has_many :participants
has_many :tasks, through: :participants
end
class Task < ActiveRecord::Base
belongs_to :project
has_many :participants
has_many :project_users, through: :participants
accepts_nested_attributes_for :participants
end
class Participant < ActiveRecord::Base
belongs_to :project_user
belongs_to :task
end
So the flow should go like this:
1. User creates the project
2. User adds users to project via the join model ProjectUser
3. User creates a task for the project and selects those users from ProjectUsers, who will participate in the task. So I put an accepts_nested_attributes_for method and tried to build the nested form.
In my controller:
def new
#task = Task.new
#task.participants.build
end
def task_params
params.require(:task).permit(:project_id, :project_phase_id, :priority_id, :title, :due_date, :estimation, :responsible_id, :description, :participant_ids => [])#, :participants_attributes => [:project_user_id, :task_id])
end
participants_attributes is commented
In my view:
= f.association :participants, as: :select
Actual HTML generated:
<input name="task[participant_ids][]" type="hidden" value="">
<select class="select optional form-control" id="task_participant_ids" multiple="multiple" name="task[participant_ids][]">
<option value="57">AlexandeR MazeiN</option>
<option value="59">Firenze</option>
<option value="58">Vasily Strekotkin</option>
</select>
I add options via ajax, value = ProjectUser.id
I Have to do it like so, because I dont know which ProjectUsers there will be unless a concrete project for a task is selected.
Error I am getting:
Started POST "/tasks" for 127.0.0.1 at 2014-04-11 13:18:24 +0300
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = 6 ORDER BY "users"."id" ASC LIMIT 1
Processing by TasksController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"aXuk9ZuDvFZce+sbIQLRhWZlVjitMvySaJ7CwWfdmaQ=", "task"=>{"project_id"=>"20", "priority_id"=>"4", "project_phase_id"=>"40", "title"=>"Skepta", "due_date"=>"", "estimation"=>"8", "responsible_id"=>"6", "participant_ids"=>["", "57", "58"], "description"=>""}, "commit"=>"Create Task"}
Team Load (0.4ms) SELECT "teams".* FROM "teams" WHERE "teams"."id" = $1 LIMIT 1 [["id", 3]]
Participant Load (0.5ms) SELECT "participants".* FROM "participants" WHERE "participants"."id" IN (57, 58)
Completed 404 Not Found in 7ms
ActiveRecord::RecordNotFound - Couldn't find all Participants with IDs (57, 58) (found 0 results, but was looking for 2):
Your param hash has the IDs from ProjectUser as participant_ids, so when it queries the database it is looking for Participant models with these IDs. You need to set these as project_user_id inside of a list of participants, something like this:
participants: [ { project_user_id: 57 }, { project_user_id: 58 } ]
I'm not super familiar with build, but something along these lines should allow AR to properly construct the associations.

Rails 4 has_many through join table manage attribute increasing

I have the following models in Rails 4 with a simple has_many :through association:
class Plan < ActiveRecord::Base
belongs_to :project
belongs_to :item
quantity: decimal
end
class Project < ActiveRecord::Base
has_many :plans
has_many :items, through: :plans
end
class Item < ActiveRecord::Base
has_many :plans
has_many :projects, through: :plans
belongs_to :unit
end
I have 6 plans:
2.1.1 :002 > Plan.all.count
(0.1ms) SELECT COUNT(*) FROM "plans"
=> 6
I can't acces to quantity attribute:
2.1.1 :014 > Plan.where(item_id: 1, project_id: 2).count
(0.1ms) SELECT COUNT(*) FROM "plans" WHERE "plans"."item_id" = 1 AND "plans"."project_id" = 2
=> 1
2.1.1 :015 > Plan.where(item_id: 1, project_id: 2).quantity
NoMethodError: Plan Load (0.1ms) SELECT "plans".* FROM "plans" WHERE "plans"."item_id" = 1 AND "plans"."project_id" = 2
undefined method `quantity' for #<ActiveRecord::Relation::ActiveRecord_Relation_Plan:0x000000020a8d48>
When a plan contains an item and add same item one more, then have to increase quantity.
The plan controller:
def create
#plan = Plan.new(plan_params)
#plan.project_id = #project.id
if #project.plans.where(:item_id => #plan.item_id).blank?
#plan.save
redirect_to project_plans_url
else
**?????**
redirect_to project_plans_url
end
end
private
def set_project
#project = Project.find(params[:project_id])
end
def plan_params
params.require(:plan).permit(:item_id, :quantity)
end
How can I manage the controller else branch?
You need something like this:
#plan.project_id = #project.id
new_plan = #project.plans.where(item_id: #plan.item_id).first_or_initialize
new_plan.quantity += 1
new_plan.save
and in that case you don't need else at all.

Resources