I'm fairly new to rails and I'm trying to figure out the best way to do this.
I have a players table and a teams table. They both are HABTM with each other and use a join table.
Models
class Player < ActiveRecord::Base
has_and_belongs_to_many :teams
end
class Team < ActiveRecord::Base
has_and_belongs_to_many :players
end
Controller
def players
#players = Player.all
end
View
<%#players.each do |player|%>
<tr>
<td><%= link_to "Add", "steam://friends/add/#{player.steamid}"%></td>
<td><%= link_to player.name, player%></td>
<td><%=player.email%></td>
<td><%=player.teams.teamname%></td>
</tr>
<%end%>
First, I know teamname should be team_name.
I've tried building a loop that loops through the teams but this page has over 1600 players and so it takes a few minutes to run it.
Am I missing a better way to do this?
The reason this is slow is because you're executing another query for every user. It's what is known as an N+1 problem, as that's the algorithmic complexity.
It's very easy to resolve this by retrieving the data from the database more efficiently. You can tell Rails to load all of the necessary records using what's called Eager Loading.
In this case, it's as easy as this:
#players = Player.includes(:teams).all
Rails will execute a query to retreive all players, then execute a second query to retrieve all teams, and you'll access them just the same — your view doesn't need to change at all!
Related
I want to enable users of my app to create online polls which have an arbitrary amount of questions. Questions come in two flavours: multiple choice and open ended
My idea is to build something like this:
Poll
has_many open_question
has_many multichoice_questions
With apropriate belongs_to in the associated models.
How do i make it possible to save the order in which questions appear, so that it can be recreated when the poll is taken?
I'm thinking about serializing an ordered 3D array with question id's and types, but that feels wrong (it's saving the same information twice).
What would be a Rails way to model this?
If it were me I would set up my model like this:
User has_many Polls has_many OpenQuestions && MultichoiceQuestions
Then I could do something like this:
#user = current_user
#poll = #user.polls.find(params[:poll_id])
#open_questions = #poll.open_questions.order('created_at ASC')
Alternatively, if you feel you need even more control you could leverage some scopes.
http://guides.rubyonrails.org/active_record_querying.html#scopes
I have three models: teachers, students, and assignments
class Teacher < ActiveRecord::Base
has_many :assignments
has_many :students, through: assignments, uniq: true
end
For any given teacher, I would like to retrieve a list of unique students - simple enough, I just call:
teacher.students
However, I would like to order that list of students based on who has submitted assignments recently. Specifically, I would like the student with the most recently updated assignment to appear first, and so on.
I am stuck on the following code, which is not working:
teacher.students.group("assignments.student_id").order("MAX(assignments.updated_at) DESC")
Any suggestions?
It appears that my original query was correct:
teacher.students.group("assignments.student_id").order("MAX(assignments.updated_at) DESC")
I had thought it wasn't working because I had written a bad RSpec test that was failing because I wasn't handling the timestamps well. Once I used Timecop to handle the timestamps properly, the test passed.
Sorry about that.
Not really tested, but I believe
teacher.students.includes(:assignments).order('assignments.updated_at')
should work
In SQL you can have SELECT x,y FROM ...
but if I use where, the sql generated will only select from the models which it is acting upon.
So for example:
user has_many :posts
post belongs_to :user
Post.includes(:users).where('created_at < ?' 1.day.ago) will have
SELECT "posts".* FROM "posts" WHERE (created_at > '2013-03-06 07:37:09.010916')
but how do I SELECT the post and the users so it will return the posts users as well?
UPDATE: As Saurabh pointed out, the loaded association should be :user and not :users
Includes is used to eager load association records, meaning that given the following
#posts = Post.includes(:user)
#posts.each do |post|
post.user
end
Only 2 queries will be made to the database, one for fetching the posts and another for fetching all the users that is associated to #posts. Rails manages the associations for you. Simple.
If you want to get a certain column value from a different table, try the following
#posts = Post.joins(:user).select('posts.*, users.name AS user_name')
#posts.first.user_name # will give you the name of the first user associated to the post
First, your associations are wrong. You can't do Post.includes(:users) when you say: post belongs_to :user.
The correct way is -
Post.includes(:user) for the belongs_to associations.
Second, you can select a value from different table by doing so:
Post.includes(:user).where(:user => {:name => params[:name]})
The above query will give you all the posts whose user name is params[:name] where :name is the field from the user table.
SCENARIO
I have a table full of posts with a users table.
I want to be able to fetch all the posts and group them by users but I want to set a limit of say 10 per user.
class Post < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :posts
end
# I thought this might work but it just grabs the first 10 posts and groups them
Post.find(:all, :limit=>10).group_by(&:user)
Any thoughts? Do I have to write custom SQL for or can Active Record do this?
Something like?
Post.group(:user_id).limit(10)
Post.group(:user_id).limit(10)
group_by is not a query method, but rather a method of Enumerable.
In your code, Post.find(:all, :limit => 10) is turned into an Array before being passed to group_by. The method above chains query methods together and only converts them to an Array when you need to use them.
ActiveRecord handles the whole thing. The above method translates to
SELECT `posts`.* FROM `posts` GROUP BY user_id LIMIT 10
The only way I know to grab the recent 10 posts per user would require a nested sub-query (which can have performance issues) or postgres-style lateral join. Fairly confident this cannot be accomplished with only active-record and requires writing custom SQL, which you've indicated you want to avoid.
As an alternative that could be accomplished without custom SQL, you could list each user and their posts within a time window (e.g. past month or year) with the following:
class User < ActiveRecord::Base
has_many :recent_posts, -> { where(posts: {created_at 1.month.ago..Time.now}) }, class_name: 'Post'
end
User.includes(:recent_posts).each do |user|
user.recent_posts
end
Which would not execute a SQL query for each user and would therefore be relatively performant compared with doing it purely in ruby.
I have a model Comment, which has_a User.
In my display page, I am doing a Comment.all, and then displaying each comment.
In the view I need to display not only the comment, but also information about the associated user (ie the author).
<% #comments.each do |comment| %>
<%= comment.user.name %>
<%= comment.user.email %>
...etc...
<% end %>
This is fine and all, but activerecord translates this into one SELECT * FROM users WHERE USER.id = commentId query per EACH comment I have.
This is a little ridiculous, especially on a page with hundreds of comments. (That's hundreds of individual separate DB hits!)
When I am doing the Comment.all, is there away to tell rails to not only grab the comments, but also grab the associated users, and then when I call comment.user.blah later, for it to not grab it from db again? (This way it would do it all in one db statement).
You should use .includes.
From the doc:
Solution to N + 1 queries problem
Active Record lets you specify in advance all the associations that are going to be loaded. This is possible by specifying the includes method of the Model.find call. With includes, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.
Revisiting the above case, we could rewrite Client.all to use eager load addresses:
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
Or, in your case, it would be
Comment.includes(:user)
You can do this easily with ActiveRecord, you just need to use the eager loading feature in it. So Comment.all(:include => :user) will run one large query rather than one query per record.
Checkout http://stackoverflow.com/questions/29908230/rails-have-activerecord-grab-more-than-one-association-in-one-go/29908317#29908317 if you want to associate with more than one association in the includes statement.
The gist is using Comment.includes(:user, :dates, :whatevertableetc).