How do I eager load two levels of associations in 1 call? - ruby-on-rails

I have FamilyTree, Node, Comment, & User models.
The relationship is like this:
FamilyTree
class FamilyTree < ActiveRecord::Base
belongs_to :user
has_many :memberships, dependent: :destroy
has_many :members, through: :memberships, source: :user, dependent: :destroy
has_many :nodes, dependent: :destroy
end
Node
class Node < ActiveRecord::Base
belongs_to :family_tree
belongs_to :user
has_many :comments, dependent: :destroy
end
Comment
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :node
end
User
class User < ActiveRecord::Base
has_one :family_tree, dependent: :destroy
has_many :memberships, dependent: :destroy
has_many :nodes, dependent: :destroy
has_many :comments
end
Membership ## This is just to store the user memberships on various family_trees
class Membership < ActiveRecord::Base
belongs_to :family_tree
belongs_to :user
end
In my Dashboard#IndexController where I am using this, I have:
def index
#family_tree = current_user.family_tree
#nodes = #family_tree.nodes.includes(:comments)
#node = current_user.nodes.new
#memberships = current_user.memberships.limit(3)
end
When I am trying to optimize my app with the Bullet gem, I get this message:
N+1 Query detected
Comment => [:user]
Add to your finder: :include => [:user]
N+1 Query method call stack
My _comments partial that is generating this N+1 issue is called like this - in my views/dashboard/index.html.erb:
<% #nodes.each do |node| %>
<%= render partial: "shared/comments", locals: {node: node} %>
<% end %> <!-- node -->
This is where the n+1 offending queries occur, around these lines in my _comments partial.
<% node.comments.each do |comment| %>
<li class="clearfix">
<a class="avatar" href="#">
<%= image_tag(comment.user.avatar.url)%>
So it seems the solution is to optimize my controller call, but I am not quite sure how to do 2-levels of association. I tried this:
#nodes = #family_tree.nodes.includes(:comments).includes(:user)
But that doesn't seem to get rid of the N+1 query problem.
Any ideas?

You have to pass a hash to the includes
#nodes = #family_tree.nodes.includes(:comments => :user)

Related

Rails 5.1, sum by currency with nested resources

I have a problem with a SUM calculation. I have multiple currency in a table, I have users, I have groups and I have a bets table. The user can join a group via member (user_id and group_id). And the user can share his bet via groupbet (bet_id and member_id). I don't enter in detail here because all this works properly.
I would like to do some SUM calculation in the group view (show). This is where I'd like to display the total amount of money the members of the group spend for there bet. I also would like to use chartkick in the mix to show some nice graph.
This is what I have so far
My controller :
m = Member.where(group_id: #group.id)
gs = Groupbet.where(member_id: m)
#tasks = gs.joins(:bet).select("bets.*, sum(amount) as total")
#amount = gs.joins(:bet).
select(:symb, 'bets.*, sum(amount) AS amount').
where(bet_id: { id: #current_user.bets.all.map(&:id) }).
group(:symb)
#tasks works but SUM all the spendings from the group, that's cool, but I have several currencies so the amount is not correct. This is why I came up with #amount which in the view :
<% #amount.each do |amount| %>
<%= '%.02f' % "#{amount.amount}" %>
<%= "#{amount.symb}" %>
<% end %>
display nothing at all.
the :symb is one of my Currency column btw. In currency I have name and symb, where symbole is this : € or $ for example. In my spending table I have a column amount and the currency_id.
My relations :
class Currency < ApplicationRecord
has_many :bets, dependent: :destroy
has_many :users, through: :bets
end
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :users, through: :members, source: :user
belongs_to :owner, class_name: "User"
has_many :links, through: :grouplinks
has_many :grouplinks, through: :members
has_many :bets, through: :groupbets
has_many :groupbets, through: :members
has_many :notifications, dependent: :destroy
def to_param
auth_token
end
end
class Groupbet < ApplicationRecord
belongs_to :bet
belongs_to :member
end
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
has_many :grouplinks, dependent: :destroy
has_many :groupbets, dependent: :destroy
validates :user_id, :presence => true
validates :group_id, :presence => true
validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}
end
class Bet < ApplicationRecord
belongs_to :user
belongs_to :currency
has_many :groupbets, dependent: :destroy
end
class User < ApplicationRecord
has_secure_password
has_many :bets, dependent: :destroy
has_many :currencies, through: :bets
has_many :links, dependent: :destroy
has_many :notifications, dependent: :destroy
has_many :members, :dependent => :destroy
has_many :groups, :through => :members
has_one :owned_group, foreign_key: "owner_id", class_name: "Group"
end
Links has nothing to do here but yeah...
I'm also using groupdate and for chartkick I don't really know because when a user creates a bet he can actually go on his profile and check all the bets he did, as well in a chart. To display this chart, which is working, I'm doing like so :
My helper
def expenses_bet_amount_per_month
#current_user.bets.all.joins(:currency).group('currencies.symb').group_by_month(:date, format: "%B %Y").sum(:amount)
end
date is the date when the user create the bet. Don't ask why I didn't use created_at, doesn't matter :)
And my view :
<%= column_chart expenses_bet_amount_per_month %>
That works. For one single user. But for the group I don't know how I could make that happen.
If anyone can save the situation ? Thank you very much
EDIT :
The code for the single user :
def sums_by_currency_total
Currency.joins(:bets).
select(:symb, 'SUM(bets.amount) AS amount').
where(bets: { id: user_bets_total.map(&:id) }).
group(:symb)
end
def user_spendings_total
user.bets.all
end
That works for one user. Display the SUM from all bets per currency. But for a group ? I don't know
Ok people I managed !!!!
I share the code in case anybody is interested :
def sums_by_currency_group
m = Member.where(group_id: #group.id)
gs = Groupbet.where(member_id: m)
Currency.joins(:bets)
.select(:symb, 'SUM(bets.amount) AS amount')
.where(bets: { id: gs.all.map(&:bet_id) })
.group(:symb)
end
In the view :
<% sums_by_currency_group.each do |sum| %>
<%= '%.02f' % "#{sum.amount}" %> <%= "#{sum.symb}" %>
<% end %>
And there we go. All the SUM from the bets that are shared in each group.
I also solved the problem for chart kick :
def bets_amount_per_month_group
m = Member.where(group_id: #group.id)
gs = Groupbet.where(member_id: m)
Currency.joins(:bets)
.where(bets: { id: gs.all.map(&:bet_id) })
.group('currencies.symb')
.group_by_month(:date, format: "%B %Y")
.sum(:amount)
end
In the view :
<%= column_chart bets_amount_per_month_group %>
Et voila ! Nice chart of all the bets shared in the group. I used the same method as the calculation and it worked !! Olé !!

rails 4 multiple ruby arrays with nested attributes

I'm trying to build a method that pulls an array of arrays, much like nested loops in the view.
I am trying to build a method on my User model that does this:
#past_parties = User.parties
<%= #past_parties.each do |past_party| %>
<%= past_party.guests("email").uniq.each do |guest| %>
<%= guest.name %> <%= guest.email %>
<% end %>
<% end %>
These are my models:
class User < ActiveRecord::Base
has_many :hosts, dependent: :destroy
has_many :parties, through: :hosts
def past_guests
self.parties.guests
end
end
class Host < ActiveRecord::Base
belongs_to :user
has_many :parties, dependent: :destroy
has_many :guests, through: :parties
end
class Party < ActiveRecord::Base
belongs_to :host
has_many :guests, dependent: :destroy
end
class Guest < ActiveRecord::Base
belongs_to :party
end
I can get the loop to work in the view, but when I try to build it as a method (so I can call current_user.past_guests, if current_user is an instance of User.), I get this error
undefined method `guests' for #<ActiveRecord::Associations::CollectionProxy []>
How do I get all of the guests that a user has ever invited without building the loop in the view?
The problem is that you're trying to access an array from another array:
self.parties.guests
self.parties returns an #<ActiveRecord::Associations::CollectionProxy []>, so if you want to get the guests of the parties you have to loop over the elements.
But since you want only the guests, you can simply change your user class to:
class User < ActiveRecord::Base
has_many :hosts, dependent: :destroy
has_many :parties, through: :hosts
has_many :guests, through: :parties
# Call your guests
def past_guests
self.guests
end
end
Each party is going to have collection of guests. You need to do this in your method definition:
def past_guests
self.parties.collect(&:guests)
end

Rails Checking if One Array is in Another

I have five models: Course, Lesson, Question, Answer and User.
What I'm trying to do is determine if the User has Answers for all of the Questions in a Lesson (so I can put "Done" next to the lesson in the view if this is the case).
My models:
class Course < ActiveRecord::Base
has_many :lessons, dependent: :destroy
has_many :questions, :through => :lessons
has_many :users, through: :purchases
end
class Lesson < ActiveRecord::Base
belongs_to :course
has_many :questions, dependent: :destroy
has_many :answers, through: :questions
end
class Question < ActiveRecord::Base
belongs_to :lesson
belongs_to :course
has_many :answers, dependent: :destroy
end
class Answer < ActiveRecord::Base
belongs_to :question
belongs_to :user
end
class User < ActiveRecord::Base
has_many :answers
has_one :author
has_many :courses, through: :purchases
end
What I tried to do was to check if a Lesson's Questions were in the Questions the User Answered, but the includes? line doesn't seem to be working the way I want.
in my controller, I have:
#lessons = #course.lessons
#answers = current_user.answers
#questions = Question.where(:id => #answers.map(&:question_id))
in my view, I have:
<% #lessons.each do |lesson| %>
<% lesson_questions = lesson.questions %>
<%= user_questions = #questions.where("lesson_id = ?", lesson.id)%>
<% if user_questions.include?(lesson_questions)%>
Done!
<% end %>
<% end %>
I'm not sure if this is the cause, but I noticed the lesson_questions are #<Question::ActiveRecord_Associations_CollectionProxy:0x9c49698>
While the user_questions are: #<Question::ActiveRecord_Relation:0x9c48330>
I'm wondering, (a) how I accomplish my objective of finding the Lessons with all of the Questions answered, and (b) if there's a more efficient way to do this. Thanks!
Problem
You can't check if an array includes another array just like this:
user_questions.include?(lesson_questions)
You need to check if each element from lesson_questions is included in the user_questions.
Try these instead:
Solution: 1
lesson_questions.all? { |lq| user_questions.include?(lq) }
This should return true if all the lesson_questions are included in the user_questions.
Solution: 2
(lesson_questions - user_questions).empty?

Listing all instances of a document model that belongs to user through group relationship

I basically followed the ROR guide, http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association, to create the relationship models as shown below.
Because of the through association, I figured that #user.trips would give you both the trips that the user created and the trips that belong to the user. However, when I do #user.trips.count in console, the result was only the number of trips that the users created; the trips that belonged to the user through the 'group' association was not counted.
Question: How do I get my view to display both the trips that the user created and the trips that the user belongs to through 'group'?
user/show.html.erb
<% unless #user.all_trips.empty? %>
<% #user.all_trips.each do |trip| %>
<!-- Content -->
<% end %>
<% end %>
user.rb
class User < ActiveRecord::Base
has_many :group_trips, :through => :groups,
:source => :trip
has_many :trips, :dependent => :destroy
has_many :groups
def all_trips
self.trips | self.group_trips
end
end
trip.rb
class Trip < ActiveRecord::Base
belongs_to :user
belongs_to :traveldeal
has_many :groups
has_many :users, :through => :groups
end
group.rb
class Group < ActiveRecord::Base
belongs_to :trip
belongs_to :user
end
Thanks!
Edit: Modified code per TSherif's partial solution.
Edit 2: Fixed up the all_trips method. Everything appears to work for me at this point.
Oh! I think I get what you're trying to do and why it's a problem. I was wondering why has_many :trips was called twice. But from what I understand, you have two different User-Trip relationships. These two can't have the same name, otherwise one will hide the other. Try something like this:
class User < ActiveRecord::Base
has_many :group_trips, :through => :groups,
:class_name => "Trip"
has_many :trips, :dependent => :destroy
has_many :groups
def all_trips
Trip.joins(:groups).where({:user_id => self.id} | {:groups => {:user_id => self.id}})
end
end
Or if you're using an older version of Rails that doesn't have MetaWhere:
def all_trips
Trip.joins(:groups).where("(trips.user_id = ?) OR (groups.user_id = ?)", self.id, self.id)
end

NoMethodError? Counting Records

I have the following models:
class Label < ActiveRecord::Base
has_many :releases
end
class Release < ActiveRecord::Base
belongs_to :label
has_many :products
has_and_belongs_to_many :tracks
def self.releases_count
self.count(:all)
end
end
class Product < ActiveRecord::Base
belongs_to :release
has_many :releases_tracks, :through => :release, :source => :tracks
has_and_belongs_to_many :tracks
def self.products_count
self.count(:all)
end
end
On my label/index view i'm able to display a count of the Releases absolutely fine using:
<%= label.releases.releases_count %>
I'm trying to do the same for Products using:
<%= label.releases.products.products_count %>
But get a NoMethodError:
undefined method `products' for #<Label:0x10ff59690>
Any ideas?
I have lots of other aggregations I want to perform (Track Counts etc) so some guidance on where I'm going wrong would be really appreciated.
You need define your production/Label association
class Label < ActiveRecord::Base
has_many :releases
has_many :products, :through => :releases
end

Resources