rails, nested associated models AR query - ruby-on-rails

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

Related

How to join on nested belongs_to in Ruby on Rails

I have three models, Application, which belongs to a Board, which belongs to a User.
class User < ActiveRecord::Base
has_many :boards
end
class Board < ActiveRecord::Base
belongs_to :user
has_many :applications
end
class Application < ActiveRecord::Base
belongs_to :board
end
I'm always only ever going to want to show the boards or applications for the current user. How can I say "show every application for the current board for the current user"? Basically how to query for something for specific parent id values.
You should provide the current user id and board id at first.
user = User.find(user_id) #get current user
board = user.boards.find(board_id) #get current board
board.applications #get every application
You can get more info from Rails Guide--Active Record Associations
show every application for the current board for current_user
The power of ActiveRecord should make this relatively simple:
board = current_user.boards.find params[:board_id]
board.applications #-> collection of Application objects for board
This assumes you're using devise, and thus have access to the current_user method.
If not, you'll be able to use something like the following:
#config/routes.rb
resources :users do
resources :applications #-> url.com/users/:user_id/applications
end
#app/controllers/applications_controller.rb
class ApplicationsController < ApplicationController
def index
#user = User.find params[:user_id]
#boards = #user.boards
end
end
#app/views/applications/index.html.erb
<% #user.boards.each do |board| %>
<%= board.name %>
<% board.applications.each do |application| %>
<%= application.name %>
<% end %>
<% end %>
<% end %>
ORM
I'll give you some context to this (hopefully it will help you in the future).
Firstly, you must understand that Rails is built on top of a relational database - or at least it should be. Relational databases use foreign_keys to give you access to associative data objects:
Whilst this might not seem important, it explains the entire functionality of the ActiveRecord system.
ActiveRecord is what's known as an ORM (Object Relationship Mapper)... in short, gives you the ability to populate objects with associative data from your relational database.
In the case of Ruby/Rails, the object orientated nature of ActiveRecord is a direct result of Ruby. That is, Ruby is an object orientated language; everything you do in it revolves around objects:
With Rails, those objects are built in the models.
Thus, if you want to understand how Rails works - you really need to get down to the model level -- you have to understand that calling Model.find x will build an object around that model, populating it with data from your database.
It's very clever.
The associations are provided by ActiveRecord, and pulled through the relational database infrastructure. This means that you can call the likes of #board = #user.boards.first and populate with the correct data.
Here's a good demonstration of how it works:
I'd share about how running query with ActiveRecord::Relation. To know about query, no matter you want to show data that far away from the current table but when those have associations, those could be connected.
Step by step to do query:
Determine all relations tables
Here, you have to determine tables that related to its table associations. In this case: users, boards, and applications.
Determine the condition
You can put current_user is a condition. You need users, boards, applications
So the condition is:
User.joins(boards: :applications).where("users.id = ?", current_user.id)
NOTE:
I would try to explain. User joins boards because user has many boards. Next boards has many applications so we have to join boards with application into boards: :applications.
This is good explain for query has many through associations. activerecord-query-through-multiple-joins

cascading relational models Ruby on Rails

I have three models
User has_many :articles
Article has_many :daily_view_metrics & belongs_to :user
DailyViewMetric belongs_to :article
I want each user to have an overview of the metrics for their articles. At the moment I pass #user = User.find(params[:id]) in the show action of the users controller to open a specific users's show page. I also pass in #articles = Article.where(user_id: params[:id]).load in order to limit the available articles to only the user's articles.
Since the DailyViewMetric model has the same article multiple times (at different days) I need to aggregate them into a new array of arrays. I.e. I need
article_id date views
1 feb-01 20
1 feb-02 50
2 feb-01 30
to be returned as [[1,70],[2,30]] so I can sort it according to user wishes. How do I do this? Do I make a named scope in the DailyViewMetric model? or in the user_helper.rb?
It would be nice if I could have something that I can run newArray.each do |a| on in my view to make a table with stuff like <%= a.article_id %> and <%= sumviews %>, etc.. with something I can pass in the user_id so the aggregate is limited to his/her articles
You should be able to do it in the following way
Long form:
views_array = []
#articles.each do |a|
a.views do |v|
views_array << [a.id, v.date, v.count]
end
end
Short form:
#articles.collect {|a| a.daily_view_metrics.collect {|dv| [a.id, dv.date, dv.count] } }

Rails associate record counting inside view

I have two models, story & vote. I have a list of articles which a user can up vote which is stored as a separate vote in the vote model.
The story model has many votes and the story_id is stored with the vote record.
At the moment in my index action on the story view I'm just trying to do a simple count to see how many votes a given story has, I'm achieving this with the following code.
<%= #total_votes = Vote.where(:story_id => story.id).count %>
However I'd like to remove this from my story index action view but not sure were to store it? Is there a more efficient way of doing this?
Seems the relationship is story has many votes. So, you can simply do:
<%= story.votes.count %>
If you are worried about database performance you may want to add a counter cache.
You said you'd like to store the total votes value:
$~ rails g migration add_total_votes_to_stories total_votes:integer
class Story
after_save, :update_total_votes
private
def update_total_votes
write_attribute :total_votes, votes.count
end
Inside the model you could define a method and use it in the controller.
def total_votes(sid)
where(:story_id => sid ).count
end
Then in the controller:
#total_votes = Vote.total_votes(story_id)
this should go in controller
def index
#total_votes = Vote.where(:story_id => story.id).count
end

Loop through 2 model objects at once rails

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.

N+1 problem in mongoid

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.

Resources