Better recursive loop in Ruby on Rails - ruby-on-rails

Using Rails 3.2. Let's say I want 2 options:
Get all trip photos.
Get the first trip photo.
I have the following code:
# trip.rb
class Trip < ActiveRecord::Base
has_many :trip_days
def trip_photos
if (photos = trip_days.map(&:spots).flatten.map(&:photos).flatten.map)
photos.each do |photo|
photo.url(:picture_preview)
end
end
end
def trip_photo
trip_photos.first
end
end
# trip_day.rb
class TripDay < ActiveRecord::Base
belongs_to :trip
has_many :trip_day_spots
has_many :spots, :through => :trip_day_spots
end
# trip_day_spot.rb
class TripDaySpot < ActiveRecord::Base
belongs_to :trip_day
belongs_to :spot
end
#spot.rb
class Spot < ActiveRecord::Base
end
# trips_controller.rb
class TripsController < ApplicationController
def index
#trips = Trip.public.paginate(:page => params[:page], :per_page => 25)
end
end
As expected, the trip_photos method generates lots of SQL query. I wonder if there is any better way to do it?

It is because of N+1 queries. In this cases, we need to eager load all the associations of base object, so that when ever you call its associated object, it wont fire any queries for fetching them, simply it will get them from its cached object.
Hope this will work, but not tested. I assumed and wrote the following query.
def trip_photos
user_trip_days = trip_days.includes(:spots => :photos)
photos = user_trip_days.collect {|trip_day| trip_day.spots.map(&:photos).flatten}.flatten
photos.each do |photo|
photo.url(:picture_preview)
end if photos
end
Let me know if you get any errors.
For more info on eager loading associated objects in ActiveRecord, go through
Guides for Rails and Rails cast and Rails Tips

This might not be the most rails-y way, but if you truly wanted to get all the spots in one hit you could do something like:
def spots
Spot.joins("join trip_days_spots on spots.id = trip_days_spots.spot_id join trip_days on trip_days.id = trip_days_spots.trip_day_id join trips on trips.id = trip_days.trip_id").where("trips.id = ?", self.id)
end
then change your loop to:
def trip_photos
spots.map(&:photos).flatten.each do |photo|
photo.url(:picture_preview)
end
end

The code works fine, but to eager load, just add :include:
# trips_controller.rb
class TripsController < ApplicationController
def index
#trips = Trip.public.paginate(:include => [:trip_days => [:spots => :photos]], :page => params[:page], :per_page => 25)
end
end

Related

Ruby on Rails: Undefined Method Error with has_one associations

For some reason I'm getting an NoMethodError for some query, which actually works in the rails console.
Code in index.html.erb
#requests.first.acceptance
This is my Error
undefined method `acceptance' for #<ActiveRecord::Relation::ActiveRecord_Relation_Arrangement:0x000000042ceeb8>
These are my Modules.
class Arrangement < ActiveRecord::Base
belongs_to :acceptance
belongs_to :request
belongs_to :offer, inverse_of: :arrangements
end
class Acceptance < ActiveRecord::Base
belongs_to :user, inverse_of: :acceptances
has_one :arrangement
end
class Request < ActiveRecord::Base
belongs_to :user, inverse_of: :requests
has_one :arrangement
end
This is my Controller
def index
#requests = Array.new
for request in current_user.requests do
#requests << Arrangement.where("request_id = ?", request.id)
end
#acceptances = Array.new
for acceptance in current_user.acceptances do
#acceptances << Arrangement.where("acceptance_id = ?", acceptance.id)
end
end
I can't figure out what I've done wrong here. Everything works in the console, though not in the browser.
Arrangement.where("request_id = ?", request.id)
Returns an array-like relation object which may contain multiple records, not just a single record.
However, on this line in your controller
#requests << Arrangement.where("request_id = ?", request.id)
You're adding the relation to your array, so that
#requests.first.acceptance
Returns the relation, instead of the first record.
One way to fix this is to do this in your controller:
#requests = Array.new
for request in current_user.requests do
#requests << Arrangement.where("request_id = ?", request.id).first
end
Solved
I was passing an array in the #requests array in my controller.

Eager loading belongs_to relationship in rails

My rails 4 application has many Measures which belong to a Station.
I tried to eager load the measures in my controller:
#station = Station.includes(:measures).friendly.find(params[:id])
#measures = #station.measures
However when I added a method to the measure model which access a property of the station it causes an additional query per measure:
SELECT "stations".* FROM "stations" WHERE "stations"."id" = ? ORDER BY "stations"."id" ASC LIMIT 1
This means about 50 queries per page load which totally tanks performance.
How do I properly eager load the relationship to avoid this n+1 query? Am I going about it wrong?
github: /app/controllers/stations_controller.rb
class StationsController < ApplicationController
...
# GET /stations/1
# GET /stations/1.json
def show
#station = Station.includes(:measures).friendly.find(params[:id])
#measures = #station.measures
end
...
end
github: /app/models/measure.rb
class Measure < ActiveRecord::Base
belongs_to :station, dependent: :destroy, inverse_of: :measures
after_save :calibrate!
after_initialize :calibrate_on_load
...
def calibrate!
# this causes the n+1 query
unless self.calibrated
unless self.station.speed_calibration.nil?
self.speed = (self.speed * self.station.speed_calibration).round(1)
self.min_wind_speed = (self.min_wind_speed * self.station.speed_calibration).round(1)
self.max_wind_speed = (self.max_wind_speed * self.station.speed_calibration).round(1)
self.calibrated = true
end
end
end
def calibrate_on_load
unless self.new_record?
self.calibrate!
end
end
def measure_cannot_be_calibrated
if self.calibrated
errors.add(:speed_calbration, "Calibrated measures cannot be saved!")
end
end
end
github: /app/models/stations.rb
class Station < ActiveRecord::Base
# relations
belongs_to :user, inverse_of: :stations
has_many :measures, inverse_of: :station, counter_cache: true
# slugging
extend FriendlyId
friendly_id :name, :use => [:slugged, :history]
...
end
ADDITION
It interesting to note that this does not cause a n+1 query. But I would rather not duplicate it across my controllers.
class Measure < ActiveRecord::Base
...
# after_initialize :calibrate_on_load
...
end
#station = Station.includes(:measures).friendly.find(params[:id])
#measures = #station.measures
#measures.each do |m|
m.calibrate!
end
Try to include station to measure as well.
def show
#station = Station.includes(:measures).friendly.find(params[:id])
#measures = #station.measures.includes(:station)
end
The :includes option will usually trigger loads in separate queries as you are seeing, because in many cases it is more performant. If you want to ensure it's done in one query, try using :joins instead, but beware that the resulting records returned will be read-only.
after_initialize occurs before relations are eager-loaded by joins or include. See https://github.com/rails/rails/issues/13156
I decided to after some good advice use a dfferent approach and came up with this:
class Station < ActiveRecord::Base
...
alias_method :measures_orig, :measures
def measures
measures_orig.map do |m|
m.calibrate!
end
end
end

rails adding tags to controller

I am implementing a basic tagging feature to my app. New to rails.
I have a listings model and I am working in the listings_controller # index. When the user goes to the listing page and sets the :tag param I want #users only to hold the users which match the tag.
So if they goto www.example.com/listings?tag=foo only pages which have been tagged with foo are loaded. This is what I have come up with so far but there is several problems with it.
def index
if params[:tag]
#id = Tag.where(:name => params[:tag]).first.id
#listingid = Tagging.where(:tag_id => #id)
#listingid.each do |l|
#users = User.find(l.listing_id)
end
else
#users = User.all
end
end
I am not sure how to loop and add each found user to #users. I think I may be going about this whole thing the wrong way.. My tag/tagging models look as follows:
tag.rb
class Tag < ActiveRecord::Base
has_many :taggings
has_many :listings, through: :taggings
end
tagging.rb
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :listing
end
Taggings has the following colums:
id, tag_id, listing_id
Tags has the following columns:
id, name
Any guidance would be appreciated, been trying to fix this for a while with no luck.
Trying with
def index
#tag = Tag.where(:name => params[:tag]).first
if #tag
#listings = #tag.listings.includes(:user)
#users = #listings.map{|l| l.user}
else
#users = User.all
end
end

How to define scope to get one record of a has_many association?

class User
has_many :posts do
def latest(report_date)
order(:report_date).where('report_date <= ?', report_date).limit(1)
end
end
end
class Post
belongs_to :user
end
I would like to retrieve the records of user with the last post for each user.
I could do this:
users = User.all.map{|user| user.posts.latest(7.weeks.ago).first}.compact
Is there a better way to write this? something like:
users = User.posts.latest(7.weeks.ago).all
if that were valid?
I tend to add something like this. Would it work in your case? It's nice because you can 'include' it in list queries...
class User < ActiveRecord::Base
has_many :posts
has_one :latest_post, :class_name => 'Post', :order => 'report_date desc'
...
end
In practice, you would do something like this in the controller:
#users = User.include(:latest_post)
And then, in the view where you render the user, you could refer to user.lastest_post and it will be eager loaded.
For example - if this was in index.html.haml
= render #users
you can access latest_post in _user.html.haml
= user.name
= user.latest_post.report_date

Globally Reuse ActiveRecord Queries across Models?

Not sure how to word the question concisely :).
I have say 20 Posts per page, and each Post has 3-5 tags. If I show all the tags in the sidebar (Tag.all.each...), then is there any way to have a call to post.tags not query the database and just use the tags found from Tag.all?
class Post < ActiveRecord::Base
end
class Tag < ActiveRecord::Base
end
This is how you might use it:
# posts_controller.rb
posts = Post.all
# posts/index.html.haml
- posts.each do |post|
render :partial => "posts/item", :locals => {:post => post}
# posts/_item.html.haml
- post.tags.each do |tag|
%li
%a{:href => "/tags/#{tag.name}"}= tag.name.titleize
I know about the following optimizations already:
Rendering partials using :collection
Eager loading with Post.first(:include => [:tags])
I'm wondering though, if I use Tag.all in my shared/_sidebar.haml template, is there anyway to reuse the result from that query in the post.tags calls?
You can use the tag_ids method on a Post instance.
In your controller create the tag hash. Better still cache the tag hash.
Add this to your application_controller.rb.
def all_tags
#all_tags ||=Rails.cache.fetch('Tag.all', :expire_in => 15.minutes)){ Tag.all }
# without caching
##all_tags ||= Tag.all
end
def all_tags_hash
#all_tags_hash ||= all_tags.inject({}){|hash, tag| hash[tag.id]=tag;hash}
end
def all_tags_by_ids ids
ids ||= []
ids = ids.split(",").map{|str| str.to_i} if ids.is_a?(string)
all_tags_hash.values_at(*ids)
end
helper_method :all_tags, :all_tags_hash, :all_tags_by_id
Now your partial can be rewritten as
# posts/_item.html.haml
- all_tags_by_ids(post.tag_ids).each do |tag|
%li
%a{:href => "/tags/#{tag.name}"}= tag.name.titleize
My solution caches the Tag models for 15 minutes. Make sure you add an observer/filter on Tag model to invalidate/update the cache during create/update/delete.
In your config\environment.rb
config.active_record.observers = :tag_observer
Add a tag_observer.rb file to your app\models directory.
class TagObserver < ActiveRecord::Observer
def after_save(tag)
update_tags_cache(tag)
end
def after_destroy(tag)
update_tags_cache(tag, false)
end
def update_tags_cache(tag, update=true)
tags = Rails.cache.fetch('Tag.all') || []
tags.delete_if{|t| t.id == tag.id}
tags << tag if update
Rails.cache.write('Tag.all', tags, :expire_in => 15.minutes)
end
end
Note: Same solution will work with out the cache also.
This solution still requires you to query the tags table for Tag ids. You can further optimize by storing tag ids as a comma separated string in the Post model(apart from storing it in post_tags table).
class Post < ActiveRecord::Base
has_many :post_tags
has_many :tags, :through => :post_tags
# add a new string column called tag_ids_str to the `posts` table.
end
class PostTag < ActiveRecord::Base
belongs_to :post
belongs_to :tag
after_save :update_tag_ids_str
after_destroy :update_tag_ids_str
def update_tag_ids_str
post.tag_ids_str = post.tag_ids.join(",")
post.save
end
end
class Tag < ActiveRecord::Base
has_many :post_tags
has_many :posts, :through => :post_tags
end
Now your partial can be rewritten as
# posts/_item.html.haml
- all_tags_by_ids(post.tag_ids_str).each do |tag|
%li
%a{:href => "/tags/#{tag.name}"}= tag.name.titleize

Resources