こんばんは!スージーです。
個人アプリの中で勉強になった部分があったので備忘録として。
accepts_nested_attributes_for
参考:Rails API
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
userが投稿する時に関連付けられたモデルも一括保存できます。
DB設計
Usersモデル
Column | Type | Options |
---|---|---|
varchar | null: false | |
nickname | text | null: false |
profile | text | null: false |
image | text |
アソシエーション
- has_many :posts, dependent: :destroy
- has_many :meals, dependent: :destroy
Postsモデル
Column | Type | Options |
---|---|---|
name | varchar | null: false |
text | text | null: false |
image | text | |
elevation | integer | |
walking_distance | integer | |
difficulty | integer | |
user_id | integer | null: false |
アソシエーション
- belongs_to :user
- has_one :meal, dependent: :destroy
Mealsモデル
Column | Type | Options |
---|---|---|
name | varchar | null: false |
image | text | |
food_stuff | text | |
cooking_time | integer | null: false |
cooking_method | integer | |
post_id | integer | null: false |
user_id | integer | null: false |
アソシエーション
belongs_to :post
モデルの説明をすると、ユーザーは投稿(post)で登った山の情報を登録します。
name=山の名前
text=説明
image=画像
elevation=山頂標高
walking_distance=歩行距離
difficulty=難易度
同時にnested model
の山メシ(meal)も登録します。
name=メシの名前
image=画像
cooking_time=調理時間
food_stuff=使う食材
cooking_method=調理方法
以上を入力してsubmit
すればPostモデルとMealモデルが同時に保存されるのがaccepts_nested_attributes_for
です。
実装
class Post < ApplicationRecord
belongs_to :user
has_one :meal, dependent: :destroy
accepts_nested_attributes_for :meal
#他省略
end
class PostsController < ApplicationController
#必要な部分のみ抜粋
def new
@post = Post.new
@post.build_meal
end
def create
@post = Post.create(post_params)
if @post.save
flash[:success] = "投稿が完了しました!"
redirect_to :root
else
flash.now[:alert] = "投稿が失敗しました。"
render :new:
end
end
private
def post_params
params.require(:post).permit(
:name,
:text,
:image,
:elevation,
:walking_distance,
:difficulty,
:prefecture_id,
:remove_image,
meal_attributes: [:id,
:name,
:image,
:cooking_time,
:food_stuff,
:cooking_method,
:user_id]
).merge(user_id: current_user.id)
end
end
いつもと違うのは、
@post.build_meal
とparams内の
meal_attributes: [:id~以下省略]
です。
nested modelの許可するパラメーターの値はparams内でmeal_attributes:
を使って記述します
<%= form_for @post do |f| %>
<%= f.label :山の名前 %>
<%= f.text_field :name %>
・
・
<%= f.label :難易度 %>
<%= f.select :difficulty, Post.difficulties_i18n.invert %>
<%#以下省略%>
<%= f.fields_for :meal do |s| %>
<%= s.label :山メシの名前 %>
<%= s.text_field :name %>
・
・
<%#以下省略%>
<% end %>
<%= f.submit %>
<% end %>
form_for @post do |f|
内にf.field_for :meal do |s|
をネストさせてあげます。
参照
Action View フォームヘルパー
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
これで無事postモデルとmealモデルは一緒に保存されるようになりました
@user.mealのアソシエーションを組みたい
無事にDBに保存されるようになりましたが、今のままではpostモデル
とmealモデル
はアソシエーションを組めていますが、userモデル
とmealモデル
のアソシエーションが組めていません。
現状はuser_id
は親モデルのpostモデル
に外部キーとして保存される。しかし子モデルのmealモデル
に保存されるのは親モデルpostのpost_id
となります。(has_one :meal
とbelongs_to :post
の主従関係の為)
コンソールで確認
$ rails console
[1] pry(main)> user = User.find(1)
User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, email: "user1@example.com", created_at: "2019-11-17 07:52:29", updated_at: "2019-11-17 07:52:29", nickname: "ニックネーム1", profile: "宜しくお願いします!", image: "ユーザー.jpeg">
[2] pry(main)> user.posts
=> [#<Post:0x00007fefdaab89d0
id: 1,
name: "テスト山1",
text: "楽しい山登り!",
image: "利尻山.jpeg",
elevation: 871,
walking_distance: 11,
difficulty: "normal",
created_at: Sun, 17 Nov 2019 07:52:30 UTC +00:00,
updated_at: Sun, 17 Nov 2019 07:52:30 UTC +00:00,
user_id: 1,
likes_count: 3,
prefecture_id: 11>,
・
・
~以下省略~
[3] pry(main)> user.meals
NoMethodError: undefined method `meals' for #<User:0x00007fefd6cc5ba0>
from /Users/username/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.3/lib/active_model/attribute_methods.rb:430:in `method_missing'
mealモデルにuser_idが保存されないとuserとアソシエーションが組めずusers#show
ページ等で描写する事ができません。
解決策 hidden_fieldを使う
良い方法か分からないがhidden_field
を使ってuser_id
をcurrent_user.id
にすればいけるんじゃないかと思い付きました。
<%= form_for @post do |f| %>
<%= f.label :山の名前 %>
<%= f.text_field :name %>
・
・
<%= f.label :難易度 %>
<%= f.select :difficulty, Post.difficulties_i18n.invert %>
<%#以下省略%>
<%= f.fields_for :meal do |s| %>
<%#s.hidden_fieldを追加%>
<%= s.hidden_field :user_id, value: current_user.id %>
<%= s.label :山メシの名前 %>
<%= s.text_field :name %>
・
・
<%#以下省略%>
<% end %>
<%= f.submit %>
<% end %>
もう一度コンソールで確認してみる
$ rails console
[1] pry(main)> user = User.find(1)
User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, email: "user1@example.com", created_at: "2019-11-17 07:52:29", updated_at: "2019-11-17 07:52:29", nickname: "ニックネーム1", profile: "宜しくお願いします!", image: "ユーザー.jpeg">
[2]pry(main)> user.meals
=> [#<Meal:0x00007fefdb2bb4c0
id: 15,
name: "カレー",
image: nil,
cooking_time: 11,
food_stuff: "にんじん。じゃがいも",
cooking_method: "煮込む",
created_at: Sun, 17 Nov 2019 08:23:08 UTC +00:00,
updated_at: Sun, 17 Nov 2019 08:23:08 UTC +00:00,
post_id: 170,
user_id: 1>,
~以下省略~
値が取れてる
ビューに表示する
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
@meals = @user.meals
@posts = @user.posts
end
#他省略
end
<% if @user.posts.any? || @user.meals.any? %>
<% @posts.each do |post| %>
<%= post.name %>
<%= post.walking_distance %>
<%#他省略%>
<% end %>
<% @meals.each do |meal| %>
<%= meal.name %>
調理時間:<%= meal.cooking_time %>分
<%#他省略%>
<% end %>
<% else %>
まだ投稿はありません。
<% end %>
いけた
まとめ
正しいやり方なのかは不明ですが、nested modelの使い方が理解できたのでまとめてみました。