I have models Category and Transactions.
Category has_many transactions, Transaction belongs_to category.
And i have scope for Category:
#relation = Category.all
#relation.joins(:transactions).where('transactions.created_at >= ?', 1.month.ago).
group('categories.id').order('SUM(transactions.debit_amount_cents) DESC')
It displays categories and sorts them by sum of transactions.debit_amount_cents
I want to display the amount for all its transactions along with each category.
Like:
id: 1,
name: "Category1",
all_amount: *some value* #like this
How can I improve this scope?
class Category < ApplicationRecord
# remember that scope is just a widely abused syntactic sugar
# for writing class methods
def self.with_recent_transactions
joins(:transactions)
.where('transactions.created_at >= ?', 1.month.ago)
.select(
'categories.*',
'SUM(transactions.debit_amount_cents) AS total_amount'
)
.order('total_amount DESC')
.group('categories.id')
end
end
If you select a column or an aggregate and give it an alias it will be available on the resulting model instances.
Category.with_recent_transactions.each do |category|
puts "#{category.name}: #{category.total_amount}"
end
For portability you can write this with Arel instead of SQL strings which avoids hardcoding stuff like table names:
class Category < ApplicationRecord
def self.with_recent_transactions
t = Transaction.arel_table
joins(:transactions)
.where(transactions: { created_at: Float::Infinity..1.month.ago })
.select(
arel_table[Arel.star]
t[:debit_amount_cents].sum.as('total_amount')
)
.order(total_amount: :desc) # use .order(t[:debit_amount_cents].sum) on Oracle
.group(:id) # categories.id on most adapters except TinyTDS
end
end
In Rails 6.1 (backported to 6.0x) you can use beginless ranges to create GTE conditions without Float::Infinity:
.where(transactions: { created_at: ..1.month.ago })
Related
I’m working with the below Rails Models.
Artist.rb
has_many: updates
Update.rb
belongs_to: artist
Updates has a popularity column (int 0-100)
I need to order artists by difference in popularity within the last 30 days. (last row - first row of updates in range)
I’ve made this work in controller by iterating over list of artists, calculate the difference in popularity, and save that value together with the artist id in a new array. Then sort that array by increase value and recreate the list of artists in the correct order. Issue is this causes a timeout error on my application as the iteration happens upon clicking “search”.
Method to calculate difference:
class Update < ApplicationRecord
belongs_to :artist
def self.pop_diff(a)
in_range = joins(:artist).where(artists: {name: a}).where(created_at: 30.days.ago.to_date..Time.now().to_date)
diff = in_range.last.popularity - in_range.first.popularity
return diff
end
end
Creating a new array in controller with correct ordering:
#artists = Artist.all
#ordering = Array.new
#artists.each do |a|
#ordering << {"artist" => a, "diff" => Update.pop_diff(a) }
end
#ordering = #ordering.sort_by { |k| k["diff"]}.reverse!
Does anyone know best practice on dealing with these types of situations?
These are the three paths I can think of:
Tweaking above solution to work more efficiently
Using a virtual column (attr_accessor) and storing the increase there. I’ve never done this before, not sure what’s possible
Build a back-end script that saves increase value in database on a daily base.
It would be most performant to do this in SQL
class Artist < ApplicationRecord
def self.get_popularity_extreme(direction = 'ASC', days_ago = 30)
<<-SQL
SELECT popularity
FROM updates
WHERE updates.created_at BETWEEN (DATEADD(DAY, -#{days_ago.to_i.abs}, NOW()), NOW())
ORDER BY updates.created_at #{direction.presence || 'ASC'}
LIMIT 1
SQL
end
def self.by_popularity_difference
joins(
<<-SQL
LEFT JOIN (
#{get_popularity_extreme}
) earliest_update ON updates.artist_id = artists.id
LEFT JOIN (
#{get_popularity_extreme('DESC')}
) latest_update ON updates.artist_id = artists.id
SQL
).
where('earliest_update.popularity IS NOT NULL').
where('latest_update.popularity IS NOT NULL').
select('artists.*', 'latest_update.popularity - earliest_update.popularity AS popularity_difference').
order('popularity_difference DESC')
end
end
Of course this is not the 'rails way'
The other option I would take would be to add a trigger to Update after_save to also set a column in the parent artist table
class Update < ApplicationRecord
belongs_to :artist
after_save :set_artist_difference
def self.pop_diff(a)
in_range = where(artist_id: a.id).where(created_at: 30.days.ago.to_date..Time.now().to_date).limit(1)
in_range.order(created_at: :desc).first.popularity - in_range.order(:created_at).first.popularity
end
def set_artist_difference
artist.update(difference: self.class.pop_diff(a))
end
end
the downside to this is if not every artist gets an update every day, the number won't be accurate
If you are to continue using your current solution, you should specify the order, explicit return is unnecessary, you shouldn't lookup an artist you already have, and the join isn't needed, (and also it's just wrong because your passing the whole artist, yet filtering it on 'name'):
class Update < ApplicationRecord
belongs_to :artist
def self.pop_diff(a)
in_range = where(artist_id: a.id).where(created_at: 30.days.ago.to_date..Time.now().to_date).limit(1)
in_range.order(created_at: :desc).first.popularity - in_range.order(:created_at).first.popularity
end
end
also instead of sorting the opposite direction then reversing, sort by negative diff:
#artists = Artist.all
#ordering = Array.new
#artists.find_in_batches do |batch|
batch.each do |a|
#ordering << {"artist" => a, "diff" => Update.pop_diff(a) }
end
end
#ordering = #ordering.sort_by { |k| -(k["diff"])}
Well, this approach you took has a problem with slow performance, in part because of the many queries you execute in the DB. Here's a simple way to do that (or very close to):
artists = Artist.all
pops =
artists.
includes(:updates).
where('updates.created_at' => 30.days.ago..Time.zone.now).
pluck(:id, 'updates.popularity').
group_by {|g| g.first}.
flat_map do |id, list|
diffs = list.map(&:second).compact
{
artist: artists.find { |artist| artist.id == id},
pops: diffs.last - diffs.first
}
end
# => [{:artist=>#<Artist id: 1, name: "1", created_at: "2018-07-10 05:44:29", updated_at: "2018-07-10 05:44:29">, :pops=>[10, 11, 1]}, {:artist=>#<Artist id: 2, name: "2", created_at: "2018-07-10 05:44:32", updated_at: "2018-07-10 05:44:32">, :pops=>[]}, {:artist=>#<Artist id: 3, name: "3", created_at: "2018-07-10 05:44:34", updated_at: "2018-07-10 05:44:34">, :pops=>[]}]
Much much more performant! But notice this is still not the most performant way to do the job. Still, it is very quick (although a little bit algebraic - you can improve somewhat) and uses a lot of the ruby and rails tricks to achieve the result you're looking for. Hope it helps! =)
I have the following structures in my Rails (4.2) e-commerce application:
class Product < ActiveRecord::Base
has_many :prices
def best_price(price_groups)
prices.where(price_group_id: price_groups).minimum(:value)
end
def default_price
prices.where(price_group_id: 1).first.value
end
end
class Price < ActiveRecord::Base
belongs_to :price_group
belongs_to :product
end
class PriceGroup < ActiveRecord::Base
has_and_belongs_to_many :users
has_many :prices
end
class User < ActiveRecord::Base
has_and_belongs_to_many :price_groups
end
Many users can be members of many price groups with many product price rows in each of them.
There is a default group with default prices for each product plus there can be many optional price groups with special (reduced) product prices for some users.
The problem is - this way I'm getting two queries per each product listed in my index page:
SELECT MIN("prices"."value") FROM "prices" WHERE "prices"."product_id" = $1 AND "prices"."price_group_id" IN (1, 2) [["product_id", 3]]
and
SELECT "prices".* FROM "prices" WHERE "prices"."product_id" = $1 AND "prices"."price_group_id" = $2 ORDER BY "prices"."id" ASC LIMIT 1 [["product_id", 3], ["price_group_id", 1]]
So, here is my question:
Is there some (easy) way to load everything at once? Like getting list of product objects with default and minimum price fields.
I understand how it can be done in a single SQL query, but I can't think of anything more natural, rails-activerecord-way.
**** Upd:
I ended up with
# I'm using Kaminari pagination
#products = Product.page(params[:page]).per(6)
product_ids = #products.map(&:id)
#best_prices=Price.where(product_id: product_ids, price_group_id: #user_price_groups).group(:product_id).minimum(:value)
#default_prices=Price.where(product_id: product_ids, price_group_id: 1).group(:product_id).minimum(:value)
These group queries produce hashes like { product_id => value }, ... so all I need is just using #best_prices[product.id] and so on in my views.
Thanks to everyone!
I agree with #Frederick Cheung, and I don't like a custom sql query, and I am not familiar with arel, so my solution will be:
products = Product.limit(10)
product_ids = products.map(&:id)
products_hash = products.reduce({}) do |hash, product|
hash[price.id] = {product: product} }
hash
end
best_prices = Price.select("MIN(value) AS min_price, product_id")
.where(product_id: product_ids, price_group_id: price_groups)
.group("product_id")
.reduce({}) do |hash, price|
hash[price.product_id] = { best_price: price.min_price }
hash
end
default_prices = Price.where(price_group_id: 1, product_id: product_ids)
.reduce({}) do |hash, price|
hash[price.product_id] = {default_price: price.value }
hash
end
# A hash like {
# 1: {product: <Product ...>, best_price: 12, default_price: 11},
# 2: {product: <Product ...>, best_price: 12, default_price: 11},
# 3: {product: <Product ...>, best_price: 12, default_price: 11}
# }
result = products_hash.deep_merge(best_prices).deep_merge(default_prices)
Still, three quests will be needed, not only one, but this solved the N+1 problem.
Just add a default scope and includes prices and your problem will be solved
class Product < ActiveRecord::Base
default_scope { includes(:prices) }
has_many :prices
def best_price(price_groups)
prices.where(price_group_id: price_groups).minimum(:value)
end
def default_price
prices.where(price_group_id: 1).first.value
end
end
I have four models in question: User, Product, Purchase, and WatchedProduct.
I am trying to get a list of products that meet one of the following criteria:
I created the product.
I bought the product.
The product is free, and I have "starred" or "watched" it.
This is what I have so far:
class User < ActiveRecord::Base
...
def special_products
product_ids = []
# products I created
my_products = Product.where(:user_id => self.id).pluck(:id)
# products I purchased
my_purchases = Purchase.where(:buyer_id => self.id).pluck(:product_id)
# free products I watched
my_watched = WatchedProduct.where(:user_id =>self.id).joins(:product).where(products: { price: 0 }).pluck(:product_id)
product_ids.append(my_products)
product_ids.append(my_purchases)
product_ids.append(my_watched)
product_ids # yields something like this => [[1, 2], [], [2]]
# i guess at this point i'd reduce the ids, then look them up again...
product_ids.flatten!
product_ids & product_ids
products = []
product_ids.each do |id|
products.append(Product.find(id))
end
products
end
end
What I am trying to do is get a list of Product models, not a list of IDs or a list of ActiveRecord Relations. I am very new to joins, but is there a way to do all of this in a single join instead of 3 queries, reduce, and re lookup?
First I like adding few scopes
class Product < ActiveRecord::Base
scope :free, -> { where(price: 0) }
scope :bought_or_created_by, lambda do |user_id|
where('user_id = :id OR buyer_id = :id', id: user_id)
end
end
class WatchedProduct < ActiveRecord::Base
scope :by_user, ->(user_id) { where(user_id: user_id) }
end
Then the queries
special_products = (Product.bought_or_created_by(id) + Product.joins(:watched_products).free.merge(WatchedProduct.by_user(id)).uniq
This will return an array of unique products using 2 queries.
Although i am not sure about your model associations but yes you can do all these things in single query somehow like this:
Product.joins([{:user => :watched_products}, :buyer, :purchases]).where(["users.id = :current_buyer && buyers.id = :current_buyer && watched_products.user_id = :current_buyer && purchases.buyer_id = :current_buyer, products.price = 0", :current_buyer => self.id])
I am assuming
Product belongs_to user and buyer
Product has_many purchases
User has_many watched_products
i have a country model and a travel note model. A country has many travel notes and a travel note belongs to one country.
class TravelNote < ActiveRecord::Base
default_scope { order(created_at: :desc) }
belongs_to :country
has_many :chapters
before_destroy { draft? }
validate :validates_uniqueness_of_draft, on: :create
enum status: { draft: 0, published: 1, archived: 2 }
enum advice_against: { no: 0, general: 1, tourists: 2 }
scope :country, ->(country_id) { where(country_id: country_id) }
# further methods omitted...
end
class Country < ActiveRecord::Base
default_scope { order(:iso_code) }
has_many :travel_notes
end
in app/controllers/countries_controller.rb:
class CountriesController < ApplicationController
def index
#countries = Country.includes(:travel_notes)
end
# rest of the class omitted...
end
in app/views/countries/index.html.haml:
#countries.each do |country|
%tr
%td= link_to country.name_de, country_travel_notes_path(country)
%td= TravelNote.published.country(country.id).first.try :published_at
because of performance reason i want to remove TravelNote.published.country(country.id).first.try :published_at so that there is not hundreds of database queries anymore instead just an array of an equivalent sql query:
select * from countries
left join travel_notes
on countries.id=travel_notes.country_id
how can i achieve it?
Apparently you are trying to eager-load the "travel_notes" associated to the country:
Country.includes(:travel_notes).where(travel_notes: { status: 1} )
so your code will be:
class CountriesController < ApplicationController
def index
#countries = Country.includes(:travel_notes).where(travel_notes: { status: 1} )
end
# rest of the class omitted...
end
#countries.each do |country|
%tr
%td= link_to country.name_de, country_travel_notes_path(country)
%td= country.travel_notes.first.published_at
You could write a custom scope to include only the published notes.
something like
scope :include_published, -> { proc do |_|
joins("LEFT OUTER JOIN (
SELECT b.*
FROM travel_notes b
WHERE published = 1
GROUP BY b.country_id
) notes_select ON notes_select.country_id = countries.id"
).select("countries.*").select("#{insert what attributes you want to include }")
end.call(self, counted_model) }
You have to include the attributes you want from the note in the second select clause then they will be included in the country active record result as methods with the same name.
The SQL-query can be written prettier, I just threw something together...
I use a similar technique in my project but in order to include counts of associated objects.
In my rails app I have a table Designs which are essentially the posts. Users can like these Designs (using the thumbs_up gem). This table is called Votes. On user's profile pages, I'm showing all of the Designs that they have liked (or voted for). In the user model, I have a method:
def favorites
Design.joins(:votes).where('voter_id = ?', self.id).where('voteable_type = ?', 'Design').where('vote = ?', true)
end
Then in the user's controller to call these designs I have
def show
#designs = #user.favorites
end
This shows all of the designs they have liked, but it's in order of when the Design was created, not when the Vote was created. The Vote table has a created_at column so I know I can sort these Designs based on when they liked them.
I tried this with no luck
def favorites
results = Design.joins(:votes).where('voter_id = ?', self.id).where('voteable_type = ?', 'Design').where('vote = ?', true)
results.sort! {|t1, t2| t2.vote.created_at <=> t1.vote.created_at}
end
How can I sort the Designs based on when that user liked them.
the Vote table has these columns
vote: boolean
voteable_id: integer
voteable_type: string
voter_id: integer
created_at: date
updated_at: date
thanks!
The prior answer doesn't work because scopes work at the Class level, so when you use it on a instance of User, it assumes favorites is a instance method or association.
What I recommend you do is use the Vote model, and reference the belongs_to Design association on Vote:
def show
#votes = #user.votes.includes(:design).order(created_at: :asc)
#designs = #votes.collect {|v| v.design }
end
You could feel free to move that to the User model in place of your favorites method like so:
def favorites
#favorites ||= self.votes.includes(:design).order(created_at: :asc).collect {|v| v.design }
end
UPDATE
Since you are using this thumbs_up gem, the following will work:
In Action
def show
#designs = #user.votes.where(voteable_type: 'Design').order(created_at: :asc).collect {|v| v.voteable}
end
Or Method
def favorites
#favorites ||= self.votes.where(voteable_type: 'Design').order(created_at: :asc).collect {|v| v.voteable}
end
https://github.com/bouchard/thumbs_up
I think you'll be able to use a scope for this - I'd use a scope on the user model to communicate with the votes model from your user instance:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :votes, as :voteable
has_many :designs, through: :votes
scope :favorites, -> { joins(:votes).where('voter_id = ? AND votable_type = ? AND vote = ?', id, 'Design', true) }
scope :latest, -> { order(created_at: :asc) }
scope :latest_favorites, -> { favorites.order(created_at: :asc) }
end
This will allow you to call:
#user.favorites.latest
#user.latest_favorites