How to loop through a joined table - ruby-on-rails

The models I have:
Category:
class Category < ApplicationRecord
has_many :categorizations
has_many :providers, through: :categorizations
accepts_nested_attributes_for :categorizations
end
Provider:
class Provider < ApplicationRecord
has_many :categorizations
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations
end
Categorization:
class Categorization < ApplicationRecord
belongs_to :category
belongs_to :provider
has_many :games, dependent: :destroy
accepts_nested_attributes_for :games
end
Game:
class Game < ApplicationRecord
belongs_to :categorization
end
I need to display the games, that belongs to a specific provider. I tried to do it like:
<% #provider.categorizations.joins(:games).each do |game| %>
<%= game.title %>
<% end %>
It gives me an error: NoMethodError: undefined method 'title' for #<Categorization:0x007f2cf6ee49e8>. So, it loops through the Categorization. What is the best way to loop through the joined games table? Thanks.

First, you should do the request in your controller, or even better call a scope (defined in a model) from the controller.
Do not forget that Active Record is just an ORM, a tool allowing you to manipulate SQL.
With #provider.categorizations.joins(:games) you are not asking for games. You are asking for the categorizations and you do a JOIN with the games table. This joins is usually to allow to filter by games attributes.
To do what you want you should do the following :
#games = Game.joins(:categorization).where('categorization.provider_id = ?',#provider.id)
As you can see, the join do not return categorization, it allow me to use categorization as a filter.
You should always be aware of the SQL generated by Active Record. Look at the SQL query generated in your server's traces.

I'm guessing 'title' is an attribute of games and not categorization, so you either need to return an array of games, or add a select on the end to pull the title attribute into the categorization object, like so:
<% #provider.categorizations.joins(:games).select('dba.games.title').each do |game| %>
<%= game.title %>
<% end %>
Just to add- you shouldn't really be doing this in the view file. I'd go as far as not even doing this in the controller. I tend to encapsulate this sort of logic into a service class, which is instantiated in the controller to return a set of results. The controller should only be passing the result set on, which is then presented by the view.
class Provider < ActiveRecrord::Base
# this could be a scope instead, or in a seperate class which
# the provider model delegates to- whatever floats you boat
def get_games
# you could use pluck instead, which would return an array of titles
categorizations.joins(:games).select('dba.games.title')
end
end
class ProviderController < ApplicationController
def show
provider = Provide.find(params[:id])
#games = provider.get_games
end
end
<% #games.each do |game| %>
<%= game.title %>
<% end %>

Related

Can't access field through 3 models in Rails 5

I'm trying to do something very similar to this question
I have 4 models, one of which (CoffeeBlend) is a join table:
class CoffeeRoast < ApplicationRecord
has_many :coffee_blends
has_many :coffee_beans, through: :coffee_blends
has_one :country, through: :coffee_beans
end
class CoffeeBean < ApplicationRecord
has_many :coffee_blends
has_many :coffee_roasts, through: :coffee_blends
belongs_to :country
end
class Country < ApplicationRecord
has_many :coffee_beans
end
class CoffeeBlend < ApplicationRecord
belongs_to :coffee_bean
belongs_to :coffee_roast
end
My coffee_beans table has a column called country_id which is populated with the id from the countries table.
In my coffee_roasts_show I want to be able to pull the associated country of the coffee bean. My latest attempt looks like
<% #coffee_roast.coffee_beans.country.country_name %>
which gives undefined method 'country'
Or
<% #coffee_roast.coffee_beans.countries.country_name %>
returns undefined method 'countries'
Do I have my associations correct? Is my show code wrong?
The method #coffee_roast.coffee_beans returns you association, not a single record. That's why you cannot call #country on that. If you need all the countries, you can use #map:
<% #coffee_roast.coffee_beans.map {|cb| cb.country.country_name } %>
Edit:
If you want to show the list in the browser, add = to your ERB mark:
<%= #coffee_roast.coffee_beans.map {|cb| cb.country.country_name } %>
It may also be useful to explicitly convert contry names to a string with Array#join
<%= #coffee_roast.coffee_beans.map {|cb| cb.country.country_name }.join(', ') %>

Rails has_many relationship with prefilled views

I have a pretty basic Rails 4 app, and am using Cocoon's nested forms to manage the has_many... :through model association.
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
# ... etc
end
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
# ... etc
end
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluation, reject_if: :all_blank
# ... etc
end
When I use Cocoon in the View, I want to use the New Assessment view to pre-fill all the Student records in order to create a new Evaluation for each one. I don't want to have to do some hacky logic on the controller side to add some new records manually, so how would I structure the incoming request? With Cocoon I see that requests have some number in the space where the id would go (I've replaced these with ?? below).
{"utf8"=>"✓", "authenticity_token"=>"whatever", "assessment"=>{"description"=>"quiz 3", "date(3i)"=>"24", "date(2i)"=>"10", "date(1i)"=>"2015", "assessments_attributes"=>{"??"=>{"student_id"=>"2", "grade" => "A"}, "??"=>{"student_id"=>"1", "grade" => "B"}, "??"=>{"student_id"=>"3", "grade"=>"C"}}, }}, "commit"=>"Create Assessment"}
I see in the Coccoon source code that this is somehow generated but I can't figure out how it works with the Rails engine to make this into a new record without an ID.
What algorithm should I use (or rules should I follow) to fill in the id above to make a new record?
"??"
Never a good sign in your params.
With Cocoon I see that requests have some number in the space where the id would go
That ID is nothing more than the next ID in the fields_for array that Rails creates. It's not your record's id (more explained below).
From your setup, here's what I'd do:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
end
#app/models/evaluation.rb
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
end
#app/models/assessment.rb
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluations, reject_if: :all_blank
end
This will allow you to do the following:
#app/controllers/assessments_controller.rb
class AssessmentsController < ApplicationController
def new
#assessment = Assessment.new
#students = Student.all
#students.each do
#assessment.evaluations.build
end
end
end
Allowing you:
#app/views/assessments/new.html.erb
<%= form_for #assessment do |f| %>
<%= f.fields_for :evaluations, #students do |e| %>
<%= e.hidden_field :student_id %>
<%= e.text_field :grade %>
<% end %>
<%= f.submit %>
<% end %>
As far as I can tell, this will provide the functionality you need.
Remember that each evaluation can connect with existing students, meaning that if you pull #students = Student.all, it will populate the fields_for accordingly.
If you wanted to add new students through your form, it's a slightly different ballgame.
Cocoon
You should also be clear about the role of Cocoon.
You seem like an experienced dev so I'll cut to the chase - Cocoon is front-end, what you're asking is back-end.
Specifically, Cocoon is meant to give you the ability to add a number of fields_for associated fields to a form. This was discussed in this Railscast...
Technically, Cocoon is just a way to create new fields_for records for a form. It's only required if you want to dynamically "add" fields (the RailsCast will tell you more).
Thus, if you wanted to just have a "static" array of associative data fields (which is I think what you're asking), you'll be able to use fields_for as submitted in both Max and my answers.
Thanks to #rich-peck I was able to figure out exactly what I wanted to do. I'm leaving his answer as accepted because it was basically how I got to my own. :)
assessments/new.html.haml (just raw, no fancy formatting)
= form_for #assessment do |f|
= f.fields_for :evaluations do |ff|
.meaningless-div
= ff.object.student.name
= ff.hidden_field :student_id, value: ff.object.student_id
= ff.label :comment
= ff.text_field :comment
%br/
assessments_controller.rb
def new
#assessment = Assessment.new
#students = Student.all
#students.each do |student|
#assessment.evaluations.build(student: student)
end
end

Rails, how to avoid the "N + 1" queries for the totals (count, size, counter_cache) in associations?

I have a these models:
class Children < ActiveRecord::Base
has_many :tickets
has_many :movies, through: :tickets
end
class Movie < ActiveRecord::Base
has_many :tickets
has_many :childrens, through: :tickets
belongs_to :cinema
end
class Ticket < ActiveRecord::Base
belongs_to :movie, counter_cache: true
belongs_to :children
end
class Cinema < ActiveRecord::Base
has_many :movies, dependent: :destroy
has_many :childrens, through: :movies
end
What I need now is in the page of "Cinemas" I wanna print the sum (count, size?) of the childrens just for the movies of that cinemas, so I wrote this:
in the cinemas_controller.rb:
#childrens = #cinema.childrens.uniq
in the cinemas/show.html.erb:
<% #childrens.each do |children| %><%= children.movies.size %><% end %>
but obviously I have bullet gem that alert me for Counter_cache and I don't know where to put this counter_cache because of different id for the movie.
And also without the counter_cache what I have is not what I want because I want a count for how many childrens in that cinema taking them from the tickets from many days in that cinema.
How to?
UPDATE
If in my view I use this code:
<% #childrens.each do |children| %>
<%= children.movies.where(cinema_id: #cinema.id).size %>
<% end %>
gem bullet don't say me anything and every works correctly.
But I have a question: this way of querying the database is more heavy because of the code in the views?
This might help you.
#childrens_count = #cinema.childrens.joins(:movies).group("movies.children_id").count.to_a
You can use includes to load all associations ahead of time. For example:
#childrens = #cinema.childrens.includes(:movies).uniq
This will load all of the children's movies in the controller, preventing the view from needing access to the database in your loop.
You might agree, that the number of movies belongs to a child equals the number of tickets they bought.
That's why you could just cache the number of tickets and show it on the cinemas#show.
You can even create a method to make it more clear.
class Children < ActiveRecord::Base
has_many :tickets
has_many :movies, through: :tickets
def movies_count
tickets.size
end
end
class Ticket < ActiveRecord::Base
belongs_to :movie, counter_cache: true
belongs_to :children, counter_cache: true
end
class Movie < ActiveRecord::Base
belongs_to :cinema
has_many :tickets
has_many :childrens, through: :tickets
end
class Cinema < ActiveRecord::Base
has_many :movies, dependent: :destroy
has_many :childrens, through: :movies
end
And then:
<% #childrens.each do |children| %><%= children.tickets.size %><% end %>
Or
<% #childrens.each do |children| %><%= children.movies_count %><% end %>
But if you want to show the number of tickets for every movie, you definitely need to consider the following:
#movies = #cinema.movies
Then:
<% #movies.each do |movie| %><%= movie.tickets.size %><% end %>
Since you have belongs_to :movie, counter_cache: true, tickets.size won't make a count query.
And don't forget to add tickets_count column. More about counter_cache...
P.S. Just a note, according to conventions we name a model as Child and an association as Children.
Actually is much more simpler than the remaining solutions
You can use lazy loading:
In your controller:
def index
# or you just add your where conditions here
#childrens = Children.includes(:movies).all
end
In your view index.hml.erb:
<% #childrens.each do |children| %>
<%= children.movies.size %>
<% end %>
The code above won't make any extra query if you use size but if you use count you will face the select count(*) n + 1 queries
I wrote a little ActiveRecord plugin some time ago but haven't had the chance to publish a gem, so I just created a gist:
https://gist.github.com/apauly/38f3e88d8f35b6bcf323
Example:
# The following code will run only two queries - no matter how many childrens there are:
# 1. Fetch the childrens
# 2. Single query to fetch all movie counts
#cinema.childrens.preload_counts(:movies).each do |cinema|
puts cinema.movies.count
end
To explain a bit more:
There already are similar solutions out there (e.g. https://github.com/smathieu/preload_counts) but I didn't like their interface/DSL. I was looking for something (syntactically) similar to active records preload (http://apidock.com/rails/ActiveRecord/QueryMethods/preload) method, that's why I created my own solution.
To avoid 'normal' N+1 query issues, I always use preload instead of joins because it runs a single, seperate query and doesn't modify my original query which would possibly break if the query itself is already quite complex.
In You case You could use something like this:
class Ticket < ActiveRecord::Base
belongs_to :movie, counter_cache: true
belongs_to :children
end
class Movie < ActiveRecord::Base
has_many :tickets
has_many :childrens, through: :tickets
belongs_to :cinema
end
class Children < ActiveRecord::Base
has_many :tickets
has_many :movies, through: :tickets
end
class Cinema < ActiveRecord::Base
has_many :movies, dependent: :destroy
has_many :childrens, through: :movies
end
#cinema = Cinema.find(params[:id])
#childrens = Children.eager_load(:tickets, :movies).where(movies: {cinema_id: #cinema.id}, tickets: {cinema_id: #cinema.id})
<% #childrens.each do |children| %>
<%= children.movies.count %>
<% end %>
Your approach using counter_cache is in right direction.
But to take full advantage of it, let's use children.movies as example, you need to add tickets_count column to children table firstly.
execute rails g migration addTicketsCountToChildren tickets_count:integer,
then rake db:migrate
now every ticket creating will increase tickets_count in its owner(children) by 1 automatically.
then you can use
<% #childrens.each do |children| %>
<%= children.movies.size %>
<% end %>
without getting any warning.
if you want to get children count by movie, you need to add childrens_count to movie table:
rails g migration addChildrensCountToMovies childrens_count:integer
then rake db:migrate
ref:
http://yerb.net/blog/2014/03/13/three-easy-steps-to-using-counter-caches-in-rails/
please feel free to ask if there is any concern.
Based on sarav answer if you have a lot of things(requests) to count you can do:
in controller:
#childrens_count = #cinema.childrens.joins(:movies).group("childrens.id").count.to_h
in view:
<% #childrens.each do |children| %>
<%= #childrens_count[children.id] %>
<% end %>
This will prevent a lot of sql requests if you train to count associated records

rails create bidirectional relation before_create

I build my relations:
# models
class Lead < ActiveRecord::Base
has_many :practices, through: :lead_practices
accepts_nested_attributes_for :practices
end
class Practice < ActiveRecord::Base
has_many :leads, through: lead_practices
end
# leads controller
def create
#lead_profile = LeadProfile.new lead_profile_params
puts "lead practice #{#lead.practices.first}"
puts " practice lead #{#lead.practices.first.lead.first}"
end
# view:
<%= form_for #lead do |f| %>
<%= f.text_field :something %>
<%= f.fields_for :practices do |practice_builder| %>
<%= practice_builder.text_field :something_else %>
<% end %>
<% end %>
The problem is in the create action, I can access the practice through lead but I cannot access the lead through practice. This becomes an issue when I want to access the lead through practice in the practice's before_create callback:
class Practice < ActiveRecord::Base
before_create :do_something
def do_something
lead.first.do_something # raises an exception because practices is nil, even though it should be populated with the relation
This seems like a common use case. How can I access the reciprocal relation?
Have you tried has_and_belongs_to_many instead of has_many :through? That is:
class Lead < ActiveRecord::Base
has_and_belongs_to_many :practices
end
class Practice < ActiveRecord::Base
has_and_belongs_to_many :leads
end
I don't know whether this will improve your situation, but it seems like it's worth a shot, as writing it this way means that ActiveRecord doesn't have to work as hard to understand the inverse relationship between the two associations, which seems key here.

is there better way to count for three level nested forms

I have a nested forms like:
class House < ActiveRecord::Base
has_many :rooms
accepts_nested_attributes_for :rooms
attr_accessible :rooms_attributes
end
class Room < ActiveRecord::Base
has_one :tv
accepts_nested_attributes_for :tv
attr_accessible :tv_attributes
end
class Tv
belongs_to :user
attr_accessible :manufacturer
validates_presence_of :user
end
Now, I want to know for house.id = 1 how many rooms and tvs totally.
In the houses_controller I gave
#houses = House.all
And it's quit simple to get the room count for each house like
<% for house in #houses %>
<%= house.rooms.count %>
<% end -%>
My question is how to get tvs count? I am using this now
<%= house.rooms.map {|room| room.tvs.count}.sum %>
It works, but I am not sure this is good or not.
Is there any better way to get it?
I'd put a method in the model, trying to avoid code in the views.
class House
...
def tvs
rooms.inject(0) {|r, t| t + r.tvs }
end
end
class Room
...
def tvs
tv ? 1 : 0 # it's has_one association right now
end
end
Also, if in your controller your are loading all House's objects, and after that you are going to need the Rooms objects, you should load the houses like:
House.find :all, :include => { :rooms => :tv }
This way you are going to do 1 query, with your approach there will be 1 + N_rooms + N_tvs queries

Resources