未ログインユーザーの閲覧数カウント

# はじめに
絶賛ポートフォリオ制作中です！
今回は未ログインのユーザーが投稿詳細画面を表示した時に、
閲覧数をカウントするように実装したためアウトプットします✍️

※旅行やデートのプランを共有するアプリなので、投稿機能はPostではなくPlanを使用しています！

# ER図

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/d263bd3e-eb15-dba5-748b-4f77493a59d1.png">

# モデル、マイグレーションファイル作成
#### モデル作成
```
$ rails g model ViewCount user:references plan:references
```

:::note info
referencesとは（直訳：参照、参考)
名前の通り、作成済みのテーブルを参照する場合に使用します
コマンドを実行すると下記カラムがマイグレーションファイルに自動追加されます
```
t.references :user, null: false, foreign_key: true
t.references :plan, null: false, foreign_key: true
```
ここで指定しなくとも、後からマイグレーションファイルに追加可能です！
のちにモデルに記載する「belongs_to」を自動で記載もしてくれます
※「has_many」は追加してくれないので注意⚠️
:::

以下の記事を参照しました！

https://prograshi.com/framework/rails/references_and_foreign-key/

#### マイグレーションファイル
足りないところを追加したり、自動入力されている箇所を修正します

```xxxxx_create_view_counts.rb
class CreateViewCounts < ActiveRecord::Migration[6.1]
  def change
    create_table :view_counts do |t|
      # 初期の記述と変わっているため注意！↓
      t.references :user, null: true, foreign_key: true
      t.references :plan, null: false, foreign_key: true
      t.string :user_or_ip, null: false

      t.timestamps
    end
    # ユーザーと投稿の組み合わせを組合せを一意に保つ為の記述
    add_index :view_counts, [:user_or_ip, :plan_id], unique: true
  end
end
```

:::note warn
注意⚠️
userの`null: false`を`null: true`に変更します
通常はnullを許可しませんが、今回は未ログインのユーザーの閲覧数をカウントする必要が(current_userがnilになる可能性が）あるためです！
:::

`t.string :user_or_ip, null: false`と`add_index :view_counts, [:user_or_ip, :plan_id], unique: true`を追加しましょう！

忘れないうちにマグレートします！
```
$ rails db:migrate
```

#### アソシエーション

```user.rb
class User < ApplicationRecord
:
has_many :view_counts, dependent: :destroy
:
end
```

```plan.rb
class Plan < ApplicationRecord
:
has_many :view_counts, dependent: :destroy
:
end
```

```view_count.rb
class ViewCount < ApplicationRecord
  belongs_to :user, optional: true
  belongs_to :plan

  validates :user, uniqueness: { scope: :plan_id }, if: -> { user.present? }
  validates :user_or_ip, uniqueness: { scope: :plan_id }, if: -> { user.nil? }
end
```

:::note info
`optional: true`とは 🌱
belongs_toの外部キーのnilを許可するという意味です
今回の例で言うと、未ログインユーザーの閲覧数をカウントする場合、user_idがnilになります！このオプションを指定しないとuser_idのnillを許可していないので、エラーが発生してしまいます
このオプションはデフォルトでは`false`になっています
:::

こちらの記事がわかりやすかったです！

https://blog.ryskit.com/entry/2018/01/27/195442

#### バリデーション
ユーザーと投稿の組み合わせが一意であることを確認しています

`if: -> { user.present? }`
userがnilでない場合はuserとplan_idの組み合わせを確認

`if: -> { user.nil? }`
userがnilの場合は、user_or_ipとplan_idの組み合わせを確認

# コントローラーに記述

```plans_controller.rb
class Public::PlansController < ApplicationController
  def show
    @plan = Plan.find(params[:id])
    user_or_ip = current_user ? current_user.id.to_s : request.remote_ip
    unless ViewCount.find_by(user_or_ip: user_or_ip, plan: @plan)
      ViewCount.create(user_or_ip: user_or_ip, plan: @plan, user: current_user)
    end
  end
end
```

`user_or_ip = current_user ? current_user.id.to_s : request.remote_ip`
三項演算子の構文を使用しています
```ruby
条件 ? 真の場合の値 : 偽の場合の値
```

`current_user ?`では現在ユーザーがログインしているかどうかを判定しています

真の場合（ユーザーがログインしている場合）
`current_user.id.to_s`current_userのidを文字列化し、`user_or_ip`に代入します
⚠️次に記載するipアドレスが文字列なので、合わせるためにidを文字列に変換しています！

偽の場合（ユーザーがログインしていない場合）
`request.remote_ip`
リクエスト元のユーザーのIPアドレスを取得し、`user_or_ip`に代入します
初めて知った記述だったため、念の為確認しました↓

https://railsdoc.com/page/request_remote_ip

```plans_controller.rb
    unless ViewCount.find_by(user_or_ip: user_or_ip, plan: @plan)
      ViewCount.create(user_or_ip: user_or_ip, plan: @plan, user: current_user)
```

find_byで`user_or_ip`と`plan`の組み合わせが既にないか確認し、
なければViewCoutデータを作成します(同じクライアントが複数回閲覧しても閲覧数は1)

# viewを記述
さいごに、表示したい場所に以下を記述します

```plans/show.html.erb
<%= @plan.view_counts.count %>Views
```

# 検討するべきポイント
未ログインのユーザーに対してIPアドレスを取得していますが、
同じネットワークを共有する複数のユーザーが同じIPアドレスを使用する場合があります
そのため異なるユーザーの閲覧を区別することができない場合があります
もっといい方法がありましたら教えていただけますと幸いです（これが限界でした…！）

# さいごに
閲覧数の記事は沢山あったのですが、未ログインユーザーの閲覧数をカウントする方法が見つからなかったためアウトプットも兼ねて書きました✍️

誤り等ございましたら教えていただけますと幸いです！

# 参照記事
参考にさせていただきました！ありがとうございます！

https://prograshi.com/framework/rails/references_and_foreign-key/

https://zenn.dev/airiin/articles/a9fe2f58fe203a#%E8%A6%81%E4%BB%B6

https://blog.ryskit.com/entry/2018/01/27/195442

https://railsdoc.com/page/request_remote_ip