What is the difference between ModelName.all and ModelName.where('') - ruby-on-rails-3

I have the following relations:
User --has many--> Reminders --has many--> Payments
I get all the payments of a user as
payments = User.reminders.collect{|reminder| reminder.payments.between(from_date, to_date)}.flatten
and then
data = payments.select("SUM(amount) as total_payment, MONTH(payment_date) as month, YEAR(payment_date) as year").group("MONTH(payment_date), YEAR(payment_date)").map{|payment| [payment.total_payment, "#{payment.month}/#{payment.year}"]}
I was trying to run the above select and group by query on a dataset which failed with the following exception:
ArgumentError: wrong number of arguments (1 for 0)
from (irb):162:in `select'
The above query runs fine on Payments.where('') but fails on Payments.all or the dataset which I have obtained above.
On debugging I found that Payments.where('') is an ActiveRecord::Relation object whereas Payment.all is an Array.
An explanation would help me understand the concept and which way should I take. I don't want to run that group by query for each reminder.payments individually and then sum it up.

You've got an array of Payment objects at that point, rather than a Relation, which represents a query that hasn't been run yet. There's two ways to approach this problem. The first would be to build payments as a relation rather than an array. I would do this by adding a has_many :through relationship between Users and Payments.
class User < ActiveRecord::Base
has_many :reminders
has_many :payments, :through => :reminders
end
payments = user.payments.between(from_date, to_date)
data = payments.select....
The other way to do it would be to just pass the ids from your array to a where clause:
payments = user.reminders.collect {...}.flatten
data = Payment.where('id in (?)', payments).select...

Related

How to select parent by most recent child with Activerecord

I have a user model which has many subscriptions. I need to make two selections:
active users defined as user with a subscription in the last month
inactive users the ones that don't meet the (1) criteria
My subscription model has a simple scope .latest which is defined as ordered("created_at DESC").first.
To make selection (1) I use:
User.joins(:subscriptions).where("subscriptions.created_at > ?", 1.month.ago).distinct
This works, no problem there. However, I can't seem to define a working query for selection (2). Currently I use selection (1) and 'subtract' that from User.all to get the remaining users. This feels a bit like a hack.
The selection I need is:
all users whose most recent subscription was created more than 1 month ago
It's the most recent part of the query that has me stuck.
Any help appreciated.
Quick & dirty way: use complex SQL like this
Assume you are using auto incremental ID
User.select("users.*, MAX(subscriptions.id) as last_subscription_id")
.joins(:subscriptions)
.group("subscriptions.user_id")
.having("last_subscription_id = (select id from subscriptions where user_id=users.id and created_at < '2017-10-01 09:23:28.182475' order by id desc limit 1)")
Recommended way
Add last_subscription_id to users table and setup a belongs_to relationship user belongs_to last_subscription then do the joins normally. You need to update last_subscription_id of an user when new subscription for this user is created too.
Example: User class looks like this (I include has_many :subscriptions to show that we have 2 relations now)
class User < ActiveRecord::Base
has_many :subscriptions
belongs_to :last_subscription, class_name: 'Subscription', foreign_key: :last_subscription_id
end
And query will be
User.joins(:last_subscription).where("subscriptions.created_at < ?", 1.month.ago)
for the most recent part you can do this.
User.joins("LEFT JOIN subscriptions ON subscriptions.user_id = users.id").order("subscriptions.created_at DESC").select("subscriptions.created_at AS max_date").group_by(&:id).select{|key, value| value[0].max_date < 1.month.ago}.values.flatten

Count total number of objects in list ordered by the number of associated objects

I have two models
class User
has_many :subscriptions
end
and
class Subscription
belongs_to :user
end
one one of my pages I would like to display a list of all users ordered by the number of subscriptions each user has. I am not to good with sql queries but I think that
list = Users.all.joins(:subscriptions).group("user.id").order("count(subscriptions.id) DESC")
dose the job. Now to my problem, when I try to count the total number of objects in list, using list.count, I get a hash with user.id and subscription count, like this
{11 => 5,
8 => 7,
1 => 11,
...}
not the total number of users in list.. .count works fine if I have a list sorted by for example user name (which is in the user table). I would really like to use .count since it in a module for pagination thats in a gem but any ideas is great!
Thanks!
We can just use a single query to finish this:
User.joins("LEFT OUTER JOIN ( SELECT user_id, COUNT(*) as num_subscriptions
FROM subscriptions
GROUP BY user_id
) AS temp
ON temp.user_id = users.id")
.order("temp.num_subscriptions DESC")
Basically, my idea is to try to query the number of subscription for each user_id in the subquery, then join with User. I used LEFT OUTER JOIN, because there will be several users which don't have any subscriptions
Improve option: You can define a scope inside User, it would be more beautiful for later usage:
user.rb
class User < ActiveRecord::Base
has_many :subscriptions
scope :sorted_by_num_subscriptions, -> {
joins("LEFT OUTER JOIN ( SELECT user_id, COUNT(*) as num_subscriptions
FROM subscriptions
GROUP BY user_id
) AS temp
ON temp.user_id = users.id")
.order("temp.num_subscriptions DESC")
}
end
Then just use it:
User.sorted_by_num_subscriptions
When grouping, the count method changes it's behavior and indeed, instead of returning the total count of records, it returns a hash of the counts for each group (see the docs for more info). So what you get with list.count is simply a hash of the subscription counts for each user.
So, your query is correct and all you need is to sum up the individual counts in the groups. This can be done simply by:
total_count = list.count.values.sum
If it is the pagination code that calls just a bare count that makes the issue, usually the pagination code is able to accept a parameter with total count. For example, will_paginate accepts the total_entries parameter, so you should be able to pass it the total count like this:
list.paginate(page: 2, total_entries: list.count.values.sum)

ActiveRecord query to sum over joins

I want to get the sum of the receipt items that are in a particular budget (same title) and from the current query I'm getting to many record and obvious wrong sum of amounts from the receipt items.
My current attempt is looking like that in ActiveRecord (AR):
ReceiptItem.includes(donation: [:budgets]).joins(:donation, :receipt).where(budgets: {title: "Some title 2015"}).sum(:amount)
and my SQL attempt was looking like that (its also wrong):
-- want to test just the outcome its not actually not summing up the amounts
SELECT "receipt_items"."amount"
FROM
"receipt_items" INNER JOIN "donations" ON "donations"."id" = "receipt_items"."donation_id"
RIGHT JOIN "receipts" ON "receipts"."receipt_id" = "receipt_items"."receipt_id"
LEFT OUTER JOIN "budgets" ON "budgets"."donation_id" = "donations"."id"
WHERE "budgets"."title" = 'Some title 2015';
Why I'm getting double records although I've joined the tables and set also the condition?
Here is the ER modell to understand the problem.
And here's the AR Assoziations:
class Budget < ActiveRecord::Base
belongs_to :donation
class Donation < ActiveRecord::Base
has_many :receipt_items
has_many :budgets
class ReceiptItem < ActiveRecord::Base
belongs_to :donation
Because a budget can be linked to a reciept item multiple times, via different donations, it's appearing in the big join table multiple times, and thus being counted several times.
Let's try to think this through a step at a time. If you wanted to do it without worrying about eager loading, you would do:
Budget.where(title: "some title").all.collect(&:donation).collect(&:receipt_items).flatten.uniq.collect(&:amount).sum
is that right?
If so, you can tailor the eager loading to fit this chain of method calls:
Budget.where(title: "some title", include: {:donation => [:receipt_items]}).all.collect(&:donation).collect(&:receipt_items).uniq.collect(&:amount).sum
try that?

Rails: Many to one ( 0 - n ) , finding records

I've got tables items and cards where a card belongs to a user and a item may or may not have any cards for a given user.
The basic associations are set up as follows:
Class User
has_many :cards
Class Item
has_many :cards
Class Card
belongs_to :user
has_and_belongs_to_many :items
I've also created a join table, items_cards with the columns item_id and card_id. I'd like to make a query that tells me if there's a card for a given user/item. In pure SQL I can accomplish this pretty easily:
SELECT count(id)
FROM cards
JOIN items_cards
ON items_cards.card_id = cards.id
WHERE cards.user_id = ?
AND items_cards.item_id = ?
I'm looking for some guidance as to how I'd go about doing this via ActiveRecord. Thanks!
Assuming you have an Item in #item and a User in #user, this will return 'true' if a card exists for that user and that item:
Card.joins(:items).where('cards.user_id = :user_id and items.id = :item_id', :user_id => #user, :item_id => #item).exists?
Here's what's going on:
Card. - You're making a query about the Card model.
joins(:items) - Rails knows how to put together joins for the association types it supports (usually - at least). You're telling it to do whatever joins are required to allow you to query the associated items as well. This will, in this case, result in JOIN items_cards ON items_cards.card_id = cards.id JOIN items ON items_cards.item_id = items.id.
where('cards.user_id = :user_id and items.id = :item_id', :user_id => #user, :item_id => #item) - Your conditional, pretty much the same as in pure SQL. Rails will interpolate the values you specify with a colon (:user_id) using the values in the hash (:user_id => #user). If you give an ActiveRecord object as the value, Rails will automatically use the id of that object. Here, you're saying you only want results where the card belongs to the user you specify, and there is a row for the item you want.
.exists? - Loading ActiveRecord objects is inefficient, so if you only want to know if something exists, Rails can save some time and use a count based query (much like your SQL version). There's also a .count, which you could use instead if you wanted to have the query return the number of results, rather than true or false.

ActiveRecord: Adding condition to ON clause for includes

I have a model offers and another historical_offers, one offer has_many historical_offers.
Now I would like to eager load the historical_offers of one given day for a set of offers, if it exists. For this, I think I need to pass the day to the ON clause, not the WHERE clause, so that I get all offers, also when there is no historical_offer for the given day.
With
Offer.where(several_complex_conditions).includes(:historical_offers).where("historical_offers.day = ?", Date.today)
I would get
SELECT * FROM offers
LEFT OUTER JOIN historical_offers
ON offers.id = historical_offers.offer_id
WHERE day = '2012-11-09' AND ...
But I want to have the condition in the ON clause, not in the WHERE clause:
SELECT * FROM offers
LEFT OUTER JOIN historical_offers
ON offers.id = historical_offers.offer_id AND day = '2012-11-09'
WHERE ...
I guess I could alter the has_many definition with a lambda condition for a specific date, but how would I pass in a date then?
Alternatively I could write the joins mysqlf like this:
Offer.where(several_complex_conditions)
.joins(["historical_offers ON offers.id = historical_offers.offer_id AND day = ?", Date.today])
But how can I hook this up so that eager loading is done?
After a few hours headscratching and trying all sorts of ways to accomplish eager loading of a constrained set of associated records I came across #dbenhur's answer in this thread which works fine for me - however the condition isn't something I'm passing in (it's a date relative to Date.today). Basically it is creating an association with the conditions I wanted to put into the LEFT JOIN ON clause into the has_many condition.
has_many :prices, order: "rate_date"
has_many :future_valid_prices,
class_name: 'Price',
conditions: ['rate_date > ? and rate is not null', Date.today-7.days]
And then in my controller:
#property = current_agent.properties.includes(:future_valid_prices).find_by_id(params[:id])