Active Record LIMIT within GROUP_BY - sql

SCENARIO
I have a table full of posts with a users table.
I want to be able to fetch all the posts and group them by users but I want to set a limit of say 10 per user.
class Post < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :posts
end
# I thought this might work but it just grabs the first 10 posts and groups them
Post.find(:all, :limit=>10).group_by(&:user)
Any thoughts? Do I have to write custom SQL for or can Active Record do this?

Something like?
Post.group(:user_id).limit(10)

Post.group(:user_id).limit(10)
group_by is not a query method, but rather a method of Enumerable.
In your code, Post.find(:all, :limit => 10) is turned into an Array before being passed to group_by. The method above chains query methods together and only converts them to an Array when you need to use them.
ActiveRecord handles the whole thing. The above method translates to
SELECT `posts`.* FROM `posts` GROUP BY user_id LIMIT 10

The only way I know to grab the recent 10 posts per user would require a nested sub-query (which can have performance issues) or postgres-style lateral join. Fairly confident this cannot be accomplished with only active-record and requires writing custom SQL, which you've indicated you want to avoid.
As an alternative that could be accomplished without custom SQL, you could list each user and their posts within a time window (e.g. past month or year) with the following:
class User < ActiveRecord::Base
has_many :recent_posts, -> { where(posts: {created_at 1.month.ago..Time.now}) }, class_name: 'Post'
end
User.includes(:recent_posts).each do |user|
user.recent_posts
end
Which would not execute a SQL query for each user and would therefore be relatively performant compared with doing it purely in ruby.

Related

Rails 4 "where"-query with "has_many belongs_to"-relationship, searching with specific count

This should be a simple query, but I'm having problems getting the Rails syntax right. I'm using Rails 4.1.1 and Postgresql(9.3). I have a model User, and model Company. User has one company, and Company has many users. I'm trying to find all companies that have more than 5 users.
class Company < ActiveRecord::Base
has_many :users, dependent: :destroy
...
class User < ActiveRecord::Base
belongs_to :company
...
Question is similar to this: Find all records which have a count of an association greater than zero
If I try similar solution as mentioned above:
Company.joins(:users).group("company.id").having("count(users.id)>5")
It gives me an error:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "company"
LINE 1: ... "users"."company_id" = "companies"."id" GROUP BY company.id...
I've tried several different queries for getting the result, but I've failed to do so. I could use SQL, but it seems dumb as this should be doable easily with ActiveRecord.
Thanks for all the replies :)
Should use "companies.id" instead of "company.id".
Company.joins(:users).group("companies.id").having("count(users.id)>5")
And if you want to get company with 0 users, you have to use LEFT JOIN:
Company.joins('LEFT JOIN users ON companies.id = users.company_id').group("companies.id").having("count(users.id) = 0")
The query xdazz provided works well for when I'm trying to look for companies that have more than 0 users (basicly what I asked in initial post). I found two ways to do the search 0 users. One is the way noted above:
Company.joins('LEFT JOIN users ON companies.id = users.company_id')
.group("companies.id").having("count(users.id) = 0")
However with help of Want to find records with no associated records in Rails 3 this is another way to do it:
Company.includes(:users).where(:users => {company_id=>nil})

How can I order a Rails collection by the attributes of associated records?

I have three models: teachers, students, and assignments
class Teacher < ActiveRecord::Base
has_many :assignments
has_many :students, through: assignments, uniq: true
end
For any given teacher, I would like to retrieve a list of unique students - simple enough, I just call:
teacher.students
However, I would like to order that list of students based on who has submitted assignments recently. Specifically, I would like the student with the most recently updated assignment to appear first, and so on.
I am stuck on the following code, which is not working:
teacher.students.group("assignments.student_id").order("MAX(assignments.updated_at) DESC")
Any suggestions?
It appears that my original query was correct:
teacher.students.group("assignments.student_id").order("MAX(assignments.updated_at) DESC")
I had thought it wasn't working because I had written a bad RSpec test that was failing because I wasn't handling the timestamps well. Once I used Timecop to handle the timestamps properly, the test passed.
Sorry about that.
Not really tested, but I believe
teacher.students.includes(:assignments).order('assignments.updated_at')
should work

How to select 2 or more models back in a where clause

In SQL you can have SELECT x,y FROM ...
but if I use where, the sql generated will only select from the models which it is acting upon.
So for example:
user has_many :posts
post belongs_to :user
Post.includes(:users).where('created_at < ?' 1.day.ago) will have
SELECT "posts".* FROM "posts" WHERE (created_at > '2013-03-06 07:37:09.010916')
but how do I SELECT the post and the users so it will return the posts users as well?
UPDATE: As Saurabh pointed out, the loaded association should be :user and not :users
Includes is used to eager load association records, meaning that given the following
#posts = Post.includes(:user)
#posts.each do |post|
post.user
end
Only 2 queries will be made to the database, one for fetching the posts and another for fetching all the users that is associated to #posts. Rails manages the associations for you. Simple.
If you want to get a certain column value from a different table, try the following
#posts = Post.joins(:user).select('posts.*, users.name AS user_name')
#posts.first.user_name # will give you the name of the first user associated to the post
First, your associations are wrong. You can't do Post.includes(:users) when you say: post belongs_to :user.
The correct way is -
Post.includes(:user) for the belongs_to associations.
Second, you can select a value from different table by doing so:
Post.includes(:user).where(:user => {:name => params[:name]})
The above query will give you all the posts whose user name is params[:name] where :name is the field from the user table.

Joining a "has many through" association using ActiveRecord

Working with Exported Data from API
I'm building a leaderboard that displays the Team.name of each team as well as the users who have picked that particular team as their favorites. I'm also populating another attribute Favorite.points; to display the users with the most points accumulated for that respective team.
Here are the models I'm working with:
Favorite.rb
class Favorite < ActiveRecord::Base
belongs_to :users
belongs_to :teams
end
Team.rb
class Team < ActiveRecord::Base
has_many :favorites
has_many :users, :through => :favorites
end
User.rb
class User < ActiveRecord::Base
has_many :favorites
has_many :teams, :through => :favorites
end
To start this process, I'm trying to match up the id's that are common between Team.external_id and Favorite.team_id (the same is the case for User.external_id => Favorites.user_id). I can use Team.find_all_by_external_id(3333) to get the IDs of all Team objects that have an external_id of '3333'and the same goes for Favorite.find_all_by_team_id.
What's the next best step for me to obtain/show the data I'm looking for? Is a SQL join clause best? Or is it better to write if statements matching up values and iterating through the JSON arrays?
Any help is appreciated, thanks!
This will get you all the favorites whose team_id matches the external_id attribute of a row in the teams table, for a specific team (here, the team with id 3333):
Favorite.joins("left outer join teams on teams.external_id = favorites.team_id")\
.where('team_id' => 3333)
The tricky thing here, as I mentioned in my comments, is that you are going entirely against the grain of rails associations when you match the external id on the Team model (an attribute which you have created) to the team_id on the Favorite model (which is used throughout rails to get and assign associations).
You will see the problem as soon as you try to actually get the team for the favorite you find in the above join:
f = Favorite.joins("left outer join teams on teams.external_id = favorites.team_id")\
.where('team_id' => 3333).first
=> #<Favorite id: 1, user_id: nil, team_id: 3333, points: nil, created_at: ... >
f.team
Team Load (0.3ms) SELECT "teams".* FROM "teams" WHERE "teams"."id" = 3333 LIMIT 1
=> nil
What's going on here? If you look closely at the query, you'll see that rails is selecting teams whose id is 3333. Note that it is not looking for teams whose external id is 3333, which is what you want.
The fundamental problem is that you are trying to use external ids (ids specific to your API) for associations, which won't work. And indeed, there is no reason to do it this way.
Instead, try this:
Favorite.joins(:team).where('teams.external_id = 3333')
This will get you all favorites whose teams have the external id 3333. Note that Rails will do this by joining on teams.id = favorites.team_id, then filtering by teams.external_id:
SELECT "favorites".* FROM "favorites" INNER JOIN "teams"
ON "teams"."id" = "favorites"."team_id" WHERE (teams.external_id = 3333)
You can do the same thing the other way around:
Team.joins(:favorites).where('teams.external_id = 3333')
which will generate the SQL:
SELECT "teams".* FROM "teams" INNER JOIN "favorites"
ON "favorites"."team_id" = "teams"."id" WHERE (teams.external_id = 3333)
Note again that it is the id that is being used in the join, not the external id. This is the right way to do this: use the conventional id for your associations, and then just filter wherever necessary by your (custom-defined, API-specific) external id.
Hope that helps!
UPDATE:
From the comments, it seems that the team_id on your Favorite model is being defined from the API data, which means that the id corresponds to the external_id of your Team model. This is a bad idea: in rails, the foreign key <model name>_id (team_id, user_id, etc.) has a specific meaning: the id is understood to map to the id field of the corresponding associated model (Team).
To get your associations to work, you need to use ids (not external ids) for associations everywhere (with your User model as well). To do this, you need to translate associations defined in the API to ids in the rails app. When you add a favorite from the API, find the Team id corresponding to the API team id.
external_team_id = ... # get external team id from API JSON data
favorite.team_id = Team.find_by_external_id(external_team_id).id
So you are assigning the id of the team with a given external id. You need to query the DB for each favorite you load from the API, which is potentially costly performance-wise, but since you only do it once it's not a big deal.

How can I convert this SQL into ActiveRecord/Arel syntax?

I'm running into problems when I move this SQL around to different database adapters. I want to convert it into something more flexible. Here it is:
Foo.includes([{ :group => :memberships }, :phone]).
where('("memberships"."user_id" = ? AND "memberships"."owner" = ?)
OR "phone"."user_id" = ?', user.id, true, user.id)
user is the current user (this query is within a CanCan ability file). The idea is to query all foo's where the current user is the owner of the foo's group, OR the foo's phone is owned by the current user.
I have tried many different queries, but I can't see to get the OR syntax right. Because this is in a CanCan ability, it is not possible to combine two queries, it all needs to be in one large scope like the one above. Is this possible to do?
Group membership is done through the join table memberships, the owner is designated with the owner field.
class Foo
belongs_to :group
belongs_to :phone
end