I have two models
Post
has_many :comments
Comment
belongs_to :post
When I want display a list of posts and it's comment count. I usually include comments in the post like this .
Post.find(:all,:include => :comments)
To display a number of comment for post.
post.comments.size
Can I create a has_many relation which return count of comments ?
has_one :comments_count
Can I include this relationship like this ?
Post.find(:all,:include => :comments_count)
Rails has a counter cache which will automatically update a columns value based on the count of associated items. This will allow you to include the count when you return the posts object. Just add it to the belongs_to on comment.
Comment
belongs_to :post, :counter_cache => true
You'll need to add a new integer column to posts for the counter:
class AddCounterCacheToPosts < ActiveRecord::Migration
def self.up
add_column :posts, :comments_count, :integer
end
end
To answer you question, yes you can; but this is not the most efficient way to do it. Normally you add a column to Post called comments_count and you updated that column every Comment CRUD action.
Add the column:
rails g migration add_comment_count_to_post
Then in that migration add the following line:
add_column :posts, :comments_count, :integer, :default => 0
Then there are two way to handle it from here.
The first is a custom before_save and before_destroy in Comment model.
app/models/comment.rb
class Comment << ActiveRecord::Base
belongs_to :post
before_save :update_comments_count
before_destroy :update_comments_count
def update_comment_count
post.update_attribute(:comment_count, post.comments.count)
end
end
The second way is to use Rails custom helper for this:
app/models/comment.rb
class Comment << ActiveRecord::Base
belongs_to :post, :counter_cache => true
end
Related
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
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) }
This is my first question here in stackoverflow, so please bear with me hehe.
I have three models: User, Post, and Comments.
# user.rb
class User < ApplicationRecord
has_many :posts
has_many :comments
end
# post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
# comments.rb
class Comment < ApplicationRecord
belongs_to :user
belongs_to :post
end
What i'm trying to achieve is to let the user comment only once in a post. If there is already an existing comment by the same user, it should not have accept the save/create.
So far, my idea is to create a model function where it iterate all of the exisiting post.comments.each then check all of the users, then if there is, then the comment is invalidated. Though I have no Idea how to do it.
If you have any idea (and perhaps the code snippets), please do share. Thanks in advance! :)
In your comment.rb
validates :user_id, uniqueness: { scope: [:post_id]}
This validation will make sure the combination user_id and post_id will be unique in your comments table.
Well, there are a couple of ways to do it.
Firstly, it is possible to validate uniqueness of user_id - post_id combinations in comments:
# app/models/comments.rb
class Comment < ActiveRecord::Base
# ...
validates_uniqueness_of :user_name, scope: :account_id
Another approach is to manually check for comment existance before creating a comment:
if Comment.exists?(user_id: current_user.id, post_id: params[:comment][:post_id])
# render error
else
Comment.create(params[:comment])
end
But in a concurrent environment both approaches may fail. If your application is using a concurrent server like puma or unicorn you will also need a database constraint to prevent creation of duplicated records. The migration will be as follows:
add_index :comments, [:user_id, :post_id], unique: true
Im trying to destroy multiple records in my database table where :list column has the same name, however I get an error when I click on Destroy link: Could not find table 'bookmarks_posts', it says the error is in my controller line:
if #bookmarks.destroy_all
Why is it expecting a join table? How can I change that? Also I don't want to destory anything outside the given Bookmarks table. (I am using sqlite3, if that changes anything)
My table migration:
class CreateBookmarks < ActiveRecord::Migration
def change
create_table :bookmarks do |t|
t.string :list
t.references :user, index: true, foreign_key: true
t.references :post, index: true, foreign_key: true
t.timestamps null: false
end
end
end
My controller - destroy and show:
def destroy
#list = Bookmark.find(params[:id])
#bookmarks = Bookmark.where(:list => #list.list)
if #bookmarks.destroy_all
redirect_to bookmarks_url
end
end
def show
#lists = Bookmark.where(user_id: current_user.id).where(post_id: nil)
#list = Bookmark.find(params[:id])
#bookmarks = Bookmark.where.not(post_id: nil).where(list: #list.list)
#posts = Post.where(:id => #bookmarks.map(&:post_id))
end
in my show view I use this:
<%= link_to 'Destroy', #list, method: :delete %>
My models:
class Bookmark < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :posts
end
"Why is it expecting a join table?"
Because you have specified a HABTM association between Bookmark and Post models. So when you delete a Bookmark or a Post, it wants to remove any rows in the join table that are referencing the deleted item's ID.
The problem seems to be that you're either specifying the wrong association type in your models, or you've created the wrong migration to support a HABTM association.
For discussion, let's assume your database migration above is correct, eg: you want to store a user_id and post_id in the Bookmarks table. This means that you would have the following associations:
class Bookmark < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
class User < ActiveRecord::Base
has_many :bookmarks
end
class Post < ActiveRecord::Base
has_many :bookmarks
end
If you actually need a HABTM relationship, then you need to do a migration that creates a join table.
One way to figure out what the type of association you need is to remember that if a table has the ID of another model (eg: Bookmarks table has a user_id column), then that is a belongs_to association.
What i have created is a "active" field in my topics table which i can use to display the active topics, which will contain at first the time the topic was created and when someone comments it will use the comment.created_at time and put it in the active field in the topics table, like any other forum system.
I found i similar question here
How to order by the date of the last comment and sort by last created otherwise?
But it wont work for me, im not sure why it wouldn't. And i also don't understand if i need to use counter_cache in this case or not. Im using a polymorphic association for my comments, so therefore im not sure how i would use counter_cache. It works fine in my topic table to copy the created_at time to the active field. But it wont work when i create a comment.
Error:
NoMethodError in CommentsController#create
undefined method `topic' for
Topic.rb
class Topic < ActiveRecord::Base
attr_accessible :body, :forum_id, :title
before_create :init_sort_column
belongs_to :user
belongs_to :forum
validates :forum_id, :body, :title, presence: true
has_many :comments, :as => :commentable
default_scope order: 'topics.created_at DESC'
private
def init_sort_column
self.active = self.created_at || Time.now
end
end
Comment.rb
class Comment < ActiveRecord::Base
attr_accessible :body, :commentable_id, :commentable_type, :user_id
belongs_to :user
belongs_to :commentable, :polymorphic => true
before_create :update_parent_sort_column
private
def update_parent_sort_column
self.topic.active = self.created_at if self.topic
end
end
Didn't realise you were using a polymorphic association. Use the following:
def update_parent_sort_column
commentable.active = created_at if commentable.is_a?(Topic)
commentable.save!
end
Should do the trick.