Consider I have 3 tables: Users (has_many) -> Websites (has_many) -> Visits.
How would one find out the total number of Visits each user has without writing plain sql code?
I have an idea of which I'm not very proud since I let rails do the math instead of mysql:
count = 0
user.websites.each |website|
count += website.visits.count()
I'm new with rails and maybe i'm missing some docs. Is it possible to find out that count just from the query builder?
you can define that the User has many Visits through the Websites like this:
class User < ActiveRecord::Base
has_many :websites
has_many :visits, :through => :websites
end
now, if you do
some_user.visits.count
this sql is executed:
(0.4ms) SELECT COUNT(*) FROM "visits" INNER JOIN "websites" ON "visits"."website_id" = "websites"."id" WHERE "websites"."user_id" = 1
=> 8
That is, ActiveRecord creates the SQL query for you.
Related
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'm doing a simple blog application - There are posts, which have many tags through a posts_tags table (my models are below). What I have implemented is if a user clicks a tag, it will show just the posts with that tag. What I want is for the user to them be able to select another tag, and it will filter to only the posts that have both of those tags, then a third, then a fourth, etc. I'm having difficulty making the active record query - especially dynamically. The closest I've gotten is listed below - however its in pure SQL and I would like to at least have it in ActiveRecord Rubyland syntax even with the complexity it contains.
Also, the "having count 2" does not work, its saying that "count" does not exist and even if I assign it a name. However, it is outputting in my table (the idea behind count is that if it contains a number that is as much as how many tags we are searching for, then theoretically/ideally it has all the tags)
My current test SQL query
select posts_tags.post_id,count(*) from posts_tags where tag_id=1 or tag_id=3 group by post_id ### having count=2
The output from the test SQL (I know it doesnt contain much but just with some simple seed data).
post_id | count
---------+-------
1 | 2
2 | 1
My Models:
/post.rb
class Post < ActiveRecord::Base
has_many :posts_tags
has_many :tags, :through => :posts_tags
end
/tag.rb
class Tag < ActiveRecord::Base
has_many :posts_tags
has_many :posts, :through => :posts_tags
end
/poststag.rb
class PostsTag < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
Give a try to:
Post.joins(:tags).where(tags: {id: [1, 3]}).select("posts.id, count(*)").group("posts.id").having("count(*) > 2")
I think "count = 2" is not correct. It should be "count(*) = 2". Your query then will be
select post_id,count(post_id)
from posts_tags
where tag_id = 1 or tag_id = 3
group by post_id
having count(post_id) = 2
In general you want to stay away from writing raw sql when using rails. Active Record has great helper methods to make your sql more readable and maintainable.
If you only have a few tags you can create scopes for each of them (http://guides.rubyonrails.org/active_record_querying.html#scopes)
Since people are clicking on tags one at a time you could just query for each tag and then use the & operator on the arrays. Because you have already requested the exact same set of data from the database the query results should be cached meaning you are only hitting the db for the newest query.
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 }
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.
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).