閲覧数を管理する足跡モデルの設計 - その1 DB設計とその実装

## 閲覧数を管理するモデルの設計にあたって

### 目的：SQLの学習

ポートフォリオサイトの機能拡張とSQLの学習を目的に「*この作品を閲覧しているユーザーはこんな作品に興味があります*」機能を追加をします。

この機能の追加に向け、どのユーザーがどの作品を閲覧したかを管理する、**足跡モデル**を作成します。

## 利用イメージ

1. ユーザーがログイン(current_user)
2. ユーザーが作品を閲覧 → リクエスト (works_controllerのshowメソッド)
3. 新規足跡オブジェクトを追加、または既存の足跡オブジェクトのカウントを+1する
4. 作品viewを表示

## DB設計（関連付けと足跡モデルについて）

テーブル設計は以下のように設定する。

![足跡モデル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779676/67300c36-2b0d-0268-62b4-9730c5139e7b.png)


```ruby:各モデルの相関
class User < ApplicationRecord
  has_many :works
  has_many :footprints, dependent: :destroy
end

class Work < ApplicationRecord
  belongs_to :user
  has_many :footprints
end

class Footprint < ApplicationRecord
  belongs_to :user
  belongs_to :work
  validates :user_id, presence: true, uniqueness: { scope: :work_id }
  validates :work_id, presence: true
  validates :counts, presence: true
end
```

```ruby:Footprintモデルの各カラム
create_table "footprints", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
  t.integer "counts", default: 1, null: false
  t.bigint "user_id", null: false
  t.bigint "work_id", null: false
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
  t.index ["user_id", "work_id"], name: "index_footprints_on_user_id_and_work_id", unique: true
  t.index ["user_id"], name: "index_footprints_on_user_id"
  t.index ["work_id"], name: "index_footprints_on_work_id"
end
```

### こだわりポイント

1. `user_id`と`work_id`の組み合わせは一意
2. あるユーザーによるある作品の閲覧数は`counts`で管理する
3. 足跡オブジェクトが作成された時点で`counts`のデフォルト値は1
4. 閲覧される度に`counts`に+1する

## 足跡作成メソッド

```ruby:models/work.rb
class Work < ApplicationRecord
	:
  def create_footprint_by(user)
  if Footprint.find_by(user_id: user.id, work_id: id).present?
    footprint = Footprint.find_by(user_id: user.id, work_id: id)
    counts = footprint.counts
    footprint.update_attribute(:counts, counts + 1)
  else
    Footprint.create(user_id: user.id, work_id: id)
  end
	:
end
```

足跡作成メソッドのこだわり点

1. ユーザーと作品の組み合わせが存在するかどうかで条件分岐
2. 存在する場合には`counts`+1
3. 存在しない場合には足跡オブジェクト作成

## 実際の動作に基づき足跡を追加する

```ruby:controller/works_controller.rb
class WorksController < ApplicationController
	:  
  def show
    @work = Work.includes(:user).find(params[:id])
    @work.create_footprint_by(current_user)
    @footprints = Footprint.select("SUM(footprints.counts) as total").find_by(work_id: @work.id)
  end
	:
end
```

```ruby:views/works/show.html.erb
<div class="work__footprints">
	<i class="far fa-eye"></i><%= @footprints.total %>
</div>
```

## 課題点

### 今回の変更で可能になった点

1. 足跡モデルの作成・関連付け・デフォルト値の設定
2. 足跡オブジェクトの自動作成（create_footprint_by)
3. 足跡の表示　
`Footprint.select("SUM(footprints.counts) as total").find_by(work_id: @work.id)`

これらの設定により、実際の流れに基づき足跡を作成する事ができた。

### 一方で課題点

#### 1. ModelへのSQLの発行が乱立している。

```ruby:controller/works_controller.rb
  def show
    @work = Work.includes(:user).find(params[:id]) # 1回目 SELECT
    @work.create_footprint_by(current_user) # 2回目 UPDATE
    @footprints = Footprint.select("SUM(footprints.counts) as total").find_by(work_id: @work.id) # 3回目 SELECT
  end
```

コントローラーにて、個別のSQLへのリクエストが3度も行われてしまっている。3回目のSQLに関しては、1回目と同一にする事ができると考えられる事から、以下のように変更する。

```ruby
@work = Work.select("works.*, SUM(footprints.counts) as total").joins(:footprints).includes(:user).find(params[:id]) # 1回目 SELECT
@work.create_footprint_by(current_user) # 2回目 UPDATE
```

しかしこの場合には、2つの問題が発生する。

1. workの呼び出し後に、足跡が作成または追加されている。（create_footprint_byメソッドの位置）
2.  joinsにおける内部結合の特徴 = 結合相手がいない行は**結合結果から消滅する**

これらの解決や原因などについては次の記事でまとめるものとする。