Rails - count query while joining three tables to one - ruby-on-rails

I have a fairly straightforward query that I can't seem to get right....
My model:
User - has many Friendships (with other users)
User - has many submissions
User - has many comments
User - has many votes
I need a count that represents
All current_user's friends, whose submissions, comments or votes created_at dates are > current_user.last_refresh_date
Right now, I am building up an array by iterating over friendships and adding all submissions, comment and votes. I then re-iterate this built-up array while comparing the dates to determine if the count should be incremented. Not the most ideal solution.
Edit:
#TobiasCohen
Efficient solution. Thanks!
Followup:
I wish to add yet one more count to the present query. I need to count all new comments & votes on the current_user.submissions that are not part of the original count (ie. not a friend).
Psuedo-code :
current_user.submissions.join(:comments, :votes, :friends).where('last_activity >? AND friend_id != ?', current_user.last_refresh_date, current_user.id).count
I can't quite get the query correct (new to complex queries via active record).
I was going to make it a separate query and then add it to the original count. Can it be absorbed into one query instead of two?

I think you'd get the best results by adding a cache column on User, let's call it :last_activity_at, then update this with an after_create callback on Submission, Comment and Vote.
class Submission < ActiveRecord::Base
belongs_to :user
after_create :update_user_last_activity_at
private
def update_user_last_activity_at
user.update_attribute :last_activity_at, Time.now
end
end
You could then fetch users simply with:
current_user.friends.where('last_activity_at > ?', current_user.last_refresh_date)

Related

Rails get associations on condition

I have a problem - I do not know how to get associated records only if condition is met.
I have Posts model and Comments, Post has_many :comments, Comment belongs_to :post.
Now, I want to retrieve All of the Posts, but only with specific comments (lets say with user_id = 1).
How can I achieve that?
Query like
Post.includes(:comments).where("comments.user_id = ?", "1") will retrieve only some Posts, I want all of them, but only with comments with user_id equal to 1.
I guess I should use LEFT JOIN of some sort, maybe something like
posts.joins("LEFT JOIN comments ON posts.id comments.post_id")
but I am not sure how to put condition restricting right table results.
It can be achieved as below
Post.includes(:comments).where("comments.user_id = ?", "1").references(:comments)
For more information go here

Rails query with group and count must include zeros

I know this question has been asked before but I have read many of the posts and tried applying them to my situation and I just can't get it working. I'm a beginner who could use some help. Here are my models:
class Action < ActiveRecord::Base
belongs_to :Student
belongs_to :Meeting
belongs_to :ClassSection
end
class Meeting < ActiveRecord::Base
belongs_to :floorplan
has_many :actions
belongs_to :class_section
end
I am trying to get a count of actions for a single student across multiple meetings, including meetings where he/she had 0 actions. So, if there are two rows in the meetings table, ID=1 and ID=2, and student 83 has a single action with meeting_id=1 in the actions table, my query, which will include where(:student_id=>83) somewhere, should return something like
1=>1
2=>0
I hope this makes sense. I know the answer involves outer joins but I am bad at implementing them in pure SQL and worse at doing them through ActiveRecord. FYI I am using MYSQL. Thanks in advance for whatever help you can provide.
Meeting
.joins('LEFT JOIN actions ON meeting_id = meetings.id')
.where(student_id: 83)
.group('meetings.id')
.count('actions.id')
Explanation
.joins is the left/outer join that you intuited that you needed. It means "include at least one row for every meeting, even if there are no actions".
.group needs to be on the meeting id, since this will always be present and different meetings should be grouped separately.
.count needs to be on actions id. COUNT does not count null records, so meetings with no actions will be counted as 0.
It's a little bit weird that for a count of actions you nee to start your query with Meeting, but that is necessary when you want to include 0 counts. Otherwise there would be no way for SQL to know what meetings were missing!
For reference, the generated SQL is:
SELECT
COUNT(actions.id) AS count_actions_id,
meetings.id AS meetings_id
FROM "meetings" LEFT JOIN actions ON meeting_id = meetings.id
GROUP BY meetings.id
I think this should work fine just by grouping
Meeting.where(student_id: 83).group(:actions).count
This will return the hash you want
{1=>1, 2=>0}

Ruby on Rails - Counting goals of a team in many matches

I've got a Match model and a Team model.
I want to count how many goals a Team scores during the league (so I have to sum all the scores of that team, in both home_matches and away_matches).
How can I do that? What columns should I put into the matches and teams database tables?
I'd assume your Match model looks something like this:
belongs_to :home_team, class_name:"Team"
belongs_to :away_team, class_name:"Team"
attr_accessible :home_goal_count, :away_goal_count
If so, you could add a method to extract the number of goals:
def goal_count
home_matches.sum(:home_goal_count) + away_matches.sum(:away_goal_count)
end
Since this could be expensive (especially if you do it often), you might just cache this value into the team model and use an after_save hook on the Match model (and, if matches ever get deleted, then an after_destroy hook as well):
after_save :update_team_goals
def update_team_goals
home_team.update_attribute(:goal_count_cache, home_team.goal_count)
away_team.update_attribute(:goal_count_cache, away_team.goal_count)
end
Since you want to do this for leagues, you probably want to add a belongs_to :league on the Match model, a league parameter to the goal_count method (and its query), and a goal_count_cache_league column if you want to cache the value (only cache the most recently changed with my suggested implementation, but tweak as needed).
You dont put that in any table. Theres a rule for databases: Dont ever store data in your database that could be calculated from other fields.
You can calcuate that easyly using this function:
def total_goals
self.home_matches.collect(&:home_goals).inject(&:+)+self.away_matches.collect(&:away_goals).inject(&:+)
end
that should do it for you. If you want the mathes filtered for a league you can use a scope for that.

Ruby on rails activerecord joins - select fields from multiple tables

models:
#StatusMessage model
class StatusMessage < ActiveRecord::Base
belongs_to :users
default_scope :order => "created_at DESC"
end
#User Model
class User < ActiveRecord::Base
has_many :status_messages
end
In controller I want to join these two tables and get fields from both table. for example I want email field from User and status field from StatusMessage. When I use :
#status = User.joins(:status_messages)
Or
#status = User.includes(:status_messages)
It gives me only the user table data.
How can I implement this requirement?
You need to use includes here. It preloads data so you won't have another SQL query when you do #user.status_messages.
And yes you can't really see the effect: you need to check your logs.
First of all, I don't think it is possible (and reasonable) what you want to do. The reason for that is that the relation between User and StatusMessage is 1:n, that means that each user could have 0 to n status messages. How should these multitudes of attributes be included in your user model?
I think that the method joints in class ActiceRecord has a different meaning, it is part of the query interface. See the question LEFT OUTER joins in Rails 3
There are similar questions on the net, here is what I have found that matches most:
Ruby on Rails: How to join two tables: Includes (translated for your example) in the user a primary_status_message, which is then materialized in the query for the user. But it is held in one attribute, and to access the attributes of the status_message, you have to do something like that: #user.primary_status_message.status
When you use #status = User.includes(:status_messages) then rails eagerley loads the data of all the tables.
My point is when you use this User.includes(:status_messages) it will loads the data of status_messages also but shows only users table data then if you want first user status_messages then you have to #user.first.status_messages

ActiveRecord Order the list of a model by associated model two levels

I have a rails app which has users who post recipes.People can post comments to the recipes with a rating.I want to sort the list of users(index action) by the average of the ratings received by the recipes posted by them.
I am already sorting the list of recpes by using this:
#courses = #recipes.joins(:comments).group('recipes.id').order('AVG(comments.rating)').reverse_order
How can I do something similar(or not) for users?
I have tried to do this using joins, group etc by reading the rails guides and apidock but still can't figure this out.
try this, not sure if it's completely right. Hope it's helpful to you.
User.joins(recipes: [:comments]).group('users.id').order('AVG(comments.rating) DESC')
update
my Models are User, Quiz, Question. The part confusing me a lot is your definition of User. Are the users who posted courses, and the users who comment in the same table or separate table? The relationship I mocked is this, which is I think is almost exactly same as your models:
User has_many quizzes, and user has_many questions.
Quiz has_many questions, and belongs_to user.
Questions belongs_to a user, and belongs_to a quizz.
in this case, if I do #users = User.joins(quizzes: [:questions]).group('users.id').order('AVG(questions.easy_count) DESC'), the result I get is, the result #users is a list of user who owns quizzes ordered by the easy_count(in your case rating) questions belongs to the corresponding quiz.
explanation of the code
User.joins(quizzes: [:questions]) simple gives you all rows of users who has quizzes(in your case should be courses) which has questions(in your case should be comment). consider the below example:
user1 has quiz1
quiz1 has question1 and question2
the result you get from User.joins(quizzes: [:questions]) will return you two duplicated rows of user1(because user1 is related to two questions through quiz1)
then group('users.id') is going to group the result from User.joins(quizzes: [:questions]) by user.id(you want to get user list)
so so far, what you get is very simple, it's a list of users who has quizzes and those quizzes need to have at least one question, I'm pretty sure it is the owner user of the quiz, not the owner user of the questions.
then at the end you sort this list by average easy_count of questions(in your case, rating of comments)

Resources