问题
I am new to programming & rails and there's something I dont fully understand. I am creating an app with
product has_many categories
category has_many products
If I understand correctly I need to create a join table products_categories which has a product_id
& a category_id
. First do I also need a model for this table ? if yes I guess it would look like this :
class CategoryProduct < ActiveRecord::Base
belongs_to :category
belongs_to :product
end
and the other models in product.rb :
class Product < ActiveRecord::Base
has_many :category_products
has_many :categories, through: :category_product
has_attached_file :picture,
styles: { medium: "300x300>", thumb: "100x100>" }
validates_attachment_content_type :picture,
content_type: /\Aimage\/.*\z/
validates :price, presence: { message: "Merci d'indiquer le prix du produit" }
validates :name, presence: { message: "Merci d'indiquer le nom du produit" }
validates :weight, presence: { message: "Merci d'indiquer le poids du produit" }
validates :description, presence: { message: "Merci d'écrire une description du produit " }
end
and in category.rb
class Category < ActiveRecord::Base
has_many :category_products
has_many :products, through: :category_product
validates :name, presence: { message: "Merci d'indiquer le nom de la catégorie" }
end
Now let's say I want to create a product and while I am creating it, specify as many categories as I want for this product from the categories' list.
So far this was my Product/new.html.slim in my views :
div class="container marged-top"
div class= "col-xs-12 col-md-offset-3 col-md-5 bigmarge"
div class="panel panel-default"
div class= "panel-heading"
h4 Création Produit
div class= "panel-body"
=simple_form_for @product, html: { multipart: true } do |t|
= t.error_notification
= t.input :name, label: 'Nom'
= t.input :description, label: 'Description', required: true
= t.input :price, label: 'Prix', required: true
= t.input :weight, label: 'Poids', required: true
= t.label :picture
= t.file_field :picture
= t.association :categories, as: :check_boxes
= t.button :submit, value: "Valider", class: "btn-success marge-bas"
This was a simple form for my Product instance. I guess I need to have a form for a CategoryProduct now ? How do I have to change that if I want the user to be able to add as many categories he wants to the product while he's creating it ?
Here's my migration file for category_product table :
class CreateTableCategoriesProducts < ActiveRecord::Migration
def change
create_table :categories_products do |t|
t.references :product, index: true
t.references :category, index: true
end
add_foreign_key :categories_products, :categories
add_foreign_key :categories_products, :products
end
end
I renamed the previous table with the following migration file :
class RenameTableCategoriesProducts < ActiveRecord::Migration
def self.up
rename_table :categories_products, :category_products
end
def self.down
rename_table :category_products, :categories_products
end
end
I am getting the following error on the simple_form in product/new.html.slim :
undefined method `klass' for nil:NilClass
The code breaks here :
= t.association :categories, as: :check_boxes
so I guess my associations arent still quite right
回答1:
In Rails there are two ways to do many-to-many relationships:
has_and_belongs_to_many
sets up a many to many relationship without an intervening model.
class Category < ActiveRecord::Base
has_and_belongs_to_many :products
end
class Product < ActiveRecord::Base
has_and_belongs_to_many :categories
end
This is nice if you know that it is a simple direct relationship and you know that you will not need to store any additional data about the relationship. It uses less memory since it does not have to instantiate an extra model just to do product.category
.
When using has_and_belongs_to_many
the convention is that the join table is named after the two entities in plural. In alfabetical order:
Category + Product = products_categories
has_many through
as you already have guessed uses an intermediate model.
class CategoryProduct < ActiveRecord::Base
belongs_to :product
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :category_products
has_many :products, through: :category_products
end
class Product < ActiveRecord::Base
has_many :category_products
has_many :categories, through: :category_products
end
The advantage here is that you can store and retrieve additional data in the join table which describes the relationship. For example if you wanted to store who added a product to a category - or when the relationship was created.
In order to for Rails to be able to correctly find the ProductCategory class the join table for a has_many though
naming convention is
model 1(singular) + model 2(plural)
Product + Category = category_products
This is due to the way that rails infers the model class based on the table name. Using categories_products
would case rails to look for Category::CategoriesProduct
.
Many to Many in forms and controllers.
As IvanSelivanov already mentioned SimpleForm has helper methods for creating selects, checkboxes etc.
- https://github.com/plataformatec/simple_form#associations
But instead of overriding the .to_s
method in your model you may want to use the label_method option instead.
f.assocation :categories, as: :checkboxes, label_method: :name
Overriding .to_s
can make debugging harder and in some cases give confusing test error messages.
To whitelist the params in your controller you would do:
class ProductsController < ApplicationController
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new
end
end
def product_params
params.require(:product)
.permit(:name, :categories_ids, ...)
end
end
回答2:
You also have to add CategoryProduct to each model:
class Product < ActiveRecord::Base
has_many :category_products
has_many :categories, through: :category_product
It is very simple using gem simple form
. All you have to do is to add:
t.association :categories
in a form for product and add :category_ids => []
to a list of permitted parameters in your products controller
If you prefer checkboxes instead of multi-select list, you can do
t.association :categories, as: check_boxes
And the last thing, to display categories in human-readable format, you need to define a to_s
method in your category model, i. e.:
class Category < ActiveRecord::Base
...
def to_s
name
end
end
来源:https://stackoverflow.com/questions/33045170/join-table-for-has-many-through-in-rails