[Rails]親子同時保存(cocoon)とリコメンド機能(おすすめ機能)の実装

## 実装すること
今回は私が作成した、「お気に入りのコーヒーを投稿するポートフォリオ」で実装した「あなたへのおすすめ機能」のコードを書いていきます。
簡単にポートフォリオについて説明させて頂くと、
・ユーザーは飲んだコーヒーを投稿します。(Itemテーブル)
・投稿する際に飲んだコーヒーのテイスト(Taistテーブル)を16個のブロックから選びます。
・その投稿に対してユーザーはいいねをすることができます。(Likeテーブル)
・ログインユーザーはマイページに「あなたへのおすすめ」の投稿(Item)が表示されます。
「あなたへのおすすめ」を表示する流れは下記となります。
①ログインユーザーが一番最近いいねした投稿と同じテイストデータの投稿を取ってくる
②①の投稿の中でまだログインユーザーがいいねしていない投稿を取ってくる
③②の投稿の中で最新の投稿を１つ取ってくる(②が無い場合はランダムに1つ取ってくる)

※いいね機能に関しては、下記リンク先で説明しております。
https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448

## イメージ
新規投稿をする際に、飲んだコーヒーのテイスト(Taistテーブル)を16個のブロックから選びます。
16個のブロックには、それぞれTaistテーブルの「refresh,bitter,body,fruity」の4つのカラムに入る数字が割り振られています。
(例)左上なら、refresh=3, bitter=0, body=0, fruity=3

<img width="485" alt="コーヒーマップ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539971/dbe68902-8e5f-a20e-6855-8de29bd0aba1.png">


## ER図
Taistテーブルは、投稿(コーヒー)のテイスト(味)データを格納します。
Itemテーブルは、投稿(コーヒー)のデータを格納します。
関係は、Taist:Item = 1:Nになります。

<img width="715" alt="おすすめ機能ER図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539971/9bff1cfa-6d55-53fc-68d1-b6a6ff2eece1.png">

## アソシエーションの確認

### Taistモデル
`accepts_nested_attributes_for`は、Railsが標準で提供している、ActiveRecordのメソッドの一つです。モデル同士が関連付けられている時に、ネストさせることで一度にまとめてレコードの更新ができるようになります。
今回はTaistレコードが作成・更新されるとそれに紐づいたItemレコードも作成・更新されるようにしています。
`allow_destroy: true`をattributesメソッドに追加することで削除可能になります。削除するには対象となるレコードに`_destroy: 1`のようなパラメーターを渡します。

```app/models/taist.rb

class Taist < ApplicationRecord
	#アソシエーション
	has_many :items
	#親子同時保存(親:taist/子:item)
	accepts_nested_attributes_for :items, allow_destroy: true
end

```

### Itemモデル

```app/models/item.rb
class Item < ApplicationRecord

    belongs_to :taist
	belongs_to :user

    has_many :likes, dependent: :destroy
	has_many :liked_users, through: :likes, source: :user

    #refile
	attachment :image

end
```

## 投稿機能の作成(コントローラー・ビュー)
### taists_controller.rb
親子同時保存(Taistテーブル・Itemテーブル)にはcocoonを使用しています。
画像投稿はrefileを使用しています。

```app/controllers/taists_controller.rb
class TaistsController < ApplicationController

   def new
		#cocoonで親子同時保存
		@taist = Taist.new
		@taist.items.build
	end

	def create
		@taist = Taist.new(taist_params)
	      if @taist.save
	      	render :index
	      else
	      	render :new
	      end
	end

	private
	def taist_params
	    params.require(:taist).permit(:fruity, :refresh, :body, :bitter, items_attributes: [:id, :title, :taist_id, :image, :user_id])
	end
end
```

### taists/new.html.erb
・Taistモデルの子要素であるItemモデルは、`fields_for`を使用してフォームを作成します。
・`f.label '✔︎', for: 'refreshA'`は、id: 'refreshA'のラジオボタンに紐づいています。
・refresh以外のbiiter,body,fruityは`hidden_field`で隠し、`disabled: "disabled"`で初期状態では保存されないようにしています。javascriptを使用して、id=refreshAがチェックされていた時は、class=refreshAのhidden_fieldを持つ`disabled: "disabled"`を解除することで、特定のデータのみ保存するようにします。

```app/views/taists/new.html

<h1>新規投稿</h1>
<h5>コーヒーマップ</h5>
<%= form_for(@taist, url: users_taists_path, remote: true) do |f| %>
    <%= f.fields_for :items do |item| %>
        <%= render 'item_fields', f: item %>
    <% end %>
    <div class="refresh_contentA">
	      <%= f.radio_button :refresh, 3, class: "radio_btn", id: 'refreshA' %>
	      <%= f.label '✔︎', for: 'refreshA' %>
	      <%= f.hidden_field :bitter, :value => 0, disabled: "disabled", class: 'hiddenA' %>
	      <%= f.hidden_field :body, :value => 0, disabled: "disabled", class: 'hiddenA' %>
	      <%= f.hidden_field :fruity, :value => 3, disabled: "disabled", class: 'hiddenA' %>
	</div>									 
        ... <!-- refresh_contentがA-Qの16個あります。 -->
        <%= f.submit "保存", class:"form-control" %>
<% end %>
```

### taists/_item_fields.html.erb
・`hidden_field`でuser_idにcurrent_user.idを代入しています。

```app/views/taists/_item_fields.html

<div class="nested-fields form-inline">
    <p>画像投稿</p>
		<%= f.attachment_field :image %>
    <p>タイトル</p>
		<%= f.text_field :title, class: "form-control" %>
    <%= f.hidden_field :user_id, :value => current_user.id %>
</div>
```

### application.js
チェックされたrefreshIDに紐づいた'disabled'を解除します。

```app/assets/javascripts/application.js
$(document).on("turbolinks:load", function() {

  		$('#refreshA').click(function() {
      			$(".hiddenA").attr('disabled', false);
  		});

        // #refreshA-Qまで記載しています。
});
```

## おすすめを表示する(コントローラ・ビュー)
大変お待たせしました。おすすめ機能を表示するロジックを記載致します。

### users_controller.rb

```app/controllers/users_controller.rb

class UsersController < ApplicationController

  def show
		@user = User.find(params[:id])
        #@userがいいねした投稿
		@likes = Like.where(user_id: @user.id).order(created_at: :desc)
        if @likes.exists?
            #like = @userがいいねしている投稿の最新1つ
            like = @likes.last
            #item = likeのitem_idが含まれるitem
            item = like.item
            #item_recommend = itemと同じtaist
			taist_recommend = Taist.find_by(refresh: item.taist.refresh, bitter: item.taist.bitter, body: item.taist.body, fruity: item.taist.fruity)
            #item_recommend = taist_recommendと同じitemを全て
			item_recommend = taist_recommend.items
            #いいねしたitem全てのitem.id
			x = [] #xに配列を代入できるようにする
			@likes.each do |like|
				x << like.item_id #xにlike.item_idを全て代入して配列にする
			end
            #like_recommend = item_recommendの中で@userがいいねしていないitemの最新1つ
			@like_recommend = item_recommend.where.not(id: x).last
        else
			@like_recommend = Item.order("RAND()").last
        end         
  end


end
```

### users/show.html.erb

```app/views/users/show.html

<h1>あなたへのおすすめ</h1>
<h3>タイトル</h3>
    <%= @like_recommend.title %>
<h3>投稿</h3>
    <%= attachment_image_tag @like_recommend, :image %>
```

## 最後に
最後までご覧いただきありがとうございます。
初学者ですので間違っていたり、分かりづらい部分もあるかと思います。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。