Eager Load indirect associations Rails - ruby-on-rails

Associations are below:
#app/models/pet.rb
class Pet < ActiveRecord::Base
belongs_to :pet_store
end
#app/models/pet_store.rb
class PetStore < ActiveRecord::Base
has_many :pets, dependent: :destroy
has_many :employees, dependent: :destroy
end
#app/models/employee.rb
class Employee < ActiveRecord::Base
belongs_to :pet_store
end
I want to do something like so which would cause an N + 1 error:
#pets = Pet.where(species: "Dog").includes(:pet_store)
#pets.each do |pet|
pet.pet_store.employees.each do |employee|
puts employee.name
end
end
This causes an N+1 error because a query must be made for each employee. I would like to eager load the indirect associated employees. However, I cannot simply includes(:employees) because a pet has no direct association to employees. How can this be done?

You can with:
#pets = Pet.includes(:pet_store => :employees)
The Rails Guide on the Query Language is great. Here's the docs on eager-loading.

Related

Avoiding N+1 queries in Ruby on Rails with nested associations and conditions

I'm working on a podcast player and wish to display a list of recently updated feeds, along with details of the play time remaining for the most recently published entry.
So the view looks something like:
#feeds.each do |f|
puts #feed.rss_image.url
puts #feed.most_recent_entry.published_at
if play = #feed.most_recent_entry.most_recent_play_by(#user)
puts play.remaining
end
end
My models are as follows:
class Feed < ApplicationRecord
has_one :rss_image, as: :rss_imageable
has_many :entries, dependent: :destroy
has_one :most_recent_entry, -> { order(published_at: :desc) }, class_name: "Entry"
has_many :plays, dependent: :destroy
end
class Entry < ApplicationRecord
belongs_to :feed, touch: true
has_many :plays, dependent: :destroy
has_one :most_recent_play, -> { order(updated_at: :desc) }, class_name: "Play"
def most_recent_play_by(user)
plays.by(user).order(updated_at: :desc).first
end
end
class Play < ApplicationRecord
belongs_to :entry
belongs_to :feed
belongs_to :user
scope :by, ->(user) { where(user: user) }
def self.most_recent_by(user)
by(user).order(updated_at: :desc).first
end
end
My query is:
#feeds = Feed
.joins(:entries)
.includes(:rss_image, most_recent_entry: :most_recent_play)
.where(most_recent_entry: {plays: {user: #user}})
.group(:id)
.order("max(entries.published_at) DESC")
.limit(10)
But this errors with:
PG::GroupingError: ERROR: column "rss_images.id" must appear in the GROUP BY clause or be used in an aggregate function
Is it possible to achieve this without N+1 queries?
Thanks!
Take a look to Bullet gem, it helps to reduce the number of queries and eliminate n+1. In this case it should suggest you how to modify you query, eg. adding .includes(:entries) ....

Rails .where statement with nested resources

I'm struggling with a .where statement in an index action.
In my Deals controller, i'd like to list all the deals where the bank of the current_user is participating.
Below are my models :
class User < ActiveRecord::Base
belongs_to :bank
end
class Deal < ActiveRecord::Base
has_many :pools
end
class Pool < ActiveRecord::Base
belongs_to :deal
has_many :participating_banks, dependent: :destroy
has_many :banks, through: :participating_banks
end
class ParticipatingBank < ActiveRecord::Base
belongs_to :pool
belongs_to :bank
end
Here is my Deals Controller Index action :
def index
#deals = Deal.all
end
I don't find any way to say : 'I only want to see a deal if this deal has, at least, one pool where the current_user.bank has been added'.
Any idea?
Many thanks :)
You should do inner join and query joined table for id. You can easily do it in Rails by:
def index
#deals = Deal.joins(pools: :banks).where(banks: { id: current_user.bank_id })
end

Filtring query in :through association

I have 3 models:
class User < ActiveRecord::Base
has_many :categories
has_many :links, through: :categories
end
class Category < ActiveRecord::Base
belongs_to :user
has_many :links
end
class Link < ActiveRecord::Base
belongs_to :category
end
For given user I want to find all his links which have favorite field equals true. I'm learning rails from 'rails guides' and I searched there for simple query to this, but I didn't find anything. Finally I resolved problem using select iterator:
#links = current_user.links.select{ |l| l.favorite }
But I'm not sure it is a good solution. How do that in rails way?
To add to #Pierre Michard's answer, you may also wish to look at ActiveRecord Association Extensions, which will basically replace the scope in the Link model:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :categories
has_many :links, through: :categories do
def favorites
where favorite: true
end
end
end
This will allow you to call:
#links = current_user.links.favorites
That works fine but the SQL query generated by that code will be something like
SELECT * FROM links where links.user_id = ?
Then the links you're interested in will be filtered by the select method.
If your user has many links and few of them are favorites, it could be more efficient to select the favorites this way:
#links = current_user.links.where(favorite: true)
This will generate this kind of query:
SELECT * FROM links where links.user_id = ? AND links.favorite = 't'
you can either create a scope in your links model to filter favorite links.
class Links < ActiveRecord::Base
scope :favorites, -> { where(favorite: true) }
end
corresponding query:
#links = current_user.links.favorites
This can be more efficient because that will create less ActiveModel objets.
Many to many relationship needs to be,
class User < ActiveRecord::Base
has_many :categories
has_many :links, through: :categories
end
class Category < ActiveRecord::Base
belongs_to :user
belongs_to :link
end
class Link < ActiveRecord::Base
has_many :categories
has_many :users, through: :categories
end
And then you can fetch link records whose favorite column is true as,
#links = current_user.categories.include(:links).where('links.favorite = ?', true)

Rails: stringing together multiple has_many relationships

I have three models which look something like this:
Class User < ActiveRecord::Base
has_many :comments
end
Class Comment < ActiveRecord::Base
belongs_to :user
has_many :votes
end
Class Vote < ActiveRecord::Base
belongs_to :comment
end
Now I want to get all the votes associated with a user's comments like so:
#user.comments.votes
But this throws the error:
undefined method `votes' for #<ActiveRecord::Relation:0x3f6f8a0>
This seems like it should work, but I suspect ActiveRecord is coughing on the deeper has_many relationship. I've hacked together an SQL query that gets the desired results, but I suspect there's a cleaner way using purely ActiveRecord. Any tips?
You should use a has_many :through association
In your case it would be
Class User < ActiveRecord::Base
has_many :comments
has_many :votes, :through => :comments
end
Class Comment < ActiveRecord::Base
belongs_to :user
has_many :votes
end
Class Vote < ActiveRecord::Base
belongs_to :comment
end
And then simply get the votes with
#user.votes
Try this:
Vote.joins(comment: :user).where(users: {id: #user.id})

Rails: Querying through 2 join table associations

I have a quite complicated relation between models and are now frustrated by a SQL Query to retrieve some objects.
given a Product model connected to a category model via a has_many :through association and a joint table categorization.
Also a User model connected to this category model via a has_many :through association and a joint table *category_friendship*.
I am now facing the problem to retrieve all products, which are within the categories of the array user.category_ids. However, I can't just not manage to write the WHERE statement properly.
I tried this:
u = User.first
uc = u.category_ids
Product.where("category_id IN (?)", uc)
However this won't work, as it doesn't have a category_id in the product table directly. But how can I change this to use the joint table categorizations?
I'm giving you the model details, maybe you find it helpful for answering my question:
Product.rb
class Product < ActiveRecord::Base
belongs_to :category
def self.from_users_or_categories_followed_by(user)
cf = user.category_ids
uf = user.friend_ids
where("user_id IN (?)", uf) # Products out of friend_ids (uf) works fine, but how to extend to categories (cf) with an OR clause?
end
Category.rb
class Category < ActiveRecord::Base
has_many :categorizations
has_many :products, through: :categorizations
has_many :category_friendships
has_many :users, through: :category_friendships
Categorization.rb
class Categorization < ActiveRecord::Base
belongs_to :category
belongs_to :product
Category_friendship.rb
class CategoryFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :category
User.rb
class User < ActiveRecord::Base
has_many :category_friendships
has_many :categories, through: :category_friendships
def feed
Product.from_users_or_categories_followed_by(self) #this should aggregate the Products
end
If you need more details to answer, please feel free to ask!
Looking at the associations you have defined and simplifying things. Doing a bit refactoring in what we have to achieve.
Product.rb
class Product < ActiveRecord::Base
belongs_to :category
end
User.rb
class User < ActiveRecord::Base
has_many :categories, through: :category_friendships
scope :all_data , includes(:categories => [:products])
def get_categories
categories
end
def feed
all_products = Array.new
get_categories.collect {|category| category.get_products }.uniq
end
end
Category.rb
class Category < ActiveRecord::Base
has_many :users, through: :category_friendships
has_many :products
def get_products
products
end
end
NO NEED OF CREATING CATEGORY_FRIENDSHIP MODEL ONLY A JOIN TABLE IS NEEDED WITH NAME CATEGORIES_FRIENSHIPS WHICH WILL JUST HAVE USER_ID AND CATEGORY_ID
USAGE: UPDATED
Controller
class UserController < ApplicationController
def index
#all_user_data = User.all_data
end
end
view index.html.erb
<% for user in #all_user_data %>
<% for products in user.feed %>
<% for product in products %>
<%= product.name %>
end
end
end
I've upvoted Ankits answer but I realized there is a more elegant way of handeling this:
given:
u = User.first
uc = u.category_ids
then I can retrieve the products out of the categories by using:
products = Product.joins(:categories).where('category_id IN (?)', uc)

Resources