Let's say I have two models, Post and Comment. Post has_many Comments.
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
Suppose I want to get a list of Post, Comment pairs. I could do this:
Post.includes(:comments).each do |post|
post.comments.each do |comment|
# do something with post and comment
end
end
But let's say that for whatever reason, maybe to do one less database query, I do a join:
Post.joins(:comments)
# => [#<Post:0xblahblah>]
Given one of the Posts returned by the above query, how do I access the Comment that the Post was joined with?
ps: for eager load, use includes instead of joins. You can access a certain post comments directly, by post.comments; or access a comment post by comment.post
Related
I have a blog model that has many posts and some of these posts are top posts. My intention is to have something like the following.
class Blog
has_many :posts
has_many :top_posts, class_name: 'Post'
end
class Post
belongs_to :blog
end
As you can see, posts and top posts are the same objects, but the collection of top posts is different from the collection of posts. Some posts are top posts too, but others not.
The problem is when I try to do blog.top_posts, it returns the same collection as blog.posts, which are all posts from that blog. I want blog.top_posts to return only the posts I have associated as the blog's top posts via blog.top_post_ids << random_post. Thanks in advance!
I will assume that , like David asked you in comment , you have particular post.
And a post could become a top_post if , by example , he has 50 likes , or 1000 views , or wathever attributes persistent in your database making a post become a top post .
If you don't have any criteria persistent on your database , you can't use active record to gets top_post.
Anyway if you have , you should use a scope :
class Blog
has_many :posts
end
class Post
belongs_to :blog
scope :top_posts, -> { where(views > 1000 ) } #or any conditions base on your criterias
end
And you can get them simply :
blog = Blog.first
blog.posts.top_posts # => [top posts belonging to this blog]
it's an answer based on assumption ...
doc for scope : http://guides.rubyonrails.org/active_record_querying.html#scopes
s
The problem with using the has_many is it simply attaches the ID of your Blog object to each Post object so when you call blog.posts or blog.top_posts it executes a SQL query looking for Posts WITH id=Blog.id thus, you get the same list twice.
I would suggest you have a single has_many posts and then sort the list that is returned by whatever makes each post the "Top Post." Or, if you'd like to avoid sorting I'd suggest something like this:
class Blog
has_many :posts
def initialize
#top_posts = []
end
def add_top_post(post)
if self.posts.include?(post)
#top_posts << post
end
end
end
class Post
belongs_to :blog
end
How about a scope?
http://guides.rubyonrails.org/active_record_querying.html#scopes
class Blog
has_many :posts
end
class Post
belongs_to :blog
scope :top, -> { where top_post: true }
end
# this gets all the top posts
blog.posts.top
I have an application with Users, Posts and Comments. Users has_many Posts, Posts has_many Comments and Comments belong_to Users and Posts.
In my view template I'm looping over the Comments like this to get the username of the commenter:
User.find(comment.user_id).name
How efficient is this if I'm doing this for every Comment per Post ?
I could imagine that it's maybe faster to store the username in a separate row in the Comments table, but duplicating the data seems just wrong.
Am I being paranoid and is ActiveRecord doing some caching magic or is there a better way of doing something like this ?
You can preload the users in the initial query.
Seeing as you have the association setup between User and Comment you can access the user via the association.
Let's assume you have a controller method:
def show
#posts = Post.limit(10).includes(:comments => :user)
end
In your view as you loop over #comments:
#posts.each do |post|
# show post stuff
post.comments.each do |comment|
comment.user.name
end
end
Revised
If you have association as below you can access user from comment object itself comment.user
class User < ActiveRecord::Base
has_many :posts
has_many :comments
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
Eager loading of associations
class Post < ActiveRecord::Base
has_many :comments
# ...
end
class Comment < ActiveRecord::Base
belongs_to :user
end
comments = #post.comments.includes(:user)
It will preload users.
SQL will look like:
SELECT * FROM `comments` WHERE `comments`.`post_id` = 42;
SELECT * FROM `users` WHERE `users`.`id` IN (1,2,3,4,5,6) # 1,2,3,4,5,6 - user_ids from comments
If performance is an issue for you, using a none relational DB, like Mongodb is the way to go.
If you still want to use ActiveRecord, either you use eager loading with Post.comments.include(:user) (which will load unused users info - and that's not great), or you can use caching techniques.
I find it OK to cache the user.name in the comments table, as you suggested, as long as you control the changes that can occur. You can do that by setting callbacks in your User model:
after_save do |user|
Comment.where(user_id: user.id).update_all(:username, user.name)
end
This technique is sort of a DB caching, but, of course, you can cache HTML fragments. And a comment block is a good to cache HTML block.
A Post belongs_to a User, and a User has_many Posts.
A Post also belongs_to a Topic, and a Topic has_many Posts.
class User < ActiveRecord::Base
has_many :posts
end
class Topic < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
belongs_to :topic
end
Well, that's pretty simple and very easy to set up, but when I display a Topic, I not only want all of the Posts for that Topic, but also the user_name and the user_photo of the User that made that Post. However, those attributes are stored in the User model and not tied to the Topic. So how would I go about setting that up?
Maybe it can already be called since the Post model has two foreign keys, one for the User and one for the Topic?
Or, maybe this is some sort of "one-way" has_many through assiociation. Like the Post would be the join model, and a Topic would has_many :users, :through => :posts. But the reverse of this is not true. Like a User does NOT has_many :topics.
So would this even need to be has_many :though association? I guess I'm just a little confused on what the controller would look like to call both the Post and the User of that Post for a give Topic.
Edit: Seriously, thank you to all that weighed in. I chose tal's answer because I used his code for my controller; however, I could have just as easily chosen either j.'s or tim's instead. Thank you both as well. This was so damn simple to implement. I think today might mark the beginning of my love affair with rails.
you can get the user_name and user_photo when displaying a topic and its posts...
something like:
#topic.posts.each do |post|
user_name = post.user.name
user_photo = post.user.photo
end
it's simple. sorry if i didn't understand your question very well.
Well, if what you want is display the user name of the author of a post, for example, you can do something like (not tested, but should work) :
#posts = topic.posts.all(:include => :user)
it should generate one request for all posts, and one for users, and the posts collection should have users.
in a view (haml here) :
- #posts.each do |post|
= post.user.name
= post.body
If I understand your question correctly, no, a has_many :through association is not required here.
If you wanted to display a list of all users that posted in a topic (skipping the post information), then it would be helpful.
You'll want to eager load the users from posts, though, to prevent n+1 queries.
Let's say you have two models: articles and comments.
class Article < ActiveRecord::Base
has_many :comments
end
You know you can fetch associated comments to an article like this:
article = Article.first
article.comments # => SELECT * FROM "comments" WHERE ("comments".article_id = 123)
Is there a way to explicitly access the article_id (123) within a named_scope?
I need this for a complex named_scope that joins another table. Basically the named_scope will depend on to be called from the associated parent object to make sense (article.comments.my_named_scope and not Comments.my_named_scope).
I don't want to pass the id as a parameter for the named_scope. So, instead of passing the article_id to the named scope with ... lambda { |article| ...} and access the id with "... #{article.id} ...", I want to somehow access this article_id that the others method uses, which I get from the has_many association.
Sounds like what you're actually after is an association extension:
http://guides.rubyonrails.org/association_basics.html#association-extensions
In particular, proxy_owner, which will be the #article in question
eg:
class Article < ActiveRecord::Base
has_many :posts do
def sample_extension
puts "Proxy Owner #{proxy_owner}"
end
end
end
#article.posts.sample_extension
Been struggling with the same issue. You can try this, which is a more elegant than using association extensions:
class Article < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
def self.get_article_id
self.new.article_id
end
end
#article = Article.new
#article.posts.get_article_id
Within class methods for Post, you can now just use get_article_id anywhere you need the ID of the parent article. With proxy associations, I wasn't able to do that.
I like #ajkochanowicz's solution but looks like there's a DB hit involved there (Rails 3.2.x), so just a heads up, not really ideal considering the fact that you already have the parent object on hand somewhere.
For Rails 4 and above
The newer way to do it in Rails4+ is:
class Article < ActiveRecord::Base
has_many :comments do
def my_named_scope
puts "Scope Owner = #{#association.owner}"
end
end
end
article = #article.comments.my_named_scope
Inside the scope my_named_scope, #association.owner returns the Article object that .comments was called on. Hence the article returned by the code above is same as the #article object.
Alternative method
If you don't want to use extensions and would rather avoid the "create a new object and get id from there" method (as described by Chanpory's answer), here is how to do it:
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
def self.get_article_id
Comment.scope_attributes["article_id"] # scope_attributes returns a hash of all the attributes inherited from the owner of this scope
end
end
#article = Article.find(10)
#article.comments.get_article_id # returns 10
I have two models, Article and Post that both inherit from a base model called ContentBase.
You can leave comments on both Articles and Posts, so I am using a Polymorphic Association between Comments and Article or Post.
However, since both Article and Post inherit from ContentBase, the commentable_type field ends up being "ContentBase" for both and screws everything up.
Is there a way to specify the commentable_type field in the has_many relationship in Article and Post?
Edit:
By "screws everything up" I mean if there is an Article with ID=1 and Post with ID=1 and I add a Comment with commentable_id=1, commentable_type=ContentBase, that comment will show up for both the Article and Post.
Here's the code:
class Article < BaseContent
has_many :comments, :as => :commentable
end
class Post < BaseContent
has_many :comments, :as => :commentable
end
and here's my Comment model:
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
What's in the ContentBase class? Can you move that code into a module instead of using inheritance?
Module BaseContent
def self.included(base)
base.class_eval do
validates_presence_of :somefield
validates_length_of :someotherfield
def my_method
"hello"
end
end
end
end
I don't think you want to do that. For polymorphic associations, you want the XXX_type field to be the base model class, not the actual class. I'm not exactly sure of the reason, but I believe it has to do with determining the table name to select from to get the polymorphic data.
I think you need to look at Single Table Inheritance, which is what ActiveRecord uses for storing derived classes in the database. It assumes that since Article and Post are subclasses of ContentBase, they will all be in the same table ("content_bases" by default). If that's the case, you'll never have an Article with ID=1 and a Post with ID=1.
A few references:
Rails Single Table Inheritance | Juixe Technow
Single Table Inheritance in Ruby on Rails