Rails associations nesting - ruby-on-rails

I have three tables that are created by a non rails application. The following models are just the associations for the tables.
class Client < ActiveRecord::Base
has_many :programs
has_many :locations, :through => :programs
end
class Program < ActiveRecord::Base
belongs_to :location
belongs_to :client
end
class Location < ActiveRecord::Base
has_many :programs
has_many :clients, :through => :programs
end
I would like a simpler way to list out all the programs by location for a client.
I thought the following code would work, but it returns all programs for that location, and that completely makes sense.
#client.locations.each do |location|
<h2>location.name</h2>
<ul>
location.programs.each do |program|
<li>program.name</li>
end
</ul>
end
I'm currently using
#client.locations.each do |location|
<h2>location.name</h2>
<ul>
Program.where(:location_id => location.id, :client_id => #client.id).each do |program|
<li>program.name</li>
end
</ul>
end
But this is against MVC principals and is ugly.
I'm not sure how to write this code elegantly, and would appreciate input.

You can use includes method for eager loading the data. Also, using include for this situation will create a join query that will fetch only those programs which are related to both location and client.
#client.locations.includes(:programs).each do |location|
<h2>location.name</h2>
<ul>
location.programs.each do |program|
<li>program.name</li>
end
</ul>
end
Though you should avoid using database queries in view files. So you can move #locations = #client.locations.includes(:programs) to your controller action and use #locations in view to maintain the structure.

You can do this
#client.programs.each do |program|
<h2>program.location.name</h2>
<ul>
<li>program.name</li>
end
</ul>
end

Related

How to loop through a joined table

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 %>

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 active record associations, nested models

Been going through http://guides.rubyonrails.org/association_basics.html but can't seem to get my head around this
I have 4 models: users, listings, comments, commentresponses. Somebody creates a listing, someone else can comment on the listing, then the original creator can respond to the comment.
class User < ActiveRecord::Base
has_many :comments, foreign_key: 'provider'
has_many :listings
has_many :comments
has_many :commentresponses
end
class Listing < ActiveRecord::Base
belongs_to :user
end
class Comment < ActiveRecord::Base
belongs_to :listing
belongs_to :user
has_one :commentresponse
end
class Commentresponse < ActiveRecord::Base
belongs_to :comment
belongs_to :user
end
Everything is working well except I can't access comment.commentresponse; this give a no method error.
Any recommendations of where my logic is wrong?
Associations
I wouldn't use a separate model for CommentResponse; keep it all in the Comment model - using a gem such as ancestry to give a parent / child system to the different comments:
Above is an example of one our Category models - showing how you can order the different associations with the likes of the ancestry gem. The reason why I posted is because this is how you can create responses to your comments, rather than having a separate model:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :listings
has_many :comments
end
#app/models/listing.rb
class Listing < ActiveRecord::Base
belongs_to :user
end
#app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :listing
belongs_to :user
has_ancestry #-> make sure you have "ancestry" column with string in db
end
This basically allows you to use the various methods which ancestry appends to your objects:
Ancestry
I would recommend using the Ancestry gem to store the responses of the comments. You can then add to this by using several partials to provide a nested interface. This way, it will show you the comments you want, with the correct responses etc
IMPORTANT
When using ancestry - you define the parents of the rows with comment_1/comment_2. Many people think you have to just define the "parent"; not true. You have to define the entire "history" of the ancestors of an object
--
Tree
If you go with the ancestry approach, you'll be able to do something like the following:
To achieve this, you can use the nested partial we created here (obviously replace for use with comments):
#app/views/categories/index.html.erb
<%= render partial: "category", locals: { collection: #categories } %>
#app/views/categories/_category.html.erb
<ol class="categories">
<% collection.arrange.each do |category, sub_item| %>
<li>
<!-- Category -->
<div class="category">
<%= link_to category.title, edit_admin_category_path(category) %>
</div>
<!-- Children -->
<% if category.has_children? %>
<%= render partial: "category", locals: { collection: category.children } %>
<% end %>
</li>
<% end %>
</ol>
I know it's not a direct answer to your question; it certainly should help you though

nested resources with shallow routes in rails 3

I have been working with rails for a while but I have yet to overcome the problem of using 3 level deep nested resources. When I am on the notes page I would like to link course name to the class and course but rails keeps giving me an error.
I have 3 models class, course, and notes. A class has many courses and courses belong to a class. Course has many notes and note belong to a course. I will explain below.
class.rb
has_many :courses
course.rb
belongs_to :class
has_many :schedules
has_many :notes, :through => :schedules
note.rb
has_many :schedules
has_many :courses, :through => :schedules
schedule.rb
belongs_to :course
belongs_to :note
routes.rb
resources :classes, :shallow => true do
resources :courses do
resources :notes
end
end
index.html.erb
<% #notes.each do |note| %>
<% note.courses.each do |course| %>
<%= note_class(course) %>
<% end %>
<% end %>
notes_helper.rb
def note_class(course)
link_to course.course_name, class_course_path(class, course)
end
Shallow routes works great except when rails give me an error 'undefined local variable or method `class' for'. I think my code above is right but I am not sure why it is not working correctly. Any suggestions on how I can get the course to link to a url like so mysite.com/classes/1/course/3?
I realize this question was asked a while back but I figured since it hasn't been 'answered', I'll give it a go.
Couple of things. First, the error you're getting inside the helper method is a result of not having a reference to the class instance. You're only passing the course object as a parameter. This would work given your associations above
notes_helper.rb
def note_class(course)
link_to course.course_name, class_course_path(course.class, course)
end
Second, as mentioned in one of the comments, class is a reserved keyword in Ruby so it would serve you best to avoid using it for your models and associations. Cheers.

Help with associations in Rails

Let me describe what i want to do:
There is a Match model, which should have info about what players and what clans attended in it, with division of home players and clan and away players and clan.
That is pretty easy, but there is another model: Summoner. In each match every player has different summoner and I need to do something like this: Match.find(1).players_home.find(1).Summoner.name to extract which summoner played player 1 in home team.
The point is: each player in each match can play with different summoner.
I hope I described it clearly.
Regards.
I'm not really sure about all your specifications regarding when an association is one or several, but I think something like this could be it:
class Match
has_many :participations
has_many :players, :through => :participations
end
class Participation
belongs_to :match
belongs_to :player
belongs_to :summoner
# also a team attribute to store either "home" or "away"
scope :home, where(:team => "home")
scope :away, where(:team => "away")
end
class Player
belongs_to :clan
has_many :participations
has_many :matches, :through => :participations
end
class Summoner
has_many :participations
end
In this setup every match has several participations. Every participation belongs to the player that is participating and also belongs to a summoner for that player and match. It can then be utilized perhaps like this:
In Controller
#match = Match.find(1)
#home_participations = #match.participations.home
#away_participations = #match.participations.away
In View
<h1>Home Players</h1>
<% #home_participations.each do |p| %>
<p>Player: <%= p.player.name %>, Summoned by: <%= p.summoner.name %></p>
<% end %>
I hope this was at least somewhat what you where going for. Let me know if you are looking for something else.

Resources