閲覧数を管理する足跡モデルの設計 その2 設計の変更とその実装


導入

前回の記事に引き続き、閲覧数管理する足跡モデルの設計を行う。前回からの引き継ぎとして、現状の課題点として「ModelへのSQLの発行が乱立している。」点があげられる。今回はその解決を行う。
最終的な実装は一番最後にまとめてあります。

前回の記事:

課題点

1. ModelへのSQLの発行が乱立

: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回目と同一にする事ができると考えられる事から、以下のように変更したいと思う。

:controller/works_controller.rb
@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) 呼び出し後に、足跡が作成または追加されている。create_footprint_byメソッドの位置

@workに代入された後に、さらに足跡が追加されている。つまり、代入されている(表示される)閲覧数と実際の閲覧数に +1 の差が発生してしまう。

(2) joinsにおける内部結合の特徴 = 結合相手がいない行は結合結果から消滅する

参考:

内部結合の挙動では、結合先のテーブルを左テーブル(works)に合わせて複製されます。しかし、結合先が存在しない場合には、結合結果から消滅します。
つまり、足跡オブジェクトが1つも存在しない場合、@work自体がnilになってしまう。
これらの解決策として、仕様を変更する。

課題点(1)の解決策

(1)の閲覧数に +1 の差が発生してしまう問題に関しては、根本的な解決策にはなっていない気もするが、以下のようにして対応する。

:controller/works_controller.rb
@work = Work.select("works.*, SUM(footprints.counts) + 1 as total_footprints_count").joins(:footprints).includes(:user).find(params[:id])
@work.create_footprint_by(current_user)

呼び出す時に、カラムに1を追加した値とすること。これにより、現状の閲覧数と同値の値が代入されることになる。ついでに擬似カラム名も意味が分かるような名前に変更。

課題点(2)の原因

問題はこっちである。この設計自体のミスを見つけた、気がする。
足跡オブジェクトが1つも存在しない場合、@work自体がnilになってしまう問題。
そもそも、現状の足跡オブジェクトの作成(増加ではなく)は全てworks_controller/showメソッドに依存している。そして現状のSQLでは以下のような挙動になる。

作品が閲覧されるタイミングで足跡オブジェクトが作成される。
しかし、足跡オブジェクトが存在しない場合は、作品を呼び出す事ができない。
大きなジレンマが発生している。だったら@workを代入する前に、足跡を作成すれば良いか?

:controller/works_controller.rb
# ここに @work = Work.find.. が必要
@work.create_footprint_by(current_user) 
@work = Work.select("works.*, SUM(footprints.counts) + 1 as total_footprints_count").joins(:footprints).includes(:user).find(params[:id]) # 1回目 SELECT

ただ、そうなると結局一番最初の実装と同じで、SQLが3行になってしまう。
と言うことで、この問題の原因は、足跡オブジェクトの作成されるタイミングに問題があると考えた。

課題点(2)の解決策

そもそも、閲覧数管理がworks_contorollerのshow*のみに*依存している問題があると考えた為、以下のように作品が生成されるタイミングで、足跡オブジェクトを作成するものとする。

その為、モデルのコールバックを用いて足跡オブジェクト作成するものとする。作品作成(works/create)後には、自動的にshowページへリクエストが飛ぶので、作成者本人は一番最初に作品を閲覧するので、user_idは作成者のidを利用する。

:models/work.rb
class Work < ApplicationRecord
  after_create { Footprint.create(user_id: user_id, work_id: id, counts: 0) }
    :
end

こだわりポイント

足跡オブジェクトのデフォルト値は1なのだが、ここでは値を0に指定する。
作品作成後には、自動的にshowページへリクエストが飛ぶ(+1される)ので、この時点での閲覧数は0にし、リクエストのタイミングでデフォルトの1になるようにしている。

また、モデルのコールバックで作成した事により、seedファイルなど作成された場合にも必ず1つの足跡オブジェクトが作成される事になる。