matthewalexander matthewalexander - 4 months ago 26
Ruby Question

Rails 4, has_many through, return a list of available items for a particular category

I'm wondering if I should create a category controller in this case. Presently I have an items controller that has #index and #show actions. I was thinking about adding a filter for a specific category in the items#index action - but it seems much easier to do from a category controller. Here are my associations which effectively sets up a has_many through relationship between items and categories:

class Category < ActiveRecord::Base
has_many :categorizations
has_many :items, :through => :categorizations
end

class Item < ActiveRecord::Base
has_many :categorizations
has_many :categories, :through => :categorizations
end

class Categorization < ActiveRecord::Base
belongs_to :item
belongs_to :category
end


I need the API I am building to return "a list of available items for a particular category". It is very easy to this from a category controller(which I do not have yet) like so:

category.items


Is it better and more restful to do this? Or should I create a filter in my items#index action, which presently looks like this:

# Returns full list of items
def index
@items = Item.all
render json: @items
end


Of course, if you have any ideas that are more efficient/inline with best practices - please let me know!

Thanks!

EDIT - One Solution:

I decided to add a categories controller, so that I can access the available items for a particular category using the following relative path:

categories/:id/available_items


class CategoriesController < ApplicationController
def available_items
@available_items = Category.find(params[:id]).items.available
render json: @available_items
end
end


The criteria was to return all items, that are associated with a particular category, and have a status of 'available'.

EDIT:

I'm finding that
Item.where(category: 1)
isn't returning all items which are categorized under category 1. Please see below the byebug console output:

1: class ItemsController < ApplicationController
2: # Returns full list of items
3: def index
4: @items = Item.all
5: byebug
=> 6: end
7:

(byebug) Item.where(category: 1)
Item Load (0.6ms) SELECT "items".* FROM "items" WHERE "items"."category" = 1
#<Item::ActiveRecord_Relation:0x007fb5d1a37f08>
(byebug) Category.find(1).items
Category Load (0.7ms) SELECT "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT 1 [["id", 1]]
Item Load (1.7ms) SELECT "items".* FROM "items" INNER JOIN "categorizations" ON "items"."id" = "categorizations"."item_id" WHERE "categorizations"."category_id" = $1 [["category_id", 1]]
#<ActiveRecord::Associations::CollectionProxy [#<Item id: 1, title: "Gorgeous Cotton Pants", description: "Dolor dicta suscipit aut cupiditate quia officiis ...", price: 73960, status: 0, published_date: "2016-07-14 05:35:49", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 1>, #<Item id: 5, title: "Sleek Marble Shoes", description: "Qui mollitia corporis qui placeat. Reiciendis ea s...", price: 35146, status: 0, published_date: "2016-07-14 05:45:02", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 1>, #<Item id: 7, title: "Rustic Concrete Lamp", description: "Sit odio non exercitationem. Atque non sapiente vo...", price: 82016, status: 2, published_date: "2016-07-13 00:00:00", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 1>, #<Item id: 10, title: "Awesome Wooden Table", description: "Possimus consequatur nulla. Quidem molestiae volup...", price: 59519, status: 2, published_date: "2016-07-09 00:00:00", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 1>, #<Item id: 12, title: "Lightweight Concrete Bag", description: "Amet ullam assumenda eligendi consectetur quae. Bl...", price: 72081, status: 2, published_date: "2016-07-16 00:00:00", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 2>, #<Item id: 13, title: "Mediocre Plastic Computer", description: "Excepturi modi est non qui iusto. Molestiae offici...", price: 94357, status: 2, published_date: "2016-07-15 00:00:00", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 2>, #<Item id: 15, title: "Incredible Plastic Bag", description: "Vel voluptas ducimus soluta atque voluptatem eum. ...", price: 15661, status: 2, published_date: "2016-07-14 00:00:00", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 2>, #<Item id: 16, title: "Lightweight Iron Watch", description: "Id sequi rerum dolor sit sunt nemo laborum. Omnis ...", price: 65306, status: 4, published_date: "2016-07-11 00:00:00", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 1>, #<Item id: 17, title: "Rustic Linen Chair", description: "Explicabo qui ad nihil. Voluptatem placeat autem. ...", price: 39752, status: 4, published_date: "2016-07-04 00:00:00", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 1>, #<Item id: 18, title: "Mediocre Copper Car", description: "Minus qui ut est non vero saepe. Qui sed quos et v...", price: 87765, status: 4, published_date: "2016-07-05 00:00:00", created_at: "2016-07-17 05:15:07", updated_at: "2016-07-17 05:15:07", seller_id: 1>]>

Answer

Both of your proposed solutions are valid, but let's look at some code:

Option 1 — Rails Defaults

resources :categories do
  resources :items
end

# /categories/42/items/7

On its own, Rails would route this to the ItemsController, which would be responsible for doing something with the category_id that is passed in. If your application architecture/logic doesn't really ask to be done differently, I would start with this approach.

Option 2 — “Show a Category”

resources :categories, :items

class CategoriesController < ApplicationController
  def index
    @categories = Category.all
  end

  def show
    @category = Category.includes(:items).find params[:id]
  end
end

This approach is great if the notion of showing a controller would naturally show its items. This might not be appropriate in your case given the typical nature of many-to-many relationships, but it really depends on the context.

Option 3 — Three Tidy Controllers

If you find that your items#show or items#index methods are starting to get overly conditional, I would look into changing up your routing and adding a controller:

class CategorizedItemsController < ApplicationController
  # ...
end

resources :categories
resources :items
scope '/categories/:category_id/' do 
  resources :items, controller: :categorized_items
end

This is less obvious to another developer joining your project, and starts to make reasoning about your routes a little more challenging, so I wouldn't begin with it. It's a great solution, though, and one that you shouldn't hesitate to adopt if you find your existing controller hierarchy failing to represent your actions within the bounds of RESTful routing.

Cheers!