I have a users object that I'm exposing through the simple call:
#users = User.all
I also have some more expensive queries I'm doing where I'm using custom SQL to generate the results.
#comment_counts = User.received_comment_counts
This call and others like it will return objects with one element per user in my list ordered by id.
In my view I'd like to loop through the users and the comment_counts object at the same time. Eg:
for user, comment_count in #users, #comment_counts do
end
I can't figure out how to do this. I also can't seem to figure out how to get an individual record from the #comment_counts result without running it through #comment_counts.each
Isn't there some way to build an iterator for each of these lits objects, dump them into a while loop and step the iterator individually on each pass of the loop? Also, if you can answer this where can I go to learn about stepping through lists in rails.
Something like this should work for you:
#users.zip(#comment_counts).each do |user, comments_count|
# Do something
end
I've never come across manual manipulation of iterators in Ruby, not to say that it can't be done. Code like that would likely be messy and so I would likely favor more elegant solutions.
Assuming the arrays have equal values:
In your controller:
#users = User.all
#comments = Comment.all
In your view.
<% #users.each_with_index do |user, i |%>
<% debug user %>
<% debug #comments[i] %>
<% end %>
Then you can just check if the values exists on #comments if you don't know if the arrays have the same number of objects.
It's not obvious because your data requires a bit of restructuring. comment_count clearly belongs to instances of User and this should be reflected so that only loop on one collection (of Users) is necessary. I can think of two possible ways to achieve this:
Eager loading. Only works for associated models, not the case here probably. Say, each of your users has comments, so writing User.includes(:comments).all would return all users with their comments and only do 2 queries to fetch the data.
For counting each user's comments you could just use counter_cache (clickable). Then you'll have one more field in the users table for caching how many comments a user has. Again, works for associated Comments only.
Related
I don't understand the Rails includes method as well as I'd like, and I ran into an issue that I'm hoping to clarify. I have a Board model that has_many :members and has_many :lists (and a list has_many :cards). In the following boards controller, the show method looks as follows:
def show
#board = Board.includes(:members, lists: :cards).find(params[:id])
...
end
Why is the includes method needed here? Why couldn't we just use #board = Board.find(params[:id]) and then access the members and lists via #board.members and #board.lists? I guess I'm not really seeing why we would need to prefetch. It'd be awesome if someone could detail why this is more effective in terms of SQL queries. Thanks!
Per the Rails docs:
Eager loading is the mechanism for loading the associated records of
the objects returned by Model.find using as few queries as possible.
When you simply load a record and later query its different relationships, you have to run a query each time. Take this example, also from the Rails docs:
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
This code executes a database call 11 times to do something pretty trivial, because it has to do each lookup, one at a time.
Compare the above code to this example:
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
This code executes a database call 2 times, because all of the necessary associations are included at the onset.
Here's a link to the pertinent section of the Rails docs.
Extra
A point to note as of recent: if you do a more complex query with an associated model like so:
Board.includes(:members, lists: :cards).where('members.color = ?', 'foo').references(:members)
You need to make sure to include the appended references(:used_eager_loaded_class) to complete the query.
i have a User model and a Post model. User has_many Posts; Post belongs_to User.
I want to create a table that lists a User and the total number of Posts per user as long as the post's public_flag is 't'. Is this possible directly in the view?
In my controller:
#users = User.all
In my view:
<% #users.each do |user| %>
<%= user.posts.size%>
<% end %>
Of course, this gets ALL of the user's posts regardless if the public_flag is true. Is there any way to set a condition in the View, or will I need to do something fancy in the controller (AR Query?).
Thanks for your help in advance!
You can daisy-chain restrictions for models, like so:
user.posts.where(:public_flag => 't').count
The advantage of using count over size is that instead of pulling all the posts into memory and counting them, ActiveRecord will execute a count query against the database and save a lot of processing.
user.posts.find_by_public_flag(true).count
Within a simple CRUD rails app i'm working on, i'm using a comments gem called Opinio
The model that is commentable is called posts. Within my view I am trying to output the comments count per post:
<%= post.comments.size %>
This is only yielding the number of parent comments. It is not including the replies. How do I get the total number of comments per post, including the replies? Where is this gem storing the comment replies?
Any help would be much appreciated!
I've browsed around the source code and apparently comments may be children of other comments but only up to one level: e.g. comment1 belongs to post1 and comment2 belongs to comment1 but comment3 can not belong to comment2 (because the parent of comment2 is already another Comment).
So the query you need to make sounds like this: fetch all parent comments of post1, then fetch all child comments of the previous parent comments, then merge results. Something like:
class Post
# This returns an Array of Comment objects. If you intend to use
# more than the #count method on the result, you should consider
# adding some sort of ordering on the two queries.
def all_comments
parent_comments = Comment.where(:commentable_id => id)
child_comments = Comment.where(:commentable_id => parent_comments.map(&:id))
parent_comments + child_comments
end
end
post = Post.find(1)
post.all_comments.count # The number of all (parent & child) comments of post
If your comment class name is not Comment feel free to change it to whatever (same for Post).
You could try to retrieve the total number of comments on an individual post by using:
#post = Post.find(params[:id])in the controller and
<%= post.comments.count %>in the view.
To get all comments posted you could try
#posts = Post.all in the controller and
<%= #posts.commments.count %> in the view.
The comment replies will be stored in the database. Check the \db\migrate\ folder in your app and you should see the comments migration. If you look in the \db\ folder you should notice a development.sqlite3 database. Opening this using a database viewer will allow you to see the stored data.
Update 1
After reading through the Github repo I spotted a couple of things. Firstly I assume you are using Rails 3? (otherwise the engine doesn't work)
To display comments for a specific item opinio provides a method:
opinio_identifier do |params|
next Review.find(params[:review_id]) if params[:review_id]
next Product.find(params[:product_id]) if params[:product_id]
end
On this method you receive the params variable and you tell the engine who owns the comments from that page. This allows you to use routes like:
/products/1/comments
/products/1/reviews/1/
Ttwo customizations are only made through the opinio initializer, and they are the accept_replies which defaults to true and strip_html_tags_on_save which also defaults to true. You should be able to find this in the commentable model.
Maybe you could try this:
<%= Opinio.model_name.comments.count %> would change to
<%= Opinio.post.comments.count %>
Let's say I have a table posts, and another table reviews that has a post_id and a rating (integer).
How can I add a filter to app/admin/post.rb which returns posts with a certain total score? (e.g. SUM(reviews.rating) GROUP BY (posts.id)). I want the filter to show up on the right side of the index, along with the other filters, and ideally to function as a range input.
To be clear, when I say "filter", I mean the ActiveAdmin filter method which adds filters to the right sidebar on the index page.
I created a scope in Post which returns posts with scores, but I haven't been able to find a way to use that in an ActiveAdmin filter.
Note: I rewrote my example because my original one didn't capture the complexity of the question.
It's common to override scoped_collection to join associated records to increase performance:
ActiveAdmin.register Post do
controller do
def scoped_collection
super.includes :author, :publisher
end
end
end
Since the entire collection now has author and publisher included, you can have a scope that queries those:
scope :random_house do |scope|
scope.where publishers: {name: 'Random House'}
end
I haven't come up with a proper solution to this question, but I have found a workaround.
I can change the scoped_collection based on a query param, and simply pass the param in when I want to use it. For example, if I have a scope with_rating(x), which returns posts with a score of at least x, I can write:
controller do
def scoped_collection
if params[:with_rating]
super.with_rating(params[:with_rating])
else
super
end
end
end
Then I can go to /admin/posts?with_rating=100 and get back posts with a rating of at least 100.
Thanks to #seanlinsley for making me aware of the scoped_collection method that I used in this solution.
Use counter cache column to store the comments count
http://railscasts.com/episodes/23-counter-cache-column
Then the column will get updated each time a comment is created to that post.This would also help in increasing the performance of search.
I'm using Mongoid to work with MongoDB in Rails.
What I'm looking for is something like active record include. Currently I failed to find such method in mongoid orm.
Anybody know how to solve this problem in mongoid or perhaps in mongomapper, which is known as another good alternative.
Now that some time has passed, Mongoid has indeed added support for this. See the "Eager Loading" section here:
http://docs.mongodb.org/ecosystem/tutorial/ruby-mongoid-tutorial/#eager-loading
Band.includes(:albums).each do |band|
p band.albums.first.name # Does not hit the database again.
end
I'd like to point out:
Rails' :include does not do a join
SQL and Mongo both need eager loading.
The N+1 problem happens in this type of scenario (query generated inside of loop):
.
<% #posts.each do |post| %>
<% post.comments.each do |comment| %>
<%= comment.title %>
<% end %>
<% end %>
Looks like the link that #amrnt posted was merged into Mongoid.
Update: it's been two years since I posted this answer and things have changed. See tybro0103's answer for details.
Old Answer
Based on the documentation of both drivers, neither of them supports what you're looking for. Probably because it wouldn't solve anything.
The :include functionality of ActiveRecord solves the N+1 problem for SQL databases. By telling ActiveRecord which related tables to include, it can build a single SQL query, by using JOIN statements. This will result in a single database call, regardless of the amount of tables you want to query.
MongoDB only allows you to query a single collection at a time. It doesn't support anything like a JOIN. So even if you could tell Mongoid which other collections it has to include, it would still have to perform a separate query for each additional collection.
Although the other answers are correct, in current versions of Mongoid the includes method is the best way to achieve the desired results. In previous versions where includes was not available I have found a way to get rid of the n+1 issue and thought it was worth mentioning.
In my case it was an n+2 issue.
class Judge
include Mongoid::Document
belongs_to :user
belongs_to :photo
def as_json(options={})
{
id: _id,
photo: photo,
user: user
}
end
end
class User
include Mongoid::Document
has_one :judge
end
class Photo
include Mongoid::Document
has_one :judge
end
controller action:
def index
#judges = Judge.where(:user_id.exists => true)
respond_with #judges
end
This as_json response results in an n+2 query issue from the Judge record. in my case giving the dev server a response time of:
Completed 200 OK in 816ms (Views: 785.2ms)
The key to solving this issue is to load the Users and the Photos in a single query instead of 1 by 1 per Judge.
You can do this utilizing Mongoids IdentityMap Mongoid 2 and Mongoid 3 support this feature.
First turn on the identity map in the mongoid.yml configuration file:
development:
host: localhost
database: awesome_app
identity_map_enabled: true
Now change the controller action to manually load the users and photos. Note: The Mongoid::Relation record will lazily evaluate the query so you must call to_a to actually query the records and have them stored in the IdentityMap.
def index
#judges ||= Awards::Api::Judge.where(:user_id.exists => true)
#users = User.where(:_id.in => #judges.map(&:user_id)).to_a
#photos = Awards::Api::Judges::Photo.where(:_id.in => #judges.map(&:photo_id)).to_a
respond_with #judges
end
This results in only 3 queries total. 1 for the Judges, 1 for the Users and 1 for the Photos.
Completed 200 OK in 559ms (Views: 87.7ms)
How does this work? What's an IdentityMap?
An IdentityMap helps to keep track of what objects or records have already been loaded. So if you fetch the first User record the IdentityMap will store it. Then if you attempt to fetch the same User again Mongoid queries the IdentityMap for the User before it queries the Database again. This will save 1 query on the database.
So by loading all of the Users and Photos we know we are going to want for the Judges json in manual queries we pre-load the data into the IdentityMap all at once. Then when the Judge requires it's User and Photo it checks the IdentityMap and does not need to query the database.
ActiveRecord :include typically doesn't do a full join to populate Ruby objects. It does two calls. First to get the parent object (say a Post) then a second call to pull the related objects (comments that belong to the Post).
Mongoid works essentially the same way for referenced associations.
def Post
references_many :comments
end
def Comment
referenced_in :post
end
In the controller you get the post:
#post = Post.find(params[:id])
In your view you iterate over the comments:
<%- #post.comments.each do |comment| -%>
VIEW CODE
<%- end -%>
Mongoid will find the post in the collection. When you hit the comments iterator it does a single query to get the comments. Mongoid wraps the query in a cursor so it is a true iterator and doesn't overload the memory.
Mongoid lazy loads all queries to allow this behavior by default. The :include tag is unnecessary.
This could help https://github.com/flyerhzm/mongoid-eager-loading
You need update your schema to avoid this N+1 there are no solution in MongoDB to do some jointure.
Embed the detail records/documents in the master record/document.
In my case I didn't have the whole collection but an object of it that caused n+1 (bullet says that).
So rather than writing below which causes n+1
quote.providers.officialname
I wrote
Quote.includes(:provider).find(quote._id).provider.officialname
That didn't cause a problem but left me thinking if I repeated myself or checking n+1 is unnecessary for mongoid.