I'm trying to figure out how to produce a certain query, using ActiveRecord.
I have the following models
class Activity < ActiveRecord::Base
attr_accessible :limit, ...
has_many :employees
end
class User < ActiveRecord::Base
belongs_to :activity
end
Each activity has a limit, that is to say, an integer attribute containing the maximum amount of users who may belong to it.
I'm looking for a way to select all activities that have spots available, i.e. where the number of users is smaller than that limit.
Any ideas?
Thanks
I think that the SQL syntax to aim for would be:
select *
from activities
where activities.limit > (
select count(*)
from users
where users.activity_id = activities.id)
In Rails-speak ...
Activity.where("activities.limit > (select count(*) from users where users.activity_id = activities.id)")
Not sure whether the column name "limit" is going to give you problems as it's a reserved word. You might have to quote it in the SQL.
I'd also seriously consider a counter cache for users on the activities table, which would make this perform much better. Some databases would support a partial index only for those rows where the users counter cache < limit.
Activity.all.select{|activity| activity.users.length < activity.limit }
Related
I want to show a line chart on the admin page (with chartkick) with the incremental number of scores related to their earliest export date.
I have the following models:
# score.rb
class Score < ApplicationRecord
has_and_belongs_to_many :export_orders, join_table: :scores_export_orders
end
# export_order.rb
class ExportOrder < ApplicationRecord
has_and_belongs_to_many :scores, join_table: :scores_export_orders
end
How do I select, for each Score having at least one ExportOrder, the corresponding ExportOrder with the earliest created_at (in date only format)?
I had a look at this, but my situation has a HABTM relationship instead of a simple has_many.
I tried this code, to get at least a mapping between oldest export date and number of scores:
sql = "
SELECT
COUNT(DISTINCT scores.id), MIN(export_orders.created_at::date)
FROM
scores
INNER JOIN
scores_export_orders
ON
scores.id = scores_export_orders.score_id
INNER JOIN
export_orders
ON
export_orders.id = scores_export_orders.export_order_id
GROUP BY
export_orders.created_at::date
".split("\n").join(' ')
query = ActiveRecord::Base.connection.execute(sql)
query.map { |v| [v['count'], v['min']] }
but the total number of scores is greater than all scores having an export date.
Any ideas?
Try:
class Score < ApplicationRecord
has_and_belongs_to_many :export_orders, join_table: :scores_export_orders
def earliest_export_date
export_orders.pluck(&:created_at).min
end
end
This will let you call #score.earliest_export_date, which should return the value you want.
I also think it's the most performant way to do it in ruby, although someone may correct me on that.
The following has better performance than Mark's solution since it relies on pure SQL. Basically, the GROUP BY clause required grouping by scores_export_orders.score_id rather than export_orders.created_at:
sql = "
SELECT
COUNT(DISTINCT scores_export_orders.score_id), MIN(export_orders.created_at::date)
INNER JOIN
scores_export_orders
INNER JOIN
export_orders
ON
export_orders.id = scores_export_orders.export_order_id
GROUP BY
scores_export_orders.score_id
".split("\n").join(' ')
query = ActiveRecord::Base.connection.execute(sql)
query.map { |v| [v['count'], v['min']] }
I couldn't find an exact equivalent in ActiveRecord instructions (all of such attempts were giving me strange results), so executing the SQL will also do the trick.
I am building a Rails 4.2.7.1 which uses Postgres database and I need to write a feature for certain group of users.
class User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
end
I need to select users from certain location who have exactly one payment and I also need to be able to pick users whose payment created_at attribute is exactly x
I tried
location.users
.without_deleted
.where(num_payments: 1)
.joins(:payments)
.where('payments.user_id = users.id').order('created_at
DESC').where("payments.created_at < ?", Date.today).group('users.id')
but it did not give me expected results.
Thanks!
You should start from User since this is what you want at end, and take joins with payments since you want to query it along.
User.joins(:payments)
.where(location_id: location.id, num_payments: 1)
.where(payments: { created_at: Date.today })
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
I am trying to find the collection of 'underpaid' events in our system. We are running Rails 3.2 using a Postgres database.
The data structure is as follows.
class Event < ActiveRecord::Base
has_many :charges
has_many :transactions
end
class Charge < ActiveRecord::Base
belongs_to :event
end
class Transaction < ActiveRecord::Base
belongs_to :event
end
Underpaid events are defined as those where the total charges are greater than total transactions.
sum(charges.total) > sum(transactions.total)
My SQL skills are poor and I have been trying to execute this using ActiveRecord. This is my latest attempt but it is not bringing back the right collection. In particular it seems to include fully paid events where there was more than one transaction.
Event.joins(:charges,:transactions).group('charges.event_id, transactions.event_id').having("sum(charges.total) > sum(transactions.total)")
Is it possible to achieve this in ActiveRecord and if so, how can I go about it?
Hey I think that in SQL it should be like that
select * from events where
(select sum(charges.total) from charges where charges.event_id = events.id) >
(select sum(transactions.total) from transactions where
transactions.event_id = events.id)
so for now you can build scope like
scope :unpaid, find_by_sql("select * from events where
(select sum(charges.total) from charges where charges.event_id = events.id) >
(select sum(transactions.total) from transactions where
transactions.event_id = events.id)")
I hope it will help!
I'm trying to do some fairly complicated record sorting that I was having a bit of trouble with. I have three models:
class User < ActiveRecord::Base
has_many :registers
has_many :results, :through => :registers
#Find all the Users that exist as registrants for a tournament
scope :with_tournament_entrees, :include => :registers, :conditions => "registers.id IS NOT NULL"
end
Register
class Register < ActiveRecord::Base
belongs_to :user
has_many :results
end
Result
class Result < ActiveRecord::Base
belongs_to :register
end
Now on a Tournament result page I list all users by their total wins (wins is calculated through the results table). First thing first I find all users who have entered a tournament with the query:
User.with_tournament_entrees
With this I can simply loop through the returned users and query each individual record with the following to retrieve each users "Total Wins":
user.results.where("win = true").count()
However I would also like to take this a step further and order all of the users by their "Total Wins", and this is the best I could come up with:
User.with_tournament_entrees.select('SELECT *,
(SELECT count(*)
FROM results
INNER JOIN "registers"
ON "results"."register_id" = "registers"."id"
WHERE "registers"."user_id" = "users.id"
AND (win = true)
) AS total_wins
FROM users ORDER BY total_wins DESC')
I think it's close, but it doesn't actually order by the total_wins in descending order as I instruct it to. I'm using a PostgreSQL database.
Edit:
There's actually three selects taking place, the first occurs on User.with_tournament_entries which just performs a quick filter on the User table. If I ignore that and try
SELECT *, (SELECT count(*) FROM results INNER JOIN "registers" ON "results"."register_id" = "registers"."id" WHERE "registers"."user_id" = "users.id" AND (win = true)) AS total_wins FROM users ORDER BY total_wins DESC;
it fails in both PSQL and the ERB console. I get the error message:
PGError: ERROR: column "users.id" does not exist
I think this happens because the inner-select occurs before the outer-select so it doesn't have access to the user id before hand. Not sure how to give it access to all user ids before than inner select occurs but this isn't an issue when I do User.with_tournament_entires followed by the query.
In your SQL, "users.id" is quoted wrong -- it's telling Postgres to look for a column named, literally, "users.id".
It should be "users"."id", or, just users.id (you only need to quote it if you have a table/column name that conflicts with a postgres keyword, or have punctuation or something else unusual).