I’m building a social networking application, users be able to have some posts and other users able to like and comments on those posts.
class Post < ActiveRecord::Base
belongs_to :user
has_many :likes
has_many :comments
end
class Comment < ActiveRecord::Base
belongs :user
belongs :post, counter_cache: true
end
class Like < ActiveRecord::Base
belongs :user
belongs :post, counter_cache: true
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :body, :likes_count, :comments_count, is_liked
def likes_count
object.likes.size # should pull from counter_cache
end
def comments_count
object.comments.size # should pull from counter_cache
end
# did current_user already like this post
def is_liked
object.likes(user_id: scope.id).exists? # N+1
end
end
Another way to prevent N+1 this issue is to use Post.recent.includes(:likes, :comments), but I think there is still a problem with scaling because it has to load likes and comments and we are not interested much on detail of those associations at all.
Do you to have any suggestion solution? Or other ways to modeling this problem?
@zamith I’m not sure, it looks like your code will return true or false if the user ever like a post. The problem with my version is just this code object.likes(user_id: scope.id).exists? will run for every post.
And it should call PostSerializer with each post. Every post it has to run another query to check if the current_user is already like the post in is_liked method. that’s what I try to avoid N+1.
@zamith I think you are confusing here. In controller we want to return recent posts in the whole app. They are not return only recent posts of current_user. And object.id, object here is a reference to individual post of active model collection inside serializer class, so we can’t access in controller.
Just recents posts by any users in the app, but we need to add another attributes is_liked to each post in json, so mobile app knows if the current user already liked some posts in the json response.
I don’t think you can do it in one query, but I’m not a DB expert. You can remove the N+1 query, though:
recent_posts = Post.recent.page(params[:page])
liked_posts = Post.joins(:likes).where(id: posts.ids, "likes.user_id" => current_user.id)
recent_posts.map! do |recent_post|
if liked_posts.include? recent_post
serialize(recent_post).merge({is_liked: true})
else
serialize(recent_post).merge({is_liked: false})
end
end
Basically you get two sets and have to run through them. Don’t know how this works in terms of performance, but it should do the job with only two queries.
I’m not sure how to serialize a post, but in order to merge that has to be an hash, if you have an attr_accessor for liked in the model, then you can just set that. Also it is common practice to used liked= and liked? for boolean fields, instead of is_liked.