First 10 items in a many-to-many relationship in RoR - ruby-on-rails

I have a database with two tables: tags and items.
Each Item has a score, the highest scoring items being the most popular. There is a many-to-many relationship between tags and items.
Getting all items belonging to a tag is easy. (= tag.items) But how do I retrieve the 10 most popular items belonging to this tag?
So in fact I need the ruby equivalent of
SELECT * from items INNER JOIN item_tags ON items.id = item_tags.item WHERE item_tags.tag = :tagid ORDER BY items.score DESC LIMIT 10
Since a tag might have a lot of items, I prefer to let the database do this work instead of retrieving all items and then filtering them manually. (and if there is a faster way to perform this operation, it is certainly welcome!)

Assuming the following setup:
class Item < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :items
end
You should be able to do:
#tag = Tag.find(PARAMS)
#tag.items.find(:all, :order => "items.score DESC", :limit => 10)
If you want to make it even slicker, add this line to your Item class:
named_scope :popular, :order => "items.score DESC", :limit => 10
You can then call
#tag.items.popular

Related

ActiveRecord Association select counts for included records

Example
class User
has_many :tickets
end
I want to create association which contains logic of count tickets of user and use it in includes (user has_one ticket_count)
Users.includes(:tickets_count)
I tried
has_one :tickets_count, :select => "COUNT(*) as tickets_count,tickets.user_id " ,:class_name => 'Ticket', :group => "tickets.user_id", :readonly => true
User.includes(:tickets_count)
ArgumentError: Unknown key: group
In this case association query in include should use count with group by ...
How can I implement this using rails?
Update
I can't change table structure
I want AR generate 1 query for collection of users with includes
Update2
I know SQL an I know how to select this with joins, but my question is now like "How to get data" . My question is about building association which I can use in includes. Thanks
Update3
I tried create association created like user has_one ticket_count , but
looks like has_one doesn't support association extensions
has_one doesn't support :group option
has_one doesn't support finder_sql
Try this:
class User
has_one :tickets_count, :class_name => 'Ticket',
:select => "user_id, tickets_count",
:finder_sql => '
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
WHERE b.user_id = #{id}
GROUP BY b.user_id
'
end
Edit:
It looks like the has_one association does not support the finder_sql option.
You can easily achieve what you want by using a combination of scope/class methods
class User < ActiveRecord::Base
def self.include_ticket_counts
joins(
%{
LEFT OUTER JOIN (
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
GROUP BY b.user_id
) a ON a.user_id = users.id
}
).select("users.*, COALESCE(a.tickets_count, 0) AS tickets_count")
end
end
Now
User.include_ticket_counts.where(:id => [1,2,3]).each do |user|
p user.tickets_count
end
This solution has performance implications if you have millions of rows in the tickets table. You should consider filtering the JOIN result set by providing WHERE to the inner query.
You can simply use for a particular user:
user.tickets.count
Or if you want this value automatically cached by Rails.
Declare a counter_cache => true option in the other side of the association
class ticket
belongs_to :user, :counter_cache => true
end
You also need a column in you user table named tickets_count.
With this each time you add a new tickets to a user rails will update this column so when you ftech your user record you can simply accs this column to get the ticket count without additional query.
Not pretty, but it works:
users = User.joins("LEFT JOIN tickets ON users.id = tickets.user_id").select("users.*, count(tickets.id) as ticket_count").group("users.id")
users.first.ticket_count
What about adding a method in the User model that does the query?
You wouldn't be modifying the table structure, or you can't modify that either?
How about adding a subselect scope to ApplicationRecord:
scope :subselect,
lambda { |aggregate_fn, as:, from:|
query = self.klass
.select(aggregate_fn)
.from("#{self.table_name} _#{self.table_name}")
.where("_#{self.table_name}.id = #{self.table_name}.id")
.joins(from)
select("(#{query.to_sql}) AS #{as}")
}
Then, one might use the following query:
users = User.select('users.*').subselect('COUNT(*)', as: :tickets_count, from: :tickets)
users.first.ticket_count
# => 5

Rails HABTM joining with another condition

I am trying to get a list, and I will use books as an example.
class Book < ActiveRecord::Base
belongs_to :type
has_and_belongs_to_many :genres
end
class Genre < ActiveRecord::Base
has_and_belongs_to_many :books
end
So in this example I want to show a list of all Genres, but it the first column should be the type. So, if say a genre is "Space", the types could be "Non-fiction" and "Fiction", and it would show:
Type Genre
Fiction Space
Non-fiction Space
The Genre table has only "id", "name", and "description", the join table genres_books has "genre_id" and "book_id", and the Book table has "type_id" and "id". I am having trouble getting this to work however.
I know the sql code I would need which would be:
SELECT distinct genres.name, books.type_id FROM `genres` INNER JOIN genres_books ON genres.id = genres_books.genre_id INNER JOIN books ON genres_books.book_id = books.id order by genres.name
and I found I could do
#genre = Genre.all
#genre.each do |genre|
#type = genre.book.find(:all, :select => 'type_id', :group => 'type_id')
#type.each do |type|
and this would let me see the type along with each genre and print them out, but I couldn't really work with them all at once. I think what would be ideal is if at the Genre.all statement I could somehow group them there so I can keep the genre/type combinations together and work with them further down the road. I was trying to do something along the lines of:
#genres = Genre.find(:all, :include => :books, :select => 'DISTINCT genres.name, genres.description, books.product_id', :conditions => [Genre.book_id = :books.id, Book.genres.id = :genres.id] )
But at this point I am running around in circles and not getting anywhere. Do I need to be using has_many :through?
The following examples use your models, defined above. You should use scopes to push associations back into the model (alternately you can just define class methods on the model). This helps keep your record-fetching calls in check and helps you stick within the Law of Demeter.
Get a list of Books, eagerly loading each book's Type and Genres, without conditions:
def Book < ActiveRecord::Base
scope :with_types_and_genres, include(:type, :genres)
end
#books = Book.with_types_and_genres #=> [ * a bunch of book objects * ]
Once you have that, if I understand your goal, you can just do some in-Ruby grouping to corral your Books into the structure that you need to pass to your view.
#books_by_type = #books.group_by { |book| book.type }
# or the same line, more concisely
#books_by_type = #books.group_by &:type
#books_by_type.each_pair do |type, book|
puts "#{book.genre.name} by #{book.author} (#{type.name})"
end

(rails) Order by Number of Likes on a Website

I have a model called websites that has_many :likes, and of course another model called likes with belongs_to :website. I want to get an array of all the websites but order them by the number of likes a website has.
helpful info:
I have it set up so #website.likes will return the likes from #website. (#website.likes.count is the number of likes a website has)
I want to get an array of all the websites but order them by the number of likes a website has.
As others have posted, you can do a join onto likes and then order by the count. Performance may be a bit iffy depending on indexing etc. You'll have slightly different syntax depending on if you're running Rails 2 or 3.
An alternative would be to maintain a denormalised likes_count column on websites which is updated when a Like model object is saved.
Then you just need to query on Website and specify an order likes_count descending (and is easily indexed).
To do this, create a likes_count integer column on the websites and specify the :counter_cache option on the belongs_to declaration in the Likes model. e.g:
class Likes
belongs_to :website, :counter_cache => true
end
Check out http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html for more info
This query should give you what you need:
all(:select => 'websites.*, count(*) as count',
:joins => :likes,
:group => :websites,
:order => 'count DESC')
Something along the lines of:
SELECT Websites.*, COUNT(Like.ID) AS Liked
FROM websites
LEFT OUTER JOIN Like ON websites.ID = Like.website_id
GROUP BY Like.website_id ORDER BY Liked DESC

Data from joined tables not typecast

I am using a join query to get the attributes of another table along with the query.
city = City.first(:select => "cities.*, states.name as state_name, states.time as state_time"
:joins => "LEFT JOIN states on cities.state_id = states.id",
:conditions => ["states.name = ?", params[:state]])
Here, the problem is that when I get the values from the joined tables like city.state_time, I will get the string like 2010-11-12 05:00:00 instead of the time object(no typecasting is done by Rails for these fields). It makes sense since I am calling City model and the methods used for typecasting time column will be in State model. I will have to explicitly parse time like this and will have to fight with the time zone issues as well. (as Rails do some customizations while giving the Time object and I will have to do these for these columns). Is there any way to link the columns to the State while doing the join. One method I thought of was like this.
state = State.new(:name => city.state_name, :time => city.state_time)
and use state.name and state.time. Is there a better way?
here's probably what you want:
class City < ActiveRecord::Base
belongs_to :state
end
class State < ActiveRecord::Base
has_many :cities
end
a = City.joins(:state).includes(:state).where(['states.name = ?', params[:state]]).first
a.state.time
This works using an inner join and has some conditions:
City must belong to only one state. If the city doesn't belong to any state the query won't return it because of the inner join
Rails 2 Syntax
a = City.find(:all, :conditions => ['states.name = ?', params[:state]], :joins => :state, :include => :state)

How to sort Rails AR.find by number of objects in a has_many relationship

How can I write an AR find query to have the results ordered by the number of records in a has_many association?
class User < ActiveRecord::Base
has_many :photos
end
I want to do something like...
User.find(:all, :order => photos.count)
I realize my find is not valid code. Say I have the following data.
User 1, which has 3 photos
User 2, which has 5 photos
User 3, which has 2 photos
I want my find to bring me back the users in the order of...
User 2,
User 1,
User 3
based on the count of of the users photos
The easiest way to achieve this is probably to add a counter cache to that model and then sort by that column.
class Photo < ActiveRecord::Base
belongs_to :user, :counter_cache => true
end
And be sure to add a column to your users table called photos_count.
Then you will be able to...
User.find(:all, :order => 'photos_count')
If you don't want an extra column, you could always ask for an extra column in the returned result set:
User.all(:select => "#{User.table_name}.*, COUNT(#{Photo.table_name}.id) number_of_photos",
:joins => :photos,
:order => "number_of_photos")
This generates the following SQL:
SELECT users.*, COUNT(photos.id) number_of_photos
FROM `users` INNER JOIN `photos` ON photos.user_id = users.id
ORDER BY number_of_photos
If you don't want to add a counter cache column, your only option is to sort after the find. If you :include the association in your find, you won't incur any additional database work.
users = User.find(:all, :include => :photos).sort_by { |u| -u.photos.size }
Note the negative sign in the sort_by block to sort from high to low.
I would advise you not to write direct SQL, since implementations of it may vary from store to store. Fortunately, you have arel:
User.joins(:photos).group(Photo.arel_table[:user_id]).
order(Photo.arel_table[:user_id].count)
Counter cache will help, but you'll need an extra column in the db.
I'd add this as a comment on the top answer, but I can't for some reason. According to this post:
http://m.onkey.org/active-record-query-interface
The User.all(options) method will be deprecated after Rails 3.0.3, and replaced with a bunch of other (handy, chainable) active record type stuff, but it makes it very hard to figure out how to put together the same kind of query.
As a result, I've gone ahead and implemented the counter cache method. This was pretty easy and painless with the exception that you need to remember to update the column information in your migration, otherwise all existing records will have "0."
Here's what I used in my migration:
class AddUserCountToCollections < ActiveRecord::Migration
def self.up
add_column :collections, :collectionusers_count, :integer, :default => 0
Collection.reset_column_information
Collection.all.each do |c|
Collection.update_counters c.id, :collectionusers_count => c.collectionusers.count
end
end
def self.down
remove_column :collections, :collectionusers_count
end
end
In theory this should be faster, too. Hope that's helpful going forward.
Your question doesn't make sense. The :order parameter specifies a column name and an optional ordering direction i.e. asc(ending) or desc(ending).
What is the result that you're trying to achieve?

Resources