【Rails】accepts_nested_attributes_forを使って子モデルも一緒に保存する

こんばんは！スージーです。
個人アプリの中で勉強になった部分があったので備忘録として。

## accepts_nested_attributes_for
>参考:Rails API
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

userが投稿する時に関連付けられたモデルも一括保存できます。

## DB設計

### Usersモデル

| Column   | Type       | Options               |
| -------- | ---------- | --------------------- |
| email    | 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`です。

## 実装

```ruby:models/post.rb
class Post < ApplicationRecord
 belongs_to :user
 has_one    :meal,  dependent: :destroy
 accepts_nested_attributes_for :meal
 #他省略
end
```

```ruby:posts_controller.rb
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:`を使って記述します

```erb:new.html.erb
<%= 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`の主従関係の為）


## コンソールで確認

```terminal
$ 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`にすればいけるんじゃないかと思い付きました。

```erb:new.html.erb
<%= 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 %>
```

## もう一度コンソールで確認してみる

```terminal
$ 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>,
~以下省略~
```
**値が取れてる**

## ビューに表示する

```ruby:users_controller.rb
class UsersController < ApplicationController
 def show
  @user = User.find(params[:id])
  @meals = @user.meals
  @posts = @user.posts
 end
 #他省略
end
```

```erb:users/show.html.erb
<% 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 %>
```

## いけた
![スクリーンショット 2019-11-17 20.08.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428016/9ad3501b-8a82-2694-dc03-7ee8d07e24a1.png)

## まとめ
正しいやり方なのかは不明ですが、nested modelの使い方が理解できたのでまとめてみました。