Caching association that was in where clause - ruby-on-rails

Let me show an example:
I have 2 models:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
scope :created_in, ->(start_date, end_date) { where(created_at: start_date..end_date) }
end
What I want is to get users that created post during a specific period:
users = User.includes(:posts).joins(:posts).merge(Post.created_in(start_date, end_date))
Is it somehow possible to cache posts that are in the where clause? So after I do
users.first.posts
it will show me exactly those posts that match the condition without producing any additional queries.

No, I don't think this is possible. Depending on the context, what you can do is to do a lookup table which you memoize / cache. Something like
User.all.each do |user|
posts = posts_by_user_id[user.id]
end
def posts_by_user_id
#_posts_by_user_id ||= posts.group_by(&:user_id)
end
def posts
Post.created_in(start_date, end_date)
end

Related

activerecord complex joins

I have a many to many relationship and I want to get all the users of all the cars of one user. What is the most efficient join for that?
Basically, I want to know how to do the following but with activerecord
complex SQL query, many to many
class User < ApplicationRecord
def people_who_use_my_car
self.cars.users
end
end
In your controller or view or wherever you want to use it do:
#user = User.first
#users = #user.people_who_use_my_car
A straightforward way would be
class User < ApplicationRecord
def people_who_use_my_car
self.cars.inject([]).do |users, car|
users += car.users
users
end
end
end
But this is not as efficient as using the database as #poet suggests,
class User < ApplicationRecord
def people_who_use_my_car
User.joins(:cars).where(cars: {user: self})
end
end

Rails Where vs Join vs Union

Having trouble with my activerecord searches. I thought I had my models setup correctly, but I’m poor with my joins (not sure if a join or union is the correct way to go? It shouldn’t be this difficult).
I have guides creating bids on trips that have start_dates. I want to create a list of bids that have expired (ie. the start date is in the past). Guides can also have LosingBids if a bid has been declined
In a perfect world I would have one resultset that includes both losing bids and expired bids for that guide, but I’m find with 2 different result sets. Unfortunately I can’t get any of the “expired bids” to work. Results/errors in the comments of the code.
class GuidesController < ApplicationController
def expired_declined
#this declined_bids call works
#declined_bids = LosingBid.where("guide_id = ?", current_guide.id.to_s)
#this expired_bids call returns Trips, not Bids
#expired_bids = Bid.where("guide_id = ?", current_guide.id.to_s).expired
#this expired_bids call gives me the following error:
#SQLite3::SQLException: no such column: trips.start_date: SELECT 1 AS one FROM #”bids" WHERE (guide_id = '1') AND (trips.start_date < '2018-05-30') LIMIT ?
#expired_bids = Bid.where("guide_id = ?", current_guide.id.to_s).where("trips.start_date < ?", Date.today)
end
end
class Guide < ApplicationRecord
has_many :bids
has_many :losing_bids
end
class Trip < ApplicationRecord
has_many :bids
end
class Bid < ApplicationRecord
belongs_to :trip
belongs_to :guide
def self.expired
Trip.where("start_date <= ?", Date.today) #.where("guide_id = ?", current_guide.id.to_s)
end
end
class LosingBid < ApplicationRecord
belongs_to :trip
belongs_to :guide
end
Trip.where("start_date <= ?", Date.today).bids will return you the expired bids.
You should move the expired scope in the Trip, rather than on the Bid.
If you want a scope on Bid you can define.
class Bid
scope :expired, -> { joins(:trip).where('trips.start_date <= ?', Date.current) }
end
I would really question if you need to have a separate LosingBid model or if its just creating duplication and unnecessary complexity. Instead just add an enum column to bids which contains the status:
class Bid
enum status: [:pending, :declined, :expired, :accepted]
end
This is just a simple integer column that acts as a bit mask.
This will simply let you query by:
Bid.pending
Bid.expired
Bid.where(status: [:pending, :accepted])
Bid.where.not(status: :accepted)
You can simply reject a bid by:
class BidsController
# PATCH /bids/decline
def reject
#bid.declined!
redirect_to bid, notice: 'Bid declined'
end
end
You could then setup scheduled task which runs once per day to automatically expire tasks (example with the whenever gem):
every 1.days do
runner "BidExpiryService.perform"
end
# app/services/bid_expiry_service.rb
module BidExpiryService
def self.perform
bids = Bid.pending
.joins(:trip)
.where('trips.start_date <= ?', Date.current)
bids.update_all(status: Bid.statuses[:expired])
# #todo notify guides that bid has expired
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

Active Relation: Retrieving records through an association?

I have the following models:
class User < ActiveRecord::Base
has_many :survey_takings
end
class SurveyTaking < ActiveRecord::Base
belongs_to :survey
def self.surveys_taken # must return surveys, not survey_takings
where(:state => 'completed').map(&:survey)
end
def self.last_survey_taken
surveys_taken.maximum(:position) # that's Survey#position
end
end
The goal is to be able to call #user.survey_takings.last_survey_taken from a controller. (That's contrived, but go with it; the general goal is to be able to call class methods on #user.survey_takings that can use relations on the associated surveys.)
In its current form, this code won't work; surveys_taken collapses the ActiveRelation into an array when I call .map(&:survey). Is there some way to instead return a relation for all the joined surveys? I can't just do this:
def self.surveys_taken
Survey.join(:survey_takings).where("survey_takings.state = 'completed'")
end
because #user.survey_takings.surveys_taken would join all the completed survey_takings, not just the completed survey_takings for #user.
I guess what I want is the equivalent of
class User < ActiveRecord::Base
has_many :survey_takings
has_many :surveys_taken, :through => :survey_takings, :source => :surveys
end
but I can't access that surveys_taken association from SurveyTaking.last_survey_taken.
If I'm understanding correctly you want to find completed surveys by a certain user? If so you can do:
Survey.join(:survey_takings).where("survey_takings.state = 'completed'", :user => #user)
Also it looks like instead of:
def self.surveys_taken
where(:state => 'completed').map(&:survey)
end
You may want to use scopes:
scope :surveys_taken, where(:state => 'completed')
I think what I'm looking for is this:
class SurveyTaking < ActiveRecord::Base
def self.surveys_taken
Survey.joins(:survey_takings).where("survey_takings.state = 'completed'").merge(self.scoped)
end
end
This way, SurveyTaking.surveys_taken returns surveys taken by anyone, but #user.survey_takings.surveys_taken returns surveys taken by #user. The key is merge(self.scoped).
Waiting for further comments before I accept..

Bulk update attribute on multiple models in ActiveRecord?

I have a simple has_many association, and I want to change an attribute from public to private the associated object. What's the best way to do this:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
user = User.first #=> #<User...>
user.posts.count #=> 100
# something like this:
user.posts.bulk_update_attribute("privacy", "private") #=> 1 DB call
I believe you are looking for update_all.
In your example, you'd rewrite it to be something like
Post.update_all("privacy = 'private'", ["user_id = ?", user.id])
Or as #jenjenut233 points out
user.posts.update_all("privacy = 'private'")

Resources