Question

UPDATE

OK, I figured it out. I had to call the following for varieties:

<%=h @user.varieties.find_by_product_id(product.id).name %>

Here's my two follow questions:

(1) Is this going to cause problems when editing/deleting records because I'm not calling the join model? I've seen some Ryan Bates videos where he stresses this point, but I'm having trouble trying to reference the join model here. In other words, should the above code by called through user_products?

If I use the following code, which references the join table, I'm only able to get it to display the variety_id from the join table (since there's no name column for varieties in the join table). I'm not sure how to make this code reference the variety_id in the join table, but then go to the variety table to get the actual name of the variety from the "name" column.

<% @user.products.each do |product| %>
   <% @user.user_products.find_by_product_id(product.id).variety_id %>
<% end %> 

(2) Is this complex stuff properly placed in the view layer, or is there a better way to move it to the model or controller?

Thanks.

Original question below is now solved ...

I have the following models:
- users
- products
- varieties - user_products

Here's the real world version of what I'm trying to do. Let's say User is a grocery store. Products are fruits, like apples. And varieties are types of apples, like fuji and mcintosh.

I need to create an app where:

  • A user can add many types of products to his page. The products can include multiple varieties, but the user is NOT required to include any varieties. For example, Topps Grocery Store can add apples to their page. If that's all they want to display, that needs to be ok. However, they can also add more detail by including the types of apples they carry, like fuji, mcintosh, etc. The variety can't just be a detailed product. In other words, I can't make each product be something like apple - fuji, apple - mcintosh. They need to be two separate models.

  • On the user's page (i.e. the "show" view), I need to be able to display both the product and the variety (if any). The system needs to understand that the varieties are connected to the particular product for this particular user.

Following the first answer I received, I revised my models as described in the answer below. Each variety belongs to one product, i.e. fuji belongs to only the apple product, which is a distinct id in the product table. And, a product has many varieties, i.e. the apple product might have 5 or 10 different varieties.

However, it gets more complicated because each user might have a different set of product/variety combinations. For example, Topps grocery store (user) might have apples (product) that are fuji and mcintosh (varieties). But, Publix grocery store (user) might have apples (product) that are red delicious and gala (varieties).

On the user page, I want to be able to call up each product the user carries and then display the varieties connected to each of those products if the user has chosen any varieties.

When I try the code listed below in the show view, I get the following error: undefined method `user_product' for #:

<% @user.products.each do |product| %>
   <% @user.user_products.find_by_product_id(product.id).varieties %>
<% end %>

On the other hand, when I try the other option you gave me (listed below), the page loads and the sql query seems right in the log, but no varieties display on the page, which is weird because I triple checked and there are records in the database that should match the query. Details below...

<% @user.products.each do |product| %>
   <% @user.varieties.find_by_product_id(product.id) %>
<% end %>    

This code runs the following sql query:

User Load (0.7ms) SELECT * FROM "users" WHERE ("users"."id" = 2)

Variety Load (0.5ms) SELECT "varieties".* FROM "varieties" INNER JOIN "user_product" ON "varieties".id = "user_products".variety_id WHERE (("seasons".user_id = 2))

Rendering template within layouts/application
Rendering users/show

Product Load (0.7ms) SELECT "products".* FROM "products" INNER JOIN "user_product" ON "products".id = "user_products".product_id WHERE (("user_products".user_id = 2))

Variety Load (0.4ms) SELECT "varieties".* FROM "varieties" INNER JOIN "user_product" ON "varieties".id = "user_products".variety_id WHERE ("varieties"."product_id" = 1) AND (("user_products".user_id = 2)) LIMIT 1

Variety Load (0.2ms) SELECT "varieties".* FROM "varieties" INNER JOIN "user_products" ON "varieties".id = "user_products".variety_id WHERE ("varieties"."product_id" = 2) AND (("user_products".user_id = 2)) LIMIT 1

In this case above, the user that I'm looking at is user_id=2, he does have product_id=1 and product_id=2 in the database. And, in the user_products table, I do have a few records that list this user_id connected to each of these product_ids and associated with some variety_ids. So it seems like I should be displaying some results on my show view, but I'm getting nothing.

Finally, when I try the following:

<% @user.products.each do |product| %>
   <%=h @user.user_products.find_by_product_id(product.id) %>
<% end %>

It displays the following in my views for each record: #<User_product:0x4211bb0>

Was it helpful?

Solution

They way I see it is that you don't have a many to many relationship to the products. You have it to the product of that particulary variety (if any). So in order to keep track of it, I would use an extra model. That way you would be able to keep the relationship between products and varieties as well.

The models:

class User < ActiveRecord::Base
  has_many :user_products, :dependent => :destroy
  has_many :products, :through => :user_products
  has_many :varieties, :through => :user_products
end

class UserProduct < ActiveRecord::Base
  belongs_to :user
  belongs_to :product
  belongs_to :variety
end

class Product < ActiveRecord::Base
  has_many :varieties
end

class Variety < ActiveRecord::Base
  belongs_to :product
end

The controller:

class UserProductsController < ApplicationController
    before_filter do 
        @user = User.find(params['user_id'])
    end


    def create
      product = Product.find(params['product_id'])
      variety = Variety.find(params['variety_id']) if params['variety_id']

      @user_product = UserProduct.new
      @user_product.user = user
      @user_product.product = product
      @user_product.variety = variety
      @user_product.save
    end

    def destroy
        # Either do something like this:
        conditions = {:user_id => @user.id}
        conditions[:product_id] = params[:product_id] if params[:product_id]
        conditions[:variety_id] = params[:variety_id] if params[:variety_id]

        UserProduct.destroy_all conditions
    end
end

The views: If you are not interested in grouping the varieties into the different products and just put them as a list it is enough with this:

# users/show.html.erb
<%= render @user.user_products %>

# user_products/_user_product.html.erb
<%= h user_product.product.name %> 
<%= h user_product.variety.name if user_product.variety %>

Otherwise something a bit more complicated have to be added. Some of it might be possible to put in the model and controller by using association proxies, but I can't help you there

# users/show
<% for product in @user.products do %>
    <%= product.name %>
    <%= render :partial => 'variety', 
        :collection => @user.varieties.find_by_product_id(product.id) %>
<% end %>

# users/_variety
<%= variety.name %> 

Splitting it up in partials is of course not necessary (and in this example, maybe a bit ridiculous), but it helps in separating the different parts, especially if you want to add more things to display.

And to answer your questions:

  1. Since UserProduct is a join model, you don't really need to keep track of it's individual id. You have the User, the Product and the Variety (in those cases it exists). That's all you need, that combination is unique and thus, you can find the record for deletion. Also, a join model should never be edited (provided that you don't have more attributes in it than these fields), It is either create or delete, since all it do is keep the associations.
  2. Since it is basically a viewing thing it is best to place it in the views. You could of course use the controller or the model to collect all the products and the varieties beforehand by doing something like this:

Moving all of the logic to the controller and the model is probably not a good idea since they are not supposed to know (or care) how you want to display the data. So it is only a matter of preparing it enough.

@products_for_user = []
@user.products.each do |product|
 collected_product = {:product => product, 
                      :varieties => @user.varieties.find_by_product_id(product.id)}
  @products_for_user << collected_product
end

OTHER TIPS

MikeH

You might just have a typo here, @user.user_product should be @user.user_products

When I try the code listed below in the show view, I get the following error: undefined method `user_product' for #:

<% @user.products.each do |product| %>  
  <% @user.user_product.find_by_product_id(product.id).varieties %>  
<% end %>

Also you might consider a hierarchical type system, where you just have products like "Fuji", "Macintosh", and "Apples".

"Fuji" and "Macintosh" would then have a column "parent_id" set to the id of "Apples".

Just a thought.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top