Ruby on Rails Query Count - sql

I'm developing a site where users can post opinions, as well as rate opinion up or down.
In turn, I need to find a way to count opinion that have been rated up or down.
I'm currently working on those which have been rated up. I have a many-to-many relationship between the following entities: Opinion and Rating.
The two entities are joined together by a table called OpinionRatings. Below is the query I have come up with so far.
#topUpSize = OpinionRating.find(
:first,
:select => "count(opinion_ratings.opinion_id) as count",
:joins => "inner join ratings on opinion_ratings.rating_id=ratings.id",
:group => "opinion_ratings.opinion_id",
:having => ["ratings.up=?", true]
)
The problem I have is I currently have two separate test opinions that have been rated up so the count that should be displayed is two, however, the count that is displayed is one. I'm not sure why this is happening.
Any help would be appreciated.

Related

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.

ActiveRecord - Retrieve one record for each association

Merchant has_many Shops
Shop belongs_to Merchant
i.e. One merchant (Starbucks) can have many shops locations.
I'm using Gecoder to get the nearby shops, e.g. #shops = Shop.near("Times Square").
I would like to return only 1 record for each merchant only. I.e. #shops only contain 1 Starbucks, 1 Subway, but is a collection.
Sorry I've been Googling and searching on SO to no avail. Perhaps I'm not using the right word to describe what I want. Thanks in advance.
To answer what you should be googling for, joined or combined queries within a scope will probably solve what you are looking to do. If you build a scope with :or logic combining queries, one each for each shop, limited to the first record, you should get what you are looking for.
I won't pretend that I understand Geocoder or advanced scopes enough to do this, but I found an example that shows this approach in another problem:
named_scope :or, lambda { |l, r| {
:conditions =>
"annotations.id IN (#{l.send(:construct_finder_sql,{:select => :id})}) or " +
"annotations.id IN (#{r.send(:construct_finder_sql,{:select => :id})})"
}}
This comes from this SO question: Combine two named scopes with OR (instead of AND)
Hope this helps you find the solution.
I googled a bit more and stumbled on group by for SQL.
If I have 4 shops belonging to 2 merchants near a location called "Raffles Place", within 1 kilometer.
Calling Shop.near("Raffles Place",1) returns 4 shops.
If I add a group to Shop.near("Raffles Place",1).group(:merchant_id), only 2 shops are returned.
This can be used with other conditions too, such as Shop.where(:featured=>true).group(:merchant_id) to only show 1 shop per featured merchant.

Rails3: left join aggregate count - how to calculate?

In my application Users register for Events, which belong to a Stream. The registrations are managed in the Registration model, which have a boolean field called 'attended'.
I'm trying to generate a leaderboard and need to know: the total number of registrations for each user, as well as a count for user registrations in each individual event stream.
I'm trying this (in User.rb):
# returns an array of users and their attendence count
def self.attendance_counts
User.all(
:select => "users.*, sum(attended) as attendance_count",
:joins => 'left join `registrations` ON registrations.user_id = users.id',
:group => 'registrations.user_id',
:order => 'attendance_count DESC'
)
end
The generated SQL works for just returning the total attended count for each user when I run it in the database, but all that gets returned is the User record in Rails.
I'm about to give up and hardcode a counter_cache for each stream (they are fairly fixed) into the User table, which gets manually updated whenever the attended attribute changes on a Registration model save.
Still, I'm really curious as to how to perform a query like this. It must come up all the time when calculating statistics and reports on records with relationships.
Your time and consideration is much appreciated. Thanks in advance.
Firstly as a couple of points on style and rails functions to help you with building DB queries.
1) You're better writing this as a scope rather than a method i.e.
scope attendance_counts, select("users.*, sum(attended) as attendance_count").joins(:registrations).group('registrations.user_id').order('attendance_count DESC')
2) It's better not to call all/find/first on the query you've built up until you actually need it (i.e. in the controller or view). That way if you decide to implement action / fragment caching later on the DB query won't get called if the cached action / fragment is served to the user.
3) Rails has a series of functions to help with aggregating db data. for example if you only wanted a user's id and the sum of attended you could use something like the following code:
Registrations.group(:user_id).sum(:attended)
Other functions include count, avg, minimum, maximum
Finally in answer to your question, rails will create an attribute for you to access the value of any custom fields you have in the select part of your query. e.g.
#users = User.attendance_counts
#users[0].attendance_count # The attendance count for the first user returned by the query

ActiveRecord counting with conditions over two tables

After watching, the latest RailsCasts with the include and joins information, I know I can do what I am doing in a much more efficient way. I have a very simple User model and a Status model. The User belongs to Status and the Status has many Users. I need to count how many users have a specific kind of status, this creates a new SQL count query for every single status and I know that this is not a good way to do it. It looks like this right now.
statuses = Status.all
statuses.each do |status|
status.users.count
end
I end up with 4 queries of:
SELECT count(*) AS count_all FROM "users" WHERE ("users".status_id = 1)
SELECT count(*) AS count_all FROM "users" WHERE ("users".status_id = 2)
It goes on like that for as many different statuses as exist in the database. The big problem is that now I need to filter by another association as well which is Organization. So I need to find the count for all users who have a certain status in a certain organization. This ends up quadrupling the amount of queries I am making and feels horrible. I'm not sure what kind of join I could use to cut down on this or what I could possibly do to fix this. Thanks for the help :)
Ok so I am answering my own question, just in case anyone has the same issue.
Status.all(:joins => :users,
:select => "statuses.*, count(users.id) as users_count",
:group => "statuses.id")
This returns every status that has users and the count of the users as users_count. In order to further refine the query and only count users that belong to a certain organization the query changes to this.
Status.all(:joins => :users,
:select => "statuses.*, count(users.id) as users_count",
:conditions => {:users => {:organization_id => ORG_ID_HERE}},
:group => "statuses.id")
I hope this helps anyone with the same issue and thanks to Eimantas, and Ryan Bates(RailCasts).
You can try plain SQL:
SELECT s.name,COUNT(u.id) AS users_count FROM statuses s, users u WHERE s.id=u.status_id GROUP BY s.id;
Out of interest, what does your Status model contain? Does it really need to be its own model? I'm guessing here, but you may want to consider implementing a finite state machine. There are a number of rails plugins that make it easy to implement an FSM e.g. acts_as_state_machine

Rails include with options

Is it possible to limit an ActiveRecord :include to say only pull one record?
Item.find(:all,
:include => [ :external_ratings, :photos => LIMIT 1 ])
I have a list of items, and each item has between 5 and 15 photos. I want to load a photo id into memory, but I don't need all of them. I just want to preview the first one.
Is there a way to do this with ActiveRecord?
I don't believe it is possible to do it directly, but as finding one record is only a single query, why not do it outside the include?
e.g
first_photo = Photo.find(records.first.photo_id)
After your main find
I'm not going to investigate the detailed query, but I'm thinking find_by_sql is in order for this particular case.