Rails, tips for solving this n+1? - ruby-on-rails

I have an employee view, where are listed all skills, which are written in the skills table on my db. For every employee, all skills are displayed, just as want it to.
Employees and skills are related to each other as has many :through association.
class Employee < ApplicationRecord
has_many :employeeskillsets, foreign_key: "employee_id"
has_many :skills, through: :employeeskillsets
end
class Skill < ApplicationRecord
has_many :employeeskillsets, foreign_key: "skill_id"
has_many :employees, through: :employeeskillsets
end
class Employeeskillset < ApplicationRecord
belongs_to :employee, foreign_key: 'employee_id'
belongs_to :skill, foreign_key: 'skill_id'
end
All those skills are displayed as buttons, which are toggles to enable/disable that particular skill on that employee (per click a direct insert/delete, no extra commit needed).
<%= link_to apply_skill_employee_path(employee: #employee, skill_id: skill.id), method: :put, remote: :true do %>
<%= skill.name %></div><% end %>
But now, I want to the buttons to be shown colored, when you load the page. If the skill is already enabled, the buttoncolor should be green, else grey. And here begins my problem:
My app checks each skill with one separate select statement. I use the following code for this:
<%= link_to apply_skill_employee_path(employee: #employee, skill_id: skill.id), method: :put, remote: :true do %>
<% if #employee.skills.exists?(skill.id) %>
<div class="button skill e-true"><%= skill.name %></div>
<% else %>
<div class="button skill"><%= skill.name %></div>
<% end %>
I have already tried to use includes, but it seems, the exists? checks each skill independently.
Does anybody here have a suggestion, how I could solve this, by using one single select?
Thanks in advance, I hope I have mentioned everything, what is necessary.
Edit 1: I forgot to mention, that i render this through a partial (if that is important to know).
And here is the current used #employee var in the employees_controller.
#employee = Employee.find_by(id: params[:id])

Try plucking ids, and then check for include? as you don't need to fetch all attributes of skills
<% skills = #employee.skills.pluck(:id) %>
<%= link_to apply_skill_employee_path(employee: #employee, skill_id: skill.id), method: :put, remote: :true do %>
<% if skills.include?(skill.id) %>
<div class="button skill e-true"><%= skill.name %></div>
<% else %>
<div class="button skill"><%= skill.name %></div>
<% end %>

Since skill is also an active record model, you can use include? on the employee's skills collection to check if the employee has a particular skill.
#employee.skills.include?(skill)
This way you are free to use includes clause to eagerly load employees' skills.

This will not fire the additional query
<% if #employee.skill_ids.exists?(skill.id) %>
Also to avoid n+1 in the following line
apply_skill_employee_path(employee: #employee, skill_id: skill.id)
Make sure you are including skills
#employee = Employee.includes(:skills).where(......)

The Rails way is far simpler.
When you use the has_many macro in ActiveRecord it also creates a _ids method that can be used to add or remove relations with an array:
#employee.skills_ids = [1,2,3]
This also works with indirect assocations with the :through option.
You can use this together with the form collection helpers to create select or checkbox tags:
<%= form_for(#employee) do |f| %>
<%= f.label :skill_ids, 'Skills' %>
<%= f.collection_check_boxes(:skills_ids, Skill.all, :id, :name) %>
<% end %>
To avoid an extra query you can do a left outer join in the controller:
def edit
# left_outer_joins is new in Rails 5
# see https://blog.bigbinary.com/2016/03/24/support-for-left-outer-joins-in-rails-5.html
#employee.left_outer_joins(:skills).find(params[:id])
end
You also don't need a silly extra method in your controller for something that should be handled as a normal update. KISS.

Related

Rails 4 form to set has_many through additional column

I have a has_many association between Items and their Components through a table called ComponentItems. ComponentItems contains a column quantity in addition to item_id and component_id. How is it possible to add a number_field to my form that shows the quantity of each component required for an item? The form must contain a number_field for each Item in the database, even if no relationship exists (i.e. #item.component_ids.empty? == true).
class Item < ActiveRecord::Base
has_many :components, through: :component_items
has_many :component_items
end
class Component < Item
has_many :items, through: :component_items
has_many :component_items
end
class ComponentItem < ActiveRecord::Base
belongs_to :item
belongs_to :component
end
I believe I've tried every permutation of model, controller and form_builder possible, except the correct one.
In response to the answer below, here's a form that shows a checkbox and the item code for component items that make up one particular item;
<%= form_for [#item] do |f| %>
<%= f.collection_check_boxes :component_items, Item.active.where.not(sku: #item.sku).sort_by{|n| n.sku}, :id, :sku do |b| %>
<%= b.check_box %> <%= b.label %><br/>
<% end %>
<% end %>
So, ideally I'd replace the check_box with a number_field for quantity. How?
So it seems what I wanted is not so straightforward after all. In the end I opted for using some jQuery for adding extra Components to Items via a separate form. Trying to add/remove components and adjust the quantities was beyond me, so choosing to use separate forms for each user action seemed simpler. It may not be the most user-friendly way of working but it's the best I have.
To edit the quantities I did the following;
<% #item.component_items.each do |x| %>
<%= hidden_field_tag "item[component_items_attributes][][id]", x.id%>
<%= label_tag x.component.sku, x.component.sku.upcase, :class=>"col-md-3 control-label" %>
<%= number_field_tag "item[component_items_attributes][][quantity]", x.quantity, :class=>"col-md-5"%>
<%end %>
and ensured the Item model accepted nested attributes for component_items. Finally, add the nested params array for multiple component_items to items_controller.rb...
def item_params
params.require(:item).permit(
:component_items_attributes =>[:component_id, :item_id, :quantity, :id]
)
end
Note I didn't use fields_for which seemed to generate an extra component_items_attributes array that didn't make any sense at all.
This should work:
#item.components.to_a.sum(&:quantity)
This will throw an error if quantity on some component is nil, so you may try like this to avoid errors:
#item.components.to_a.map(&:quantity).compact.sum
UPDATE
<% #item.component_items.each do |component_item| %>
<%= form_for(component_item) do |f| %>
<div class="field">
<%= f.label :quantity, 'Quantity' %><br />
<%= f.number_field :quantity %>
</div>
<% end %>
<% end %>

Rails: Create Model and join table at the same time, has_many through

I have three Models:
class Question < ActiveRecord::Base
has_many :factor_questions
has_many :bigfivefactors, through: :factor_questions
accepts_nested_attributes_for :factor_questions
accepts_nested_attributes_for :bigfivefactors
end
class Bigfivefactor < ActiveRecord::Base
has_many :factor_questions
has_many :questions, through: :factor_questions
end
and my join-table, which holds not only the bigfivefactor_id and question_id but another integer-colum value.
class FactorQuestion < ActiveRecord::Base
belongs_to :bigfivefactor
belongs_to :question
end
Creating an new Question works fine, using in my _form.html.erb
<%= form_for(#question) do |f| %>
<div class="field">
<%= f.label :questiontext %><br>
<%= f.text_field :questiontext %>
</div>
<%= f.collection_check_boxes :bigfivefactor_ids, Bigfivefactor.all, :id, :name do |cb| %>
<p><%= cb.check_box + cb.text %></p>
<% end %>
This let's me check or uncheck as many bigfivefactors as i want.
But, as i mentioned before, the join model also holds a value.
Question:
How can I add a text-field next to each check-box to add/edit the 'value' on the fly?
For better understanding, i added an image
In the console, i was able to basically do this:
q= Question.create(questiontext: "A new Question")
b5 = Bigfivefactor.create(name: "Neuroticism")
q.bigfivefactors << FactorQuestion.create(question: q, bigfivefactor: b5, value: 10)
I also found out to edit my questions_controller:
def new
#question = Question.new
#question.factor_questions.build
end
But i have no idea how to put that into my view.
Thank you so much for your help!
Big Five Factors model considerations
It looks like your Bigfivefactors are not supposed to be modified with each update to question. I'm actually assuming these will be CMS controlled fields (such that an admin defines them). If that is the case, remove the accepts_nested_attributes for the bigfivefactors in the questions model. This is going to allow param injection that will change the behavior sitewide. You want to be able to link to the existing bigfivefactors, so #question.factor_questions.first.bigfivefactor.name is the label and #question.factor_questions.first.value is the value. Notice, these exist on different 'planes' of the object model, so there wont be much magic we can do here.
Parameters
In order to pass the nested attributes that you are looking for the paramater needs to look like this:
params = {
question: {
questiontext: "What is the average air speed velocity of a sparrow?",
factor_questions_attributes: [
{ bigfivefactor_id: 1, value: 10 },
{ bigfivefactor_id: 2, value: 5 } ]
}
}
Once we have paramaters that look like that, running Question.create(params[:question]) will create the Question and the associated #question.factor_questions. In order to create paramaters like that, we need html form checkbox element with a name "question[factor_questions_attributes][0][bigfivefactor_id]" and a value of "1", then a text box with a name of "question[factor_question_attributes][0][value]"
Api: nested_attributes_for has_many
View
Here's a stab at the view you need using fields_for to build the nested attributes through the fields for helper.
<%= f.fields_for :factor_questions do |factors| %>
<%= factors.collection_check_boxes( :bigfivefactor_id, Bigfivefactor.all, :id, :name) do |cb| %>
<p><%= cb.check_box + cb.text %><%= factors.text_field :value %></p>
<% end %>
<% end %>
API: fields_for
I'm not sure exactly how it all comes together in the view. You may not be able to use the built in helpers. You may need to create your own collection helper. #question.factor_questions. Like:
<%= f.fields_for :factor_questions do |factors| %>
<%= factors.check_box :_destroy, {checked => factors.object.persisted?}, '0','1' %> # display all existing checked boxes in form
<%= factors.label :_destroy, factors.object.bigfivefactor.name %>
<%= factors.text_box :value %>
<%= (Bigfivefactor.all - #question.bigfivefactors).each do |bff| %>
<%= factors.check_box bff.id + bff.name %><%= factors.text_field :value %></p> # add check boxes that aren't currently checked
<% end %>
<% end %>
I honestly know that this isn't functional as is. I hope the insight about the paramters help, but without access to an actual rails console, I doubt I can create code that accomplishes what you are looking for. Here's a helpful link: Site point does Complex nested queries

Notifications ala Facebook (database implementation)

I am wondering how Facebook implements their notifications system as I'm looking to do something similar.
FooBar commented on your status
Red1, Green2 and Blue3 commented on your photo
MegaMan and 5 others commented on your event
I can't have multiple notifications written into a single record, as eventually I will have actions associated with each notification. Also, in the view I'd like notifications to be rendered as expandable lists when a certain number of them exist for a single subject.
FooBar commented on your status (actions)
Red1, Green2 and Pink5 commented on your photo [+]
MegaMan and 3 others commented on your event [-]
MegaMan commented on your event (actions)
ProtoMan commented on your event (actions)
Bass commented on your event (actions)
DrWilly commented on your event (actions)
Cheers!
PS I am using postgres and rails BTW.
There are a number of ways to go about implementing this. It really depends on what kinds of notifications you want to cover and what information you need to collect about the notification to show it to the right user(s). If you are looking for a simple design that just covers notifications about posted comments, you could use a combination of polymorphic associations and observer callbacks:
class Photo < ActiveRecord::Base
# or Status or Event
has_many :comments, :as => :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commenter
belongs_to :commentable, :polymorphic => true # the photo, status or event
end
class CommentNotification < ActiveRecord::Base
belongs_to :comment
belongs_to :target_user
end
class CommentObserver < ActiveRecord::Observer
observe :comment
def after_create(comment)
...
CommentNotification.create!(comment_id: comment.id,
target_user_id: comment.commentable.owner.id)
...
end
end
What's happening here is that each photo, status, event etc. has many comments. A Comment obviously belongs to a :commenter but also to a :commentable, which is either a photo, status, event or any other model that you want to allow comments for. You then have a CommentObserver that will observe your Comment model and do something whenever something happens with the Comment table. In this case, after a Comment is created, the observer will create a CommentNotification with the id of the comment and the id of the user who owns the thing that the comment is about (comment.commentable.owner.id). This would require that you implement a simple method :owner for each model you want to have comments for. So, for example, if the commentable is a photo, the owner would be the user who posted the photo.
This basic design should be enough to get you started, but note that if you want to create notifications for things other than comments, you could extend this design by using a polymorphic association in a more general Notification model.
class Notification < ActiveRecord::Base
belongs_to :notifiable, :polymorphic => true
belongs_to :target_user
end
With this design, you would then 'observe' all your notifiables (models that you want to create notifications for) and do something like the following in your after_create callback:
class GenericObserver < ActiveRecord::Observer
observe :comment, :like, :wall_post
def after_create(notifiable)
...
Notification.create!(notifiable_id: notifiable.id,
notifiable_type: notifiable.class.name,
target_user_id: notifiable.user_to_notify.id)
...
end
end
The only tricky part here is the user_to_notify method. All models that are notifiable would have to implement it in some way depending on what the model is. For example, wall_post.user_to_notify would just be the owner of the wall, or like.user_to_notify would be the owner of the thing that was 'liked'. You might even have multiple people to notify, like when notifying all the people tagged in a photo when someone comments on it.
Hope this helps.
I decided to post this as another answer because the first was getting ridiculously long.
To render the comment notifications as expandable lists as you note in your
examples, you first collect the comments that have notifications for some target user (don't forget to add has_one :notification to the Comment model).
comments = Comment.joins(:notification).where(:notifications => { :target_user_id => current_user.id })
Note that the use of joins here generates an INNER JOIN, so you correctly exclude any comments that don't have notifications (as some notifications might have been deleted by the user).
Next, you want to group these comments by their commentables so that you can create an expandable list for each commentable.
#comment_groups = comments.group_by { |c| "#{c.commentable_type}#{c.commentable_id}"}
which will generate a hash like
`{ 'Photo8' => [comment1, comment2, ...], 'Event3' => [comment1, ...], ... }`
that you can now use in your view.
In some_page_showing_comment_notifications.html.erb
...
<ul>
<% #comment_groups.each do |group, comments| %>
<li>
<%= render 'comment_group', :comments => comments, :group => group %>
</li>
<% end %>
</ul>
...
In _comment_group.html.erb
<div>
<% case comments.length %>
<% when 1 %>
<%= comments[0].commenter.name %> commented on your <%= comment[0].commentable_type.downcase %>.
<% when 2 %>
<%= comments[0].commenter.name %> and <%= comments[1].commenter.name %> commented on your <%= comment[0].commentable_type.downcase %>.
<% when 3 %>
<%= comments[0].commenter.name %>, <%= comments[1].commenter.name %> and <%= comments[2].commenter.name %> commented on your <%= comment[0].commentable_type.downcase %>
<% else %>
<%= render 'long_list_comments', :comments => comments, :group => group %>
<% end %>
</div>
In _long_list_comments.html.erb
<div>
<%= comments[0].commenter.name %> and <%= comments.length-1 %> others commented on your <%= comments[0].commentable_type %>.
<%= button_tag "+", class: "expand-comments-button", id: "#{group}-button" %>
</div>
<%= content_tag :ul, class: "expand-comments-list", id: "#{group}-list" do %>
<li>
<% comments.each do |comment| %>
# render each comment in the same way as before
<% end %>
</li>
<% end %>
Finally, it should be a simple matter to add some javascript to button.expand-comments-button to toggle the display property of ul.expand-comments-list. Each button and list has a unique id based on the comment group keys, so you can make each button expand the correct list.
So I have kinda made a little something before the second answer was posted but got a bit too busy to compose and put it here. And I'm still studying if I did the right thing here, if it would scale or how it would perform overall. I would like to hear all your ideas, suggestions, comments to the way I have implemented this. Here goes:
So I first created the tables as typical polymorphic tables.
# migration
create_table :activities do |t|
t.references :receiver
t.references :user
t.references :notifiable
t.string :notifiable_type #
t.string :type # Type of notification like 'comment' or 'another type'
...
end
# user.rb
class User < ActiveRecord::Base
has_many :notifications, :foreign_key => :receiver_id, :dependent => :destroy
end
# notification.rb
class Notification < ActiveRecord::Base
belongs_to :receiver, :class_name => 'User'
belongs_to :user
belongs_to :notifiable, :polymorphic => true
COMMENT = 'Comment'
ANOTHER_TYPE = 'Another Type'
end
So it's just this. Then inserts quite typically like when a user does a certain type of notification. So what's new that I found for psql is ARRAY_AGG which is an aggregate function that groups a certain field into a new field as an array (but string when it gets to rails). So how I'm getting the records are now like this:
# notification.rb
scope :aggregated,
select('type, notifiable_id, notifiable_type,
DATE(notifications.created_at),
COUNT(notifications.id) AS count,
ARRAY_AGG(users.name) AS user_names,
ARRAY_AGG(users.image) as user_images,
ARRAY_AGG(id) as ids').
group('type, notifiable_id, notifiable_type, DATE(notifications.created_at)').
order('DATE(notifications.created_at) DESC').
joins(:user)
This outputs something like:
type | notifiable_id | notifiable_type | date | count | user_names | user_images | id
"Comment"| 3 | "Status" |[date]| 3 | "['first', 'second', 'third']" |"['path1', 'path2', 'path3']" |"[1, 2, 3]"
And then in my notifications model again, I have this methods which basically just puts them back to an array and removes the non-uniques (so that a name won't be displayed twice in a certain aggregated notification):
# notification.rb
def array_of_aggregated_users
self.user_names[1..-2].split(',').uniq
end
def array_of_aggregated_user_images
self.user_images[1..-2].split(',').uniq
end
Then in my view I have something like this
# index.html.erb
<% #aggregated_notifications.each do |agg_notif| %>
<%
all_names = agg_notif.array_of_aggregated_users
all_images = agg_notif.array_of_aggregated_user_images
%>
<img src="<%= all_images[0] %>" />
<% if all_names.length == 1 %>
<%= all_names[0] %>
<% elsif all_names.length == 2 %>
<%= all_names[0] %> and <%= all_names[1] %>
<% elsif all_names.length == 3 %>
<%= all_names[0] %>, <%= all_names[1] %> and <%= all_names[2] %>
<% else %>
<%= all_names[0] %> and <%= all_names.length - 1 %> others
<% end %>
<%= agg_notif.type %> on your <%= agg_notif.notifiable_type %>
<% if agg_notif.count > 1 %>
<%= set_collapsible_link # [-/+] %>
<% else %>
<%= set_actions(ids[0]) %>
<% end %>
<% end %>

Many-to-many associations and REST?

Newbie question, you've been warned!
I'm trying to implement a sample Rails app with a many-to-many association, people owning movies, and I'm trying to figure out how exactly to implement the UI for it. It's my understanding that REST requires everything to be a resource, so in this case "User" (person), "Movie" and "Possession" (the joint table) (oh, the puns).
Now the interesting part, the UX. Let's say I have a user dashboard where all of your movies are listed.
Let's say the user wants to add a movie that he owns. How do you do this in REST? It's trivial with a custom action that one could add to the User controller, but the point is not to go beyond the basic 7 REST actions, right? Therefore I'd have to first do a "new" on a movie and then do a "new" on a possession, which are two operations. How do I collapse them into one?
Basically I feel I'm not quite understanding how to maintain REST as soon as multiple models are involved and would appreciate a tip.
Thanks!
Happily, Rails has some magic just for this common scenario. Assuming a model like this:
class Movie
has_many :users, :through => :possessions
end
Your view:
<%= form_for [current_user, Movie.new] do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<% end %>
Basically this form will POST to MoviesController#create and will pass along current_user.id as a user_id parameter that (the default) MoviesController#create will know to associate with the Movie it creates. Take a look at the documentation for FormBuilder#form_for for more information.
You could also do this the other way around, by the way:
class User
has_many :movies, :through => :possessions
accepts_nested_attributes_for :movies # magic!
end
And the view:
<%= form_for current_user |user_form| %>
<%= user_form.fields_for current_user.movies.build |movie_fields| %>
<%= movie_fields.label :title %>
<%= movie_fields.text_field :title %>
<% end %>
<% end %>
In this case the form will submit to UsersController#update and its parameters will look like this:
{ :id => 123,
:movie => {
:title => "The Red Balloon"
}
}
...and the controller will know to create the Movie object. For more information check the documentation for FormHelper#fields_for.

Use accepts_nested_attributes_for to create new records or update existing

Read the big update for the latest information.
Hey everyone,
I've got a many-to-many relationship in a rails app that involves three tables: a user table, an interests table, and a join user_interests table that also has a rating value so a user can rate each of their interests on a 1-10 scale.
I am basically looking for a way for a new user to create their rating when they sign up and edit them at a future date along with any of their profile information at the same time.
I tried to follow this question Rails nested form with has_many :through, how to edit attributes of join model? but the problem I'm having is trying to incorporate a select list into the mix and having multiple interests to rate for the user.
Model Code:
user.rb
has_many :user_interests, :dependent => :destroy
has_many :interests, :through => :user_interests, :foreign_key => :user_id
accepts_nested_attributes_for :user_interests
interest.rb
has_many :user_interests, :dependent => :destroy
has_many :users, :through => :user_interests, :foreign_key => :interest_id, :dependent => :destroy
user_interest.rb
belongs_to :user
belongs_to :interest
View Code:
app/views/user/_form.html.erb
<%= form_for(#user) do |form| %>
... user fields
<%= form.fields_for :user_interests do |ui_form| %>
... loop through ALL interests
<% Interest.all.each do |interest| %>
<%= ui_form.select :rating, options_for_select(1..10) %>
<%= ui_form.hidden_field :interest_id, :value => interest.id %>
<% end %>
<% end %>
<% end %>
I also included the following in the new/edit actions in my controller #user.interests.build.build_interest
The problem I'm running into is that only one interest rating is being passed in the params hash when I want to have multiple. Also I am getting an exception thrown by rails
Interest(#2172840620) expected, got Array(#2148226700)
What tiny detail did I miss or get wrong that is causing the problem?
EDIT:
I found a way to force this to work but it requires manually editing the HTML in chrome developer tools, the :name attribute of my form elements are being generated as user[user_interests_attributes][rating] but if I change it to user[user_interests_attributes][][rating] it will work when I update a record. However I can't manually specify the :name of a form element that is tied to a form object. So what can I do to show that multiple interest ratings are being passed instead of just one that rails thinks?
BIG Update:
I got a semi functional version going with some slight changes:
View code:
<% form.fields_for :user_interests do |ui_form| %>
<p>
<%= ui_form.select :rating, options_for_select(1..5), :selected => :rating %>
<%= ui_form.label :interest_title %>
<%= ui_form.hidden_field :interest_id %>
</p>
<% end %>
Controller code:
def new
#user = User.new
Interest.all.each { |int| #user.user_interests.build({ :interest_id => int.id }) }
end
def edit
#user = #current_user
Interest.unrated_by_user_id(#user.id).each { |int| #user.user_interests.build({ :interest_id => int.id }) }
end
Now I am able to edit and get my user_interests updated or created if no rating exists, but I get an error that user is empty when I try to create a new user. Also I am unable to access any of the interest attributes in the form to display the interest the user is actually rating. Can anyone help with those caveats?
You only need #user.interests.build because its a has_many relationship. build_interest is for when there is a has_one/belongs_to relationship.
When using fields_for :user_interests you're telling the User model that an instance of one or more user_interest objects will be in the parameters hash when the user is created/updated. The form is not creating or updating any user_interests but it is sending back an array of user_interest_attributes hashes that represent the user_interests for the user the form references. This is an array of user_interests rating values for which no user_interests exist as you reference them in the form which is the reason you get the error.
Since you are passing a range to the select form helper you aren't actually providing any interests to the form for selection. The select will set a value for the rating column in the user_interests table with a value between 1 and 10. No user_interest exists for the rating to be set on even if the user_interests table has a rating column.
passing :multiple => true in the options hash of the select tag will create a multiple select list but I don't think that is what you want. I think you want many items on a page the user can put an interest rating on.
If you do want a user to be able to select many interests this is how to use fields_for with accepts_nested_attributes_for on a has_many :through relationship:
<%= form_for(#user) do |f| %>
<% f.fields_for :interest_ids do |interest| %>
<ul>
<% Interest.all.each do |choice,i| %>
<li class="selection">
<%= interest.check_box [], { :checked => f.object.user_interest_ids.include?(choice.id) }, choice.id, '' %>
<%= interest.label [], choice.name %>
</li>
<% end %>
</ul>
<% end %>
<% end %>

Resources