QA@IT

ruby1.9+rails3.2で親子関係をもったテーブルを作りたい

4622 PV

初めまして。ruby初心者です。

以下の2つのテーブルをひも付けたいと考えていますが、子テーブルに親のIDが参照できず困っています。
皆様のお力を貸してくださいm(_ _)m
・お店(親)
・メニュー(子)

お店は複数のメニューを持つことができる。(1対多の関係)
お店の詳細ページがあり、そこに「メニュー作成」リンクを実装。
「メニュー作成」をクリックすると新規作成ページに遷移。

■テーブル定義は以下とします。
shopsテーブル(親)
 id:integer #主キー:ID
 name:string #お店の名前

menusテーブル(子)
 id:integer #主キー:ID
 shop_id:integer #外部キー:shopsテーブルのID
 name:string #メニュー名称
 price:integer #価格

■shopとmenuを関連づけ
shopのモデルに以下を追記
 has_many :menus, dependent: :destroy
menuのモデルに以下を追記
 belongs_to :shop

■shopのviewに以下のリンクを張ります。(show.html.erb)
<%= link_to 'メニュー作成', new_menu_path(shop_id: @shop) %>

■menuコントローラーの修正
menu_controller.rb
def create
@menu = Menu.new(shop_id: @shop.id)
@menu.shop_id = @shop.id

respond_to do |format|
  if @menu.save
    redirect_to @menu, notice: "メニュー作成完了"
  else
    render action: "new"
  end
end

end

これでメニューを作成すると以下のエラーが表示されてしまいます。
shopのidが参照されていないみたいなのですが、どのようにテーブルに反映させたら良いかがわかりません。
Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id

初心者で相当基本的な質問で申し訳ないですが、何卒よろしくお願い致します。

回答

willnet様

返信遅くなってしまい申し訳ありません。
ご回答頂きありがとうございます。

ご指示頂いた内容でコードを書き換えてメニュー作成のリンクをクリックしたところ、メニュー作成画面が表示されず下記のようなエラーが発生してしまいました。
Can't mass-assign protected attributes: shop

shop?の属性が保護されていると思われるのですが、modelをみても特にそのような記述が見当たらず、結局わかりませんでした。。
お忙しいところ何度も大変申し訳ないですが、ご教授頂けると幸いです。
もしお時間ありましたらぜひアドバイスお願い致します。

p.s.
遅くなってしまいましたが、githubにソースをアップさせて頂きました。
設定もほぼ初期の状態のままなので、ルート設定などは行っていません。
shop画面にいくにはhttp://localhost:3000/shopsからお願いします。

https://github.com/agitajapan/maman

編集 履歴 (0)

willnet様

早々のご回答ありがとうございます!
助言頂いた内容で実行してみたのですが、下記のようなエラーがで表示されてしまいました。

SQLite3::ConstraintException: constraint failed: INSERT INTO "menus" ("created_at", "name", "price", "shop_id", "updated_at") VALUES (?, ?, ?, ?, ?)

また、ログを見てみたのですが、下記のようにうまく値が反映されていないようです。。
SQL (5.6ms) INSERT INTO "menus" ("created_at", "name", "price", "shop_id", "updated_at") VALUES
(?, ?, ?, ?, ?) [["created_at", Sun, 16 Sep 2012 12:46:23 UTC +00:00],["name", nil], ["price", nil], ["shop_id", nil], ["updated_at", Sun, 16 Sep 2012 12:46:23 UTC +00:00]]

以下フォームのコードです。
暫定的にscaffoldで作ったコードをそのままのものを利用しています。
(なんかこれが悪い気がしてきました)
■new.html.erb
<% @page_title = "新規メニュー登録" %>
<%= @page_title %>

<%= form_for @menu do |form| %>
<%= render "form", form: form %>
<%= form.submit %>
<% end %>

■_form.html.erb


<%= form.label :name, "名前" %>
<%= form.text_field :name %>


<%= form.label :price, "価格" %>
<%= form.number_field :price %>

大変恐縮ですが、何卒よろしくお願いいたします!

編集 履歴 (1)
  • 回答欄は「質問に対する回答」の投稿だけにしてください。投稿にはMarkdownをご利用ください。その辺のことはFAQやガイドラインに書いてありますので、ご一読ください。このサイトにあるほかのQAのやり取りを見てみてください。 -

コントローラを下記のような感じにするとどうでしょうか。基本的な考え方として、

  • リクエスト間でインスタンス変数の内容の受け渡しは(基本的には)出来ないので、都度 params の情報を使いインスタンスを生成する
  • params に値をセットするには、form を使うか、ルーティングで設定した値(今回のケースで言えば /shops/1/menus/new にリクエストを投げると params[:shop_id] に 1 が入るはず)を使う

の二点を抑えると form を利用したレコードの生成ができるようになると思います。

def new
  @shop = Shop.find(params[:shop_id])
  @menu = Menu.new(shop: @shop)
end

def create
  @shop = Shop.find(params[:shop_id])
  @menu = @shop.menus.build(params[:menu])
  if @menu.save
    redirect_to @menu, notice: "メニュー作成完了"
  else
    render action: "new"
  end
end
編集 履歴 (0)

sakuro様

ありがとうございます。
以下内容を修正しましたが、まだエラーとなってしまいます。。
■ルートの設定
 ご指示頂いたように変更しました。
■shopのビューを修正
 リンクを以下のようにしました。
 【show.html.erb】
 <%= link_to 'メニュー作成', new_shop_menu_url(@shop) %>
■menusのビューを修正
 【new.html.erb】
 <%= form_for [@shop,@menu] do |form| %>

これで実行してみたのですが、以下のようなエラーが表示されてしまいます。
SQLite3::ConstraintException: constraint failed: INSERT INTO "menus" ("created_at", "name", "price", "shop_id", "updated_at") VALUES (?, ?, ?, ?, ?)

menusテーブルはidとtimestamp以外のすべてのフィールドの値が入っていませんでした。
以下はそのときのログです。

Processing by MenusController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"zb+GBdCTYaMzgN0YcforXOM+NfcdyMCKYdvG22tFbbw=", "menu"=>{"name"=>"サンプルメニュー", "price"=>"100"}, "commit"=>"Create Menu"}
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO "menus" ("created_at","name", "price", "shop_id", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Sun, 16 Sep 2012 15:15:49 UTC +00:00], ["name", nil], ["price", nil], ["shop_id", nil], ["updated_at", Sun, 16 Sep 2012 15:15:49 UTC +00:00]]
SQLite3::ConstraintException: constraint failed: INSERT INTO "menus" ("created_at", "name", "price", "shop_id", "updated_at") VALUES (?, ?, ?, ?, ?)
(0.1ms) rollback transaction
Completed 500 Internal Server Error in 3ms

ホント、基本的なことで申し訳ないです。。
すいませんが何卒よろしくお願い致します。

編集 履歴 (0)
  • menuインスタンスを作成するときはどうやっていますか? また、:shop_id もparamsに入っていないようです。 -

scaffoldでShopとMenuを作ったときに config/routes.rb が以下のようになっているはずです。

resources :shops
resources :menus

このままだとMenuコントローラはShopとは独立して動くので、Menuコントローラーを実行した時にShopの情報はどこにもありません。

rake routes して、どのようなリクエストがどのようなパラメータを受け取り、処理されるか確認しましょう。(大事)

次は、ルーティングを以下のようにしてみましょう。

resources :shops do
  resources :menus
end

認識されるURL構造が変わります。(これも rake routes で確認)

:shop_id というパラメータがMenuコントローラのアクションに渡ってくるようになるので、親になる @shopfind してやりましょう。子であるMenuを作成するときは、与えられたパラメータを使って @shop.menus.create が便利です。

ビュー内では、たとえばMenuの作成ビューへのリンクは new_shop_menu_url(@shop) で作成します。(これも rake routes で確認)

form_for には [@shop,@menu] という配列を渡してやりましょう。

編集 履歴 (0)

create アクションで @shop が定義されていない状態(nilな状態)で @shop.id を呼び出しているためにエラーになっていると思われます。 new アクションの内容によりますが、きちんとフォームの定義が出来ていたと仮定すると、下記のように params で渡ってきた情報を使って Menu を作るとうまくいくと思います。

def create
  @menu = Menu.new(shop_id: params[:shop_id])
  if @menu.save
    redirect_to @menu, notice: "メニュー作成完了"
  else
    render action: "new"
  end
end

ただ、メニューの新規作成フォームがうまくできていない(shop_id を渡すようになっていない)と上記のアクションでもエラーになると思います。もしうまくいかなければフォームのコードも貼ってみてください。

編集 履歴 (0)
  • ちなみに respond_to はコードを見る限り不必要っぽいので削除しています -
ウォッチ

この質問への回答やコメントをメールでお知らせします。