Active record group by relation and count - ruby-on-rails

I have these classes
class Challenge
has_many :photos
end
class Photo
belong_to :challenge
has_many :votes
end
class Vote
belongs_to :photo
end
I'm trying to get for every photo how many vote I have.
I try with
#challenge.photos.group_by(&:votes)
But the result is not what I need...

To make it easy to fetch the votes count for each photo, you can introduce a new column in :photos names :votes_count and add counter_cache: true in the belongs_to association in the Vote model.
class Vote
belongs_to :photo, counter_cache: true
end
class AddVotesCountToPhotos < ActiveRecord::Migration
def change
add_column :photos, :votes_count, :integer
end
end
Now you can easily query the votes count for any photo:
#photo.votes_count
One way to find the votes for each photo for a particular challenge:
photos_with_votes = #challenge.photos.each_with_object([]) do |photo, arr|
arr << [photo.id, photo.votes_count]
end
By the way, as your tables might already have been populated with records, you need to reset all the counter caches to their correct values using an SQL count query. We will use reset_counters for the purpose:
Photo.pluck(:id).each { |id| Photo.reset_counters(id, :votes) }

Related

How to use Active Record counter cache with the all scope

I have an activerecord class method scope that returns all when the scope should remain unchanged. However I would expect it to use the counter cache when chaining size to the all scope. Here is an example:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
def self.filtered(only_approved:)
if only_approved
where(approved: true)
else
all
end
end
end
# This does not use the counter cache but should since the scope is unchanged
Post.first.comments.filtered(only_approved: false).size
So it looks like Post.comments.size triggers the counter cache while Post.comments.all.size does not. Is there a way around this?
This happens because of how the counter_cache works. It needs 2 things:
Add the counter_cache: true to the belonging model (Comment)
Add a column comments_count to the having model (Post)
The column added to the Post model gets updated everytime you create or destroy a model so it will count all existing records on the table. This is the reason why it won't work on a scope (a scope might be useful to filter the resulting records, but the actual column comments_count is still counting the whole table).
As a workaround I'd suggest you to take a look at and see if it can be used for your usecase https://github.com/magnusvk/counter_culture.
From their own repo:
class Product < ActiveRecord::Base
belongs_to :category
scope :awesomes, ->{ where "products.product_type = ?", 'awesome' }
scope :suckys, ->{ where "products.product_type = ?", 'sucky' }
counter_culture :category,
column_name: proc {|model| "#{model.product_type}_count" },
column_names: -> { {
Product.awesomes => :awesome_count,
Product.suckys => :sucky_count
} }
end
The only way I found to deal with this is to pass the scope to the class method and return it if no additional scope is to be added. It's not as clean but it works. Here is the updated code:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
def self.filtered(scope:, only_approved:)
if only_approved
scope.where(approved: true)
else
scope
end
end
end
# This works with counter cache if the scope is returned as is
Comment.filtered(scope: Post.first.comments, only_approved: false).size

How to display Parent record with total number of its Child record in rails

class Attachment < ActiveRecord::Base
belongs_to :user, foreign_key: :creator_id
belongs_to :deal_task, foreign_key: :relation_id
end
class DealTask < ActiveRecord::Base
has_many :attachments, foreign_key: :relation_id
end
I have parent table called DealTask and child table called Attachment
I want a list of DealTask records with associated total number of attachments
DealTask.all.map do |deal_task|
deal_task.
attributes.
with_indifferent_access.
slice(:id, :name).
merge!(total_attachment: deal_task.attachments.count)
end
Or, if you don't care about indifferent access and you don't mind having all the DealTask attributes, you can write this with on a single line:
DealTask.all.map{|deal_task| deal_task.attributes.merge!(total_attachments: deal_task.attachments.count)}
Breaking it down...
DealTask.all.map do |deal_task|
...
end
Is going to return an array. The array will contain the results of the do block.
deal_task.
attributes.
with_indifferent_access
Gives you the attributes of each deal_task in a hash that can be access with strings or symbols (thus, "indifferent_access").
deal_task.
attributes.
with_indifferent_access.
slice(:id, :name)
Keeps only the :id and :name of the deal_task hash.
merge!(total_attachments: deal_task.attachments.count)
Adds the attachments count to your hash with the key total_attachments.
Results should look something like:
[
{id: 1, name: 'name1', total_attachments: 12},
{id: 2, name: 'name2', total_attachments: 3}
]
I found the best solution for Parent child relationship count
counter_cache: true
because all above queries take too much time to load from database
so you all must prefer to use this
1-> Add one column in Parent table called DealTask
rails g migration AddAttachmentsCountToDealTask attachments_count:integer
2-> Open Migration add Edit it
class AddAttachmentCountToDealTask < ActiveRecord::Migration[5.0]
def up
add_column :deal_tasks, :attachments_count, :integer, default: 0
DealTask.reset_column_information
DealTask.find_each do |deal_task|
DealTask.reset_counters deal_task.id, :attachments
end
end
def down
remove_column :deal_tasks, attachments_count
end
end
So when you rollback the migration it will not raise an error or exception
you can also use any loop instead of using
find_each, DealTask.all.each do...end
but yes, While resetting counter Must use class name like
DealTask.reset_counters
3-> Set Counter cache
class Attachment < ActiveRecord::Base
belongs_to :deal_task, foreign_key: :relation_id, counter_cache: true
end
class DealTask < ActiveRecord::Base
has_many :attachments, foreign_key: :relation_id
end
suppose name of your model is sub_tasks than your counter_cache column must be
sub_tasks_count
if you want your own column name than you have to specify that column name in counter_cache
suppose column name is total_subtasks than
belongs_to :deal_task, foreign_key: :relation_id, counter_cache: :total_subtasks
and make changes accordingly for updating counter_cache
now when you Add any Attachment, attachments_count column increase by 1 and this is done automatically by **counter_cache
one Problem is there
** when you delete any child counter_cache is unable to decrease **
so for that solution make a callback
class Attachment < ActiveRecord::Base
belongs_to :deal_task, foreign_key: :relation_id, counter_cache: true
before_destroy :reset_counter
private
def reset_counter
DealTask.reset_counters(self.relation.id, :attachments)
end
end
so when you delete any attachments it will reset countet_cache for its Parent by relation_id which is parent_id or Foreign_key for attachments
for more info
see video on Railscast counter cache 23
Try this
DealTask.all.map { |deal_task| deal_task.attachments.ids }.count
DealTask.first.attachments.count #This will give count of attachemenets
#To show all records and all the count
DealTask.find_each do |dt|
print dt.inspect
print "\n"
print dt.attachments.count
end
Or
DealTask.joins(:attachments).select("deal_tasks.*, count(attachements.id) as count").group("deal_tasks.id")
For much nicer format
DealTask.joins(:attachments)
.select("deal_tasks.id, deal_tasks.name, count(attachements.id) as attachments")
.group("deal_tasks.id")
.collect(&:attributes)
#This will gve you something like
[
{"id"=>34332630, "name"=>"some name", "attachments"=>1},
{"id"=>71649461, "name"=>"some name", "attachments"=>1}
]
This will be lot faster as you get all data in a single query

return the count for element in a has_many relationships

i have 2 models one is listing and user
user has_many listings
listing belongs_to user
i have a view setup , i want to display for each user their own listings count ,i try this code :
<% User.all.each do |user| %>
<%= user.listings.count %>
<% end %>
i want to grab the listing count for each user . i found a bunch of solution here , all return the loop .other solutions i tried is to create a class method .
def count_listings
Listing.where(:user_id => user.id).count
end
try to call this way <%= User.count_listings%> it doesn't work .
for some reason there something i'm missing ,can't quite figure it out .
The :counter_cache option can be used to make finding the number of belonging objects more efficient. Consider these models:
class Order < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_many :orders
end
With these declarations, asking for the value of #customer.orders.size requires making a call to the database to perform a COUNT(*) query. To avoid this call, you can add a counter cache to the belonging model:
class Order < ActiveRecord::Base
belongs_to :customer, counter_cache: true
end
class Customer < ActiveRecord::Base
has_many :orders
end
With this declaration, Rails will keep the cache value up to date, and then return that value in response to the size method.
Although the :counter_cache option is specified on the model that includes the belongs_to declaration, the actual column must be added to the associated model. In the case above, you would need to add a column named orders_count to the Customer model. You can override the default column name if you need to:
class Order < ActiveRecord::Base
belongs_to :customer, counter_cache: :count_of_orders
end
class Customer < ActiveRecord::Base
has_many :orders
end
Counter cache columns are added to the containing model's list of read-only attributes through attr_readonly.
source: Rails guide on associations
..scroll down to options of belongs_to
If all you need is what you show in the example you can do it better as follows
<% Listing.group(:user_id).count.each do |user, count| %>
<%= "user: #{user} has #{count} listings" %>
<% end %>
This does a single query to the database and fetches only what you need.
SELECT COUNT(*) AS count_all, user_id AS user_id FROM `listings` GROUP BY user_id
and returns a hash like:
{
1: 123,
2: 231
}
#{ user_id: count }

Rails has_many through avoiding duplication

I have a has_many through association setup between a song model and an artist model.
My code looks something like this
SongArtistMap Model
class SongArtistMap < ActiveRecord::Base
belongs_to :song
belongs_to :artist
end
Artist Model
class Artist < ActiveRecord::Base
has_many :song_artist_maps
has_many :songs, :through => :song_artist_maps
validates_presence_of :name
end
Song Model
class Song < ActiveRecord::Base
has_many :song_artist_maps
has_many :artists, :through => :song_artist_maps
accepts_nested_attributes_for :artists
end
I have a form where a user submits a song and enters in the song title and the song artist.
So when a user submits a song and my Artists table doesn't already have the artist for the song I want it to create that artist and setup the map in SongArtistMap
If a user submits a song with an artist that is already in the Artists table I just want the SongArtistMap created but the artist not duplicated.
Currently everytime a user submits a song a new artist gets created in my artists table even if the same one already exists and a SongArtistMap is created for that duplicated artist.
Any idea on how to tackle this issue? I feel like rails probably has some easy little trick to fix this already built in. Thanks!
Ok I got this figured out awhile ago and forgot to post. So here's how I fixed my problem. First of all I realized I didn't need to have a has_many through relationship.
What I really needed was a has_and_belongs_to_many relationship. I setup that up and made the table for it.
Then in my Artists model I added this
def self.find_or_create_by_name(name)
k = self.find_by_name(name)
if k.nil?
k = self.new(:name => name)
end
return k
end
And in my Song model I added this
before_save :get_artists
def get_artists
self.artists.map! do |artist|
Artist.find_or_create_by_name(artist.name)
end
end
And that did exactly what I wanted.
I use a method in the model of the table the other two go through, that is called with before_create. This can probably be made much neater and faster though.
before_create :ensure_only_one_instance_of_a_user_in_a_group
private
def ensure_only_one_instance_of_a_user_in_a_group
user = User.find_by_id(self.user_id)
unless user.groups.empty?
user.groups.each do |g|
if g.id == self.group_id
return false
end
end
end
return true
end
Try this:
class Song < ActiveRecord::Base
has_many :song_artist_maps
has_many :artists, :through => :song_artist_maps
accepts_nested_attributes_for :artists, :reject_if => :normalize_artist
def normalize_artist(artist)
return true if artist['name'].blank?
artist['id'] = Artist.find_or_create_by_name(artist['name']).id
false # This is needed
end
end
We are essentially tricking rails by over-loading the reject_if function(as we never return true).
You can further optimize this by doing case insensitive lookup ( not required if you are on MySQL)
artist['id'] = (
Artist.where("LOWER(name) = ? ", artist['name'].downcase).first ||
Artist.create(:name => artist['name'])
).id

Rails: Method for joining if a user has liked a set of songs

I have the following models:
User
Playlist
SongLike
Song
...
When I query a playlist for all of its songs, I get an array of song objects returned. What's the most efficient way to find out which of these songs a user has "liked"? The likes are stored in the SongLike model:
class SongLike < ActiveRecord::Base
belongs_to :user
belongs_to :song, :counter_cache => "likes_count"
end
...this is the song model:
class Song < ActiveRecord::Base
has_and_belongs_to_many :playlists
has_many :featured_songs
has_many :song_likes
has_many :users, :through => :song_likes
...
end
Utilize your song_likes association along with the songs in memory rather than querying the db multiple times:
# user.rb
def songs_liked_from(subset)
like_ids = self.song_likes.where(:song_id => subset.map(&:id)).select(:song_id).map(&:song_id)
subset.select{|s| liked_ids.include?(s.id) }
end
You could put an index on user_id and song_id being looked up together and maybe make it unique if that works for your system.
def songs_user_liked
user_liked_songs = []
songs.each do |song|
song_like = SongLike.find_by_user_id_and_song_id(#,song)
if song_like.song_like_count > 0
user_liked_songs << song
end
end
user_liked_songs
end

Resources