Is there a more efficient way than this to load records associated with records in a list? - sql

I have a model Playlist, and a model User, both of which have_many of each other, through a join model PlaylistUser.
On my playlists#show action, I want to print a list of all of a Playlist's Users, along with the first two Playlists associated with each of those Users.
Right now here's what I have:
playlists/show.html.erb
<% #playlist = Playlist.find(params[:id]) %>
<% #playlist.users.each do |user| %>
<%= user.name %>
<%= user.playlists.first.name %>
<%= user.playlists.second.name %>
<% end %>
models
class User < ActiveRecord::Base
has_many :playlist_users
has_many :playlists, :through => :playlist_users
end
class PlaylistUser < ActiveRecord::Base
belongs_to :playlist
belongs_to :user
end
class Playlist < ActiveRecord::Base
has_many :playlist_users
has_many :users, :through => :playlist_users
end
But there is an enormous change in performance when I delete the user.playlists lines and print out only the user.name, because then the database only has to make one query, as opposed to hundreds.
Does anyone know of a way to make this more efficient? Maybe I could somehow load all the associated Playlists in the original query?

You can use the includes method to tell Rails to preload associated records with just one query upfront.
Loading from the database is a controller responsibility and should not happen in the view. Therefore, add the following to your controller:
playlist = Playlist.find(params[:id])
#users = playlist.users.includes(:playlists)
And change your view to just iterate over the users array:
<% #users.each do |user| %>
<%= user.name %>
<%= user.playlists.first.name %>
<%= user.playlists.second.name %>
<% end %>

Related

How to create new records with nested attributes in a ruby on rails "has many through" relationship?

I need some advice on building a has many through relationship between USER, THING and EXTRA models.
My USER model is slightly modified inside Devise gem and is noted as Creator whereas other models belonging to USER receive :created_things form.
In my app, USERS create THINGS can later add EXTRAS to their THINGS.
I chose has many through because I want to have unique data on all three models and be able to call both THINGS and EXTRAS from the USER "CREATOR" model.
I have built this many different ways and after 10 years of solving my problems by reading stackoverflow, I am finally submitting this request for support! Thank you for your help.
I have tried creating user and extra references on the THING model and declaring nested attributes in the USER and THING model. I have tried several examples from stackoverflow inside the create and new methods but nothing seems to work.
class User < ApplicationRecord
has_many :created_things, class_name: Thing, foreign_key:
:creator_id, :dependent => :destroy
has_many :extras, through: :created_things
accepts_nested_attributes_for :extras, :reject_if => :all_blank,
allow_destroy: true
class Thing < ApplicationRecord
belongs_to :creator, class_name: User
has_many :extras
accepts_nested_attributes_for :extras, :reject_if => :all_blank,
allow_destroy: true
class Extra < ApplicationRecord
belongs_to :creator, class_name: User, inverse_of: :thing
belongs_to :created_things
Members Index.html.erb
<% if thing.extras.exists? %>
<% thing.extras.each do |extra| %>
<%= extra.title %> <%= link_to "[+]", edit_extra_path(extra) %>
<% end %>
<% else if thing.extras.empty? %>
<%= link_to "+1 EXTRA", new_extra_path(current_user) %>
<% end %>
<% end %>
class MembersController < ApplicationController
before_action :authenticate_user!
def index
#user = current_user
#created_extras = #user.extras
#created_things = #user.created_things
end
class ExtrasController < ApplicationController
def new
#extra = Extra.new
end
def create
#extra = current_user.extras.build(extra_params)
if #extra.save
I am able to create a new EXTRA but the :thing_id remains nul as it does not display when called on the show extra view. Therefore I am not surprised that when I return to the member index page that my thing.extras.exists? call is returning false and the created extra never displays under the THING view. My attempts to modify the extra controller have failed and I some of my reading sugested the extras controller is not necessary in this relationship so I am really at a loss on how this is built. I'm assuming I am missing something in new and create methods maybe in things or user controller? Perhaps I'm missing something in routes resources? Any advice is greatly appreciated. Thank you.
Ok, I figured it out. I really didn't need has many through for this model and I did a lot of testing of the syntax on each model.rb and in the end was able to figure it out from this stackoverflow . . .
[Passing parent model's id to child's new and create action on rails
Here are my the various parts of setting up a has many and belongs to relationship with nested attributes.
class Thing < ApplicationRecord
belongs_to :creator, class_name: User
has_many :extras, inverse_of: :thing, :dependent => :destroy
accepts_nested_attributes_for :extras, allow_destroy: true
class Extra < ApplicationRecord
belongs_to :thing, inverse_of: :extras
extras_controller.rb
class ExtrasController < ApplicationController
def new
#extra = Extra.new(thing_id: params[:thing_id])
end
def create
#user = current_user
#extra = Extra.new(extra_params)
#extra.user_id = #user.id
if #extra.save
flash[:success] = "You have added a new Extra!"
redirect_to #extra #extras_path later
else
flash[:danger] = "The form contains errors"
render :new
end
end
edit.html.erb things
<% if #thing.extras.exists? %>
<p>current extras associated with <%= #thing.title %>: </p>
<% #thing.extras.each do |extra| %>
<p><%= extra.title %> <%= link_to "[+]", edit_extra_path(extra) %>
/ <%= link_to "[-]", extra_path(extra), method: :delete %> </p>
<% end %>
<% end %>
<%= link_to "+1 EXTRA", new_extra_path(thing_id: #thing.id) %>
<%= render 'things/form' %>

How can I efficiently order eager loaded records?

I have a model Playlist, and a model User, both of which have_many of each other, through a join model PlaylistUser.
On my playlists#show action, I want to print a list of all of a Playlist's Users, along with the first two Playlists (ordered by :song_count) associated with each of those Users. To make this only one query, I eager loaded the Playlist table along with the Users.
Right now here's what I have:
playlists/show.html.erb
<% #playlist = Playlist.find(params[:id]) %>
<% #playlist_users = #playlist.users.includes(:playlists)
<% #playlist_users.each do |user| %>
<%= user.name %>
<%= user.playlists.order(:song_count).reverse.first.name %>
<%= user.playlists.order(:song_count).reverse.second.name %>
<% end %>
models
class User < ActiveRecord::Base
has_many :playlist_users
has_many :playlists, :through => :playlist_users
end
class PlaylistUser < ActiveRecord::Base
belongs_to :playlist
belongs_to :user
end
class Playlist < ActiveRecord::Base
has_many :playlist_users
has_many :users, :through => :playlist_users
end
When I remove the ordering, the query is extremely fast. But with the ordering, it's very slow, because the database apparently has to query each Playlist before it can order them.
Can I order the Playlists in the original query?
actually when you do:
user.playlists.order(:song_count).reverse
You dont leverage the eagerload, you redo queries each time.
Thanks to eagerloading you have the collection in memory, so you can sort it using ruby method like sort:
user.playlists.sort_by {|pl| -pl.song_count }

Can't mass assign protected attributes in has_many belongs_to association

Am using rails 3.2.13 and I have models for two entities like so
class Restaurant < ActiveRecord::Base
attr_accessible :description, :menu, :restaurant_name
has_many :cuisines
end
class Cuisine < ActiveRecord::Base
attr_accessible :cuisine_name, :restaurant_id
attr_accessible :cuisine_ids
belongs_to :restaurant
end
The controller action for creating a restaurant look like this
I have a form for creating a restaurant using simple form gem like so
<%= simple_form_for #restaurant do |f| %>
<%= f.input :restaurant_name %>
<%= f.input :description %>
<%= f.input :menu %>
<%= f.association :cuisines, label_method: :cuisine_name %>
<%= f.button :submit %>
<% end %>
Am basically suppose to chose from a group of cuisines which simple form helps with. However when i select the cuisine and try to create the restaurant. It brings back the error.
ActiveModel::MassAssignmentSecurity::Error at /restaurants
Can't mass-assign protected attributes: cuisine_ids
As you can see in the model. I placed attribute as accessible but it didn't work. I even tried the singular version cuisine_id with no luck. I have no idea what is wrong? I would prefer not to tamper with the rails defaults for protecting against mass assignment. Any clues?
Cuisine doesn't have cuisine_ids, Restaurant does.
Move your attr_accessible :cuisine_ids into the Restaurant model.

How to create related models within a form in rails

Newbie question here.
I have two models which are related to each other:
class Relationship < ActiveRecord::Base
...
attr_accessible :source_item_id, :target_item_id
belongs_to :target_item, :class_name => "Item"
belongs_to :source_item, :class_name => "Item"
belongs_to :user
...
end
and:
class Item < ActiveRecord::Base
...
attr_accessible :address
...
end
Now, within my form, I already know the source_item_id. I want to be able to enter an address into the form, and create both a target_item, and the associated Relationship.
<%= form_for #new_relationship do |f| %>
<% #new_relationship.source_item_id = #current_item.id %>
<%= f.hidden_field :source_item_id %>
<%= f.submit "New Relationship" %>
<% end %>
You generally do the relationship in the controller and let the form just collect the data. If you are asking how to have a form with two models, check out this post here. I hope I understood your question right !!!

How do I set up to has_many/belongs_to assocations so that only one combination is allowed?

I have three tables, applicants, users and ratings. The basic idea is that each applicant gets assigned a rating by any number of users. This part I have working without any problems. However, if a user goes to edit their rating (which includes a score), the form adds a second rating. I need to change things so that each user can only assign one rating for a given applicant.
class Applicant < ActiveRecord::Base
has_many :ratings
accepts_nested_attributes_for :ratings, :allow_destroy => true
class User < ActiveRecord::Base
has_many :ratings
The rating table just contains an applicant_id, a user_id and a score.
class Rating < ActiveRecord::Base
belongs_to :user
belongs_to :applicant
validates_uniqueness_of :applicant_id, :scope => :user_id
The rating validation makes sure that a second rating is not accepted, but I need to change the associations (or the form) so that a second score option never appears.
My applicant form:
<%= f.fields_for :ratings do |builder| %>
<%= builder.collection_select :score, Rating::SCORES, :to_s, :humanize %>
<%= builder.hidden_field :user_id, :value => current_user.id %>
<%= builder.hidden_field :applicant_id, :value => #applicant.id %>
<% end %>
How do I specify (in the applicant model, I'd guess, since that's the form I'm editing) that the applicant_id, user_id combo in the ratings table has to be unique?
I ended up getting things working by doing the following.
In my applicant controller, I changed the edit method to:
def edit
#applicant = Applicant.find(params[:id])
#my_rating = Rating.where(:applicant_id => params[:id]).where(:user_id => current_user.id)
if #my_rating.empty?
#my_rating = #applicant.ratings.build
end
end
Since I can't make a scope using the current_user, I figured it would work here. Then, in the form, I changed the nested attribute fields to:
<%= f.fields_for :ratings, #my_rating do |builder| %>
<%= builder.collection_select :score, Rating::SCORES, :to_s, :humanize %>
<% if builder.object.user_id.nil?%>
<%= builder.hidden_field :user_id, :value => current_user.id %>
<% end %>
<% end %>
The other important bit is the uniqueness validation in the ratings model above. That made sure that each user could have only one rating per applicant.
This seems to work fine for me, but if anyone has suggestions on how to improve this, I'd love to hear them.