Rails Where vs Join vs Union - ruby-on-rails

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

Related

Caching association that was in where clause

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

Rails update product quantity

I followed Ryan Bates screencasts using paypal standard payments and i basically now have to send checkout info from the Cart model.
I'm a bit stuck with trying to update product quantity after the transaction is completed.
I tried using a callback but to no avail. Any help would be appreciated
The closest i got was to use this update quantity callback but for some reason, it is updating the wrong cart. Not sure whether it picks up the wrong line item or its when it checks the cart it goes wrong
class PaymentNotification < ActiveRecord::Base
belongs_to :cart
serialize :params
after_create :mark_cart_as_purchased, :update_quantity
private
def mark_cart_as_purchased
if status == "Completed"
cart.update_attribute(:purchased_at, Time.now)
end
end
def update_quantity
#line_item = LineItem.find(params[:id])
#line_item.upd
end
end
Line Item Class
class LineItem < ActiveRecord::Base
belongs_to :order
belongs_to :product
belongs_to :cart
belongs_to :stock
after_create :stock_stat
def total_price
product.price * quantity
end
def upd
if cart.purchased_at
product.decrement!(quantity: params[:quantity])
end
end
end
The params hash is only available in the controller. You cannot access it in the model. You have to pass params[:quantity] to the upd method as a method parameter as such:
def update_quantity
#line_item = LineItem.find(params[:id])
#line_item.upd(params[:quantity])
end
def upd(quantity)
if cart.purchased_at
product.decrement!(quantity: quantity)
end
end
Also, you should consider using Time.current instead of Time.now in order to account for the time zone your application is configured with in application.rb unless you just want to use whatever time is local.

Rails Methods that call themselves

In Rails is there a way that I can make a method call itself based on a change in the database? For instance, lets say I have two classes: Products and Orders.
Orders have three possible enum values:
class Order < ActiveRecord::Base
enum status: [:pending, :processing,:shipped]
belongs_to :products
end
I would like to batch process Orders so when a product has 50 orders, I want it to set all Orders associated with it to processed. Orders default to :pending. To change an order to :processing I would call order.processing!. I could write a method into the Products model like:
def process_orders
if self.orders.count=50
self.orders.each do |order|
order.processing!
end
end
The problem with this is that I would have to call the process_orders method for it to execute, is there anyway I could make it automatically execute once a product has 50 orders?
This is sounds like a good opportunity to use an Active Record Callback.
class Order < ActiveRecord::Base
belongs_to :product
after_save do
product.process_orders if product.pending_threshold_met?
end
end
class Product < ActiveRecord::Base
has_many :orders
def pending_threshold_met?
orders.where(status: :pending).count >= 50
end
end
I think you can use update_all to update the status column of all of your orders at once rather looping through them one by one:
self.orders.update_all(status: :processing)
and wrap that inside a callback.
Something like this:
class Order < ActiveRecord::Base
after_save do
product.process_orders if product.has_fifty_pending_orders?
end
# rest of your model code
end
class Product < ActiveRecord::Base
# rest of your model code
def process_orders
self.orders.update_all(status: :processing)
end
def has_fifty_pending_orders?
self.orders.where(status: :pending).count >= 50
end
end

Rails and Postgres: Finding items that are a "good deal"

I have two models in my Rails application which keeps track of the prices of products at different shops. Here they are, but simplified:
class Product < ActiveRecord::Base
attr_accessor :name
def latest_prices
prices.where('created_at >= ?', 30.days.ago)
end
def average_price
latest_prices.prices.map(&:value).sum / latest_prices.count
end
end
class Price < ActiveRecord::Base
attr_accessor :value, :shop_name, :created_at
belongs_to :product
end
I now want to find all Price objects which fall below the current average for that product. That basically means all Prices created in the last 30 days, that have a price below the recent average price for that Product.
Is this possible? I'm using Postgres.
Edit: I should have mentioned - I want to implement this method from the Price model - that is, just be able to display all prices that are a good deal, rather than all prices for a Product that are good deals.
Thanks in advance for any help!
Using named scopes in ActiveRecord, you can use composition to get what you want:
class Product < ActiveRecord::Base
attr_accessor :name
has_many :prices
end
class Price < ActiveRecord::Base
attr_accessor :value, :shop_name, :created_at
belongs_to :product
scope :latest, where('created_at >= ?', 30.days.ago)
scope :less_than, lambda { |value| where("value < ?", value) }
def good_deals
latest.less_than(average('value'))
end
end
try this:
class Product < ActiveRecord::Base
attr_accessor :name
def your_query
prices.where('created_at >= ?', 30.days.ago).where('value < ?', average_price)
end
def latest_prices
prices.where('created_at >= ?', 30.days.ago)
end
def average_price
latest_prices.prices.map(&:value).sum / latest_prices.count
end
end

Ruby on Rails - maximum count of associated objects?

I need help with a query. I have multiple Canteens, where each has multiple Meals, where each meal has multiple MealPicks.
Although I don't know if this MealPick model is good idea, because I need to display how many times has the meal been picked TODAY, so I needed the timestamp to make this query.
class Meal < ActiveRecord::Base
def todays_picks
meal_picks.where(["created_at >= ? AND created_at < ?", Date.today.beginning_of_day, Date.today.end_of_day])
end
end
Before I had just a meal_picked_count counter in Meal which I incremented by increment_counter method.
Okay so, now I need to display for each Canteen the Meal that has the most MealPicks, I played around in the console and tried something like Canteen.find(1).meals.maximum("meal_picks.count") but that obviously does not work as it is not a column.
Any ideas?
You can do this:
MealPick.joins(:meal => :canteen)
.where("canteens.id = ?", 1)
.order("count_all DESC")
.group(:meal_id)
.count
That will return an ordered hash like this:
{ 200 => 25 }
Where 200 would be the meal id and 25 would be the count.
Update
For anyone interested, I started playing around with this to see if I could use subqueries with ActiveRecord to give me meaningful information than what I came up with before. Here's what I have:
class Meal < ActiveRecord::Base
belongs_to :canteen
has_many :meal_picks
attr_accessible :name, :price
scope :with_grouped_picks, ->() {
query = <<-QUERY
INNER JOIN (#{Arel.sql(MealPick.counted_by_meal.to_sql)}) as top_picks
ON meals.id = top_picks.meal_id
QUERY
joins(query)
}
scope :top_picks, with_grouped_picks.order("top_picks.number_of_picks DESC")
scope :top_pick, top_picks.limit(1)
end
class MealPick < ActiveRecord::Base
belongs_to :meal
attr_accessible :user
scope :counted_by_meal, group(:meal_id).select("meal_id, count(*) as number_of_picks")
scope :top_picks, counted_by_meal.order("number_of_picks DESC")
scope :top_pick, counted_by_meal.order("number_of_picks DESC").limit(1)
end
class Canteen < ActiveRecord::Base
attr_accessible :name
has_many :meals
has_many :meal_picks, through: :meals
def top_picks
#top_picks ||= meals.top_picks
end
def top_pick
#top_pick ||= top_picks.first
end
end
This allows me to do this:
c = Canteen.first
c.top_picks #Returns their meals ordered by the number of picks
c.top_pick #Returns the one with the top number of picks
Let's say that I wanted to order all meals by the number of picks. I could do this:
Meal.includes(:canteen).top_picks #Returns all meals for all canteens ordered by number of picks.
Meal.includes(:canteen).where("canteens.id = ?", some_id).top_picks #Top picks for a particular canteen
Meal.includes(:canteen).where("canteens.location = ?", some_location) #Return top picks for a canteens in a given location
Since we are using joins, grouping, and server-side counts, the whole collection need not be loaded to determine the pick count. This is a bit more flexible and probably more efficient.
canteen.meals.max {|m| m.meal_picked_count}

Resources