Rail Application Model/Controller & Routrs organisation - ruby-on-rails

I'm trying to work on this Rails app which has the following objectives:
/foods/ - render a list of food categories (eg: Breads, Dairy, Biscuits...etc)
/foods/breads/ - render all Foods that are within the food category "Breads"
foods/breads/bagel - render a detailed view of the properties of the Food (in this example a Bagel).
Currently I have two models with associated controllers:
Foods - contains a list of foods (eg: bagel, rice, toast, rich tea biscuit...etc) and is set up to belongs_to a single Food Cat
Food Categories - a list of categories such as "Dairy", "Breads"...etc & is set up to has_many :foods
I'm really stuck on how to achieve my objectives. I really need advice on routing, controller actions and views.
Any suggestions?

In your routes.rb file, I would do the following:
match 'foods' => 'FoodCategories#index'
match 'foods/:category' => 'Foods#index'
match 'foods/:category/:name' => 'Foods#show'
I would then create a scope for Foods by category:
class Food
scope :by_category, lambda{ |category| joins(:categories).where('categories.name = ?', category) }
end
I would then have 2 actions in your FoodsController:
class FoodsController
def index
#foods = Food.by_category(params[:category])
end
def show
#foods = Food.by_category(params[:category]).where('foods.name = ?', params[:name])
end
end
And a single action in your FoodCategoriesController:
class FoodCategories
def index
#categories = Category.where(name: params[:category])
end
end
That should leave you with having to implement 3 views: categories/index, foods/index and foods/show.

You should have a FoodsController and a FoodCategoriesController dealing with Food and FoodCategory models.
if you follow the RESTful approache, then the routes neccessary to achieve the url configuration you listed will be as follows:
match '/foods' => 'food_categories#index'
match '/foods/:category_id' => 'food_categories#show'
match '/foods/:category_id/:food_id' => 'foods#show'
Your FoodCategoriesController will have methods index method which lists all the categories by performing FoodCategory.find :all lookup, as well as show method which will lookup a FoodCategory based on provided :category_id and display all the foods associated with it via has_many relationship.
Your FoodController will have a show method that will at least take the :food_id and look up the Food instance associated with it. :category_id is not really neccessary here, but its a nice routing sugar.

Related

Rails & ActiveRecord complex query: rendering a collection of has_many through objects while substituting one column

I have a List model. A List has_many :lists_movies and has_many :movies through :lists_movies and has_many :trailers through :movies. ListsMovie has a list_id, movie_id, and title columns.
I want to render all the trailers of movies in a given list, populating all the trailer data AND the list_movie.title. What is the best way to accomplish this?
If I just wanted to render the trailers, I could go:
#trailers = #list.trailers
#trailers.each do |t|
t.description
t.youtube_id
end
But I also want the title from the list_movie like:
#trailers.each do |t|
list_movie.title
t.description
t.youtube_id
end
Basically I'm trying to iterate and get data from 2 different objects.. but having trouble on which collections I should be loading.
Try writing out the query in English: "For each list, I want to write down the list_movie title, and each trailer for that movie", which, in ruby, now easily translates to:
#list.lists_movies.each do |list_movie|
# list_movie.title
list_movie.movie.trailers.each do |trailer|
# trailer.description
# trailer.youtube_id
end
end

Passing parameter from view to controller for multiple iteration

My controller is retrieving the follow:
def index
#segments = Segment.all
#products = Product.all
end
But it is not what I want. I want to iterate products based on its segment_id. Something like this:
def index
#segments = Segment.all
#products = Product.where(segment_id: x)
end
The problem is: how to pass x from view to controller?
My view is:
- #segments.each do |s|
- #products.each do |p|
This is a product from #{s.name}.
Ok, where's the issue? I'm not showing specific products of specific segments. I'm showing products of all segments. What I need is something like this:
- #segments.each do |s|
- #products(segment_id: s.id).each do |p|
This is a product from #{s.name}.
Can you understand?
Set up has_many association between Segment and Product model.
class Segment < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :segment
end
Add segment_id in the products table.
Generate the migration with
rails g migration add_segment_id_to_products segment_id:integer:index
Run rake db:migrate
Setting up the association would give you dynamic method products for an instance of segment which you can use in the view for iteration.
Then update the index action as:
def index
#segments = Segment.all
end
Update the view as:
- #segments.each do |s|
- s.products.each do |p|
This is a product from #{s.name}.
Would this do?
def index
#segments = Segment.includes(:products).all
end
And in your view:
- #segments.each do |s|
- s.products.each do |p|
This is product #{p.name} from segment #{s.name}
You can add where conditions to the query for both segmens and products if you want to filter them in any way you need like this.
Segment.includes(:products).where(:seg_val => 'something', :product => [:prod_val => 'other']).all

Eager load differently nested polymorphic

Using Rails 3.2. My models are nested in such a way:
Review => Reviewable (Country or Shop)
Country => CountryDay => Shop => Photos
Shop => Photos
I have the following:
#reviews = #user.reviews.includes(:user, :reviewable)
Usually we can include nested polymorphic in such a way:
# this will return errors because :shop is not found in the model Shop (:reviewable is actually :shop)
#reviews = #user.reviews.includes(:user, :reviewable => [:shop])
# this will return errors because :photos is not directly associated to Country
#reviews = #user.reviews.includes(:user, :reviewable => :photos)
There are many other variants. How do I work around it to have the ActiveRecord includes the correct model based on its association?
I think I had the same problem just now. I got an ActiveRecord::EagerLoadPolymorphicError when trying to load associations. My solution was to put together a custom join statement which I've put into a scope for easier usage.
Untested, but this may get you going:
class Review < ActiveRecord::Base
scope :eagerly_loaded, -> {
joins('
INNER JOIN shops
ON (reviews.reviewable_type = "Shop" AND reviews.reviewable_id = shops.id)
INNER JOIN countries
ON (reviews.reviewable_type = "Country" reviews.reviewable_id = countries.id)
')
}
end
You then use #user.reviews.eagerly_loaded. Make sure to use a matching .group()-statement to only get the ones you need.

Rails HABTM joining with another condition

I am trying to get a list, and I will use books as an example.
class Book < ActiveRecord::Base
belongs_to :type
has_and_belongs_to_many :genres
end
class Genre < ActiveRecord::Base
has_and_belongs_to_many :books
end
So in this example I want to show a list of all Genres, but it the first column should be the type. So, if say a genre is "Space", the types could be "Non-fiction" and "Fiction", and it would show:
Type Genre
Fiction Space
Non-fiction Space
The Genre table has only "id", "name", and "description", the join table genres_books has "genre_id" and "book_id", and the Book table has "type_id" and "id". I am having trouble getting this to work however.
I know the sql code I would need which would be:
SELECT distinct genres.name, books.type_id FROM `genres` INNER JOIN genres_books ON genres.id = genres_books.genre_id INNER JOIN books ON genres_books.book_id = books.id order by genres.name
and I found I could do
#genre = Genre.all
#genre.each do |genre|
#type = genre.book.find(:all, :select => 'type_id', :group => 'type_id')
#type.each do |type|
and this would let me see the type along with each genre and print them out, but I couldn't really work with them all at once. I think what would be ideal is if at the Genre.all statement I could somehow group them there so I can keep the genre/type combinations together and work with them further down the road. I was trying to do something along the lines of:
#genres = Genre.find(:all, :include => :books, :select => 'DISTINCT genres.name, genres.description, books.product_id', :conditions => [Genre.book_id = :books.id, Book.genres.id = :genres.id] )
But at this point I am running around in circles and not getting anywhere. Do I need to be using has_many :through?
The following examples use your models, defined above. You should use scopes to push associations back into the model (alternately you can just define class methods on the model). This helps keep your record-fetching calls in check and helps you stick within the Law of Demeter.
Get a list of Books, eagerly loading each book's Type and Genres, without conditions:
def Book < ActiveRecord::Base
scope :with_types_and_genres, include(:type, :genres)
end
#books = Book.with_types_and_genres #=> [ * a bunch of book objects * ]
Once you have that, if I understand your goal, you can just do some in-Ruby grouping to corral your Books into the structure that you need to pass to your view.
#books_by_type = #books.group_by { |book| book.type }
# or the same line, more concisely
#books_by_type = #books.group_by &:type
#books_by_type.each_pair do |type, book|
puts "#{book.genre.name} by #{book.author} (#{type.name})"
end

Rails ActiveRecord - Best way to perform an include?

I have three models:
class Book < ActiveRecord::Base
has_many :collections
has_many :users, :through => :collections
end
class User < ActiveRecord::Base
has_many :collections
has_many :books, :through => :collections
end
class Collection < ActiveRecord::Base
belongs_to :book
belongs_to :user
end
I'm trying to display a list of the books and have a link to either add or remove from the user's collection. I can't quite figure out the best syntax to do this.
For example, if I do the following:
Controller
class BooksController < ApplicationController
def index
#books = Book.all
end
end
View
...
<% if book.users.include?(current_user) %>
...
or obviously the inverse...
...
<% if current_user.books.include?(book) %>
...
Then queries are sent for each book to check on that include? which is wasteful. I was thinking of adding the users or collections to the :include on the Book.all, but I'm not sure this is the best way. Effectively all I need is the book object and just a boolean column of whether or not the current user has the book in their collection, but I'm not sure how to forumlate the query in order to do that.
Thanks in advance for your help.
-Damien
I have created a gem(select_extra_columns) for returning join/calculated/aggregate columns in a ActiveRecord finders. Using this gem, you will be able to get the book details and the flag indicating if the current user has the book in one query.
In your User model register the select_extra_columns feature.
class Book < ActiveRecord::Base
select_extra_columns
has_many :collections
has_many :users, :through => :collections
end
Now in your controller add this line:
#books = Book.all(
:select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user",
:extra_columns => {:belongs_to_user => :boolean},
:joins => "LEFT OUTER JOIN collections
ON book.id = collections.book_id AND
collections.user_id = #{current_user.id}"
)
Now in your view you can do the following.
book.belongs_to_user?
You're going to to want 2 SQL queries, and O(1) based lookups (probably irrelevant, but it's the principle) to check if they have the book.
The initial calls.
#books = Book.all
#user = User.find(params[:id], :include => :collections)
Next, you're going to want to write the books the user has into a hash for constant time lookup (if people won't ever have many books, just doing an array.include? is fine).
#user_has_books = Hash.new
#user.collections.each{|c|#user_has_books[c.book_id] = true}
And on the display end:
#books.each do |book|
has_book = #user_has_books.has_key?(book.id)
end
I'd err away from caching the book_ids on the user object, simply because going this route can have some funny and unexpected consequences if you ever start serializing your user objects for whatever reason (i.e. memcached or a queue).
Edit: Loading intermediary collection instead of double loading books.
Essentially you need to make one call to get the book information and the Boolean flag indicating if the current user has the book. ActiveRecord finders doesn't allow you to return the join results from another table. We work around this problem by doing a trick.
In your Book model add this method.
def self.extended_book
self.columns # load the column definition
#extended_user ||= self.clone.tap do |klass|
klass.columns << (klass.columns_hash["belongs_to_user"] =
ActiveRecord::ConnectionAdapters::Column.new(
"belongs_to_user", false, "boolean"))
end # add a dummy column to the cloned class
end
In your controller use the following code:
#books = Book.extended_book.all(
:select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user",
:joins => "LEFT OUTER JOIN collections
ON book.id = collections.book_id AND
collections.user_id = #{current_user.id}"
)
Now in your view you can do the following.
book.belongs_to_user?
Explanation:
In the extended_book method you are creating a copy of Book class and adding a dummy column belongs_to_user to the hash. During the query extra join column is not rejected as it exists in the columns_hash. You should use the extended_book only for querying.
If you use it for CRUD operations DB will throw error.
I would first create an instance method in the User model that 'caches' the all the Book ID's in his collection:
def book_ids
#book_ids ||= self.books.all(:select => "id").map(&:id)
end
This will only execute the SQL query once per controller request. Then create another instance method on the User model that takes a book_id as a parameter and checks to see if its included in his book collection.
def has_book?(book_id)
book_ids.include?(book_id)
end
Then while you iterate through the books:
<% if current_user.has_book?(book.id) %>
Only 2 SQL queries for that controller request :)
Use exists? on the association as it is direct SQL call. The association array is NOT loaded to perform these checks.
books.users.exists?(current_user)
This is the SQL executed by Rails.
SELECT `users`.id FROM `users`
INNER JOIN `collections` ON `users`.id = `collections`.user_id
WHERE (`users`.`id` = 2) AND ((`collections`.book_id = 1)) LIMIT 1
In the above SQL current_user id = 2 and book id is 1
current_user.books.exists?(book)
This is the SQL executed by Rails.
SELECT `books`.id FROM `books`
INNER JOIN `collections` ON `books`.id = `collections`.book_id
WHERE (`books`.`id` = 3) AND ((`collections`.user_id = 4)) LIMIT 1
In the above SQL current_user id = 4 and book id is 3
For more details, refer to the documentation of the exists? method in a :has_many association.
Edit: I have included additional information to validate my answer.

Resources