はじめに
ポートフォリオ作成の過程で、個人的に試行錯誤したところを備忘録的にメモしています。
自分自身もコーディング歴がそこまで長くないので、初心者目線となりますがご容赦ください。
また、おかしい点がありましたら、ぜひご指摘いただければと思います。
Railsでページの閲覧履歴を記録する手っ取り早い手段としては、cookieを使った記録方法があるようなのですが
今回はcontrollerとmodelのみを使用して作成してみました。
環境・gem情報
- Ruby 2.5.3(ローカル環境)/2.3.8(本番環境)
- Rails 5.2.2
- devise 4.6.1
目標物
複数の記事があるサイトで、利用者登録を済ませたログイン済ユーザーが
過去に閲覧した記事の履歴をリスト化し、最後に閲覧した記事から数えて10件を表示する。
もうちょっと具体的に設計へ落とし込む
上の文言だけだとやや抽象的なので、もう少し設計に落とし込んだ上で
それぞれを満たすためには何を作る必要があるのか、簡単にまとめます。
(ルーティングの設定は省略しますが、下記のアクションそれぞれに対してルーティングが必要です。念の為。)
機能設計
-
複数の記事があり、それらを閲覧する。
- 記事テーブル・モデルの準備
- 記事の閲覧ビュー・アクションの準備
-
記事の閲覧履歴を記録する。
- 閲覧履歴テーブル・モデルの準備
- 閲覧履歴のレコードを作成するアクションの準備
-
利用者登録を済ませたログイン済みユーザーが、自分の閲覧履歴を見れる。
- ユーザーテーブル・モデルの準備
- ログイン中のユーザー情報に紐づいた閲覧履歴一覧
これらの機能から、必要なコントローラーとモデルを洗い出していきます。
model & controller(アクション含む)
- models
- Article(記事)
- User(ユーザー) ※deviseで作成
- BrowsingHistory(閲覧履歴)
- controller
- articles(記事)
- showアクション(ビュー表示用)
- articles(記事)
model間のリレーションは下記のような感じです。
UserとArticleの中間テーブルとして、BrowsingHistoryが存在している形です。
この中間テーブルたるBrowsingHistoryに、ログイン中ユーザーのIDと、閲覧した記事のIDを保存していきます。
実装
model & controllerの作成
deviseのインストールについては割愛しますが、インストールが終わった状態で、下記のコマンドを実行してください。
$ rails g model Article
$ rails g model BrowsingHistory
$ rails g devise User
$ rails g db:migrate
モデルが作成されたら、続いてコントローラーを作成します。
$ rails g controller articles
この後、ルーティング作成をし、showアクションでviewが表示されるようにします。
Rails.application.routes.draw do
# 前略
get 'articles/:id' => 'article#show', as: 'article'
# 後略
end
コーディング
出来るところからコーディングしていきます。まずはmodelのアソシエーションから。
先ほどER図でも示した通り、BrowsingHistoryがUserとArticleの中間テーブルになるようにしたいので
それぞれ下記の通りhas_manyとbelongs_toを追加します。
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :browsing_histories, dependent: :destroy # ここを追加
end
class Article < ApplicationRecord
has_many :browsing_histories, dependent: :destroy # ここを追加
end
class BrowsingHistory < ApplicationRecord
belongs_to :user # ここを追加
belongs_to :article # ここを追加
end
続いて、controllerでshowアクションを設定します。
とりあえず記事が閲覧できればいいので、URLに入力されたIDの記事をインスタンス変数で受け取るようにします。
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
end
end
これで、記事が表示されるようになりました。
閲覧履歴の保存
いよいよ閲覧履歴の保存の機能実装となりますが、事前に一つ解決しておく課題があります。
いつ保存する?
閲覧履歴を保存するタイミング、つまり「ユーザーがその記事を見たと判断するタイミング」を決める必要があります。
例えば、showアクションの一番初めに持ってくると、どうなるでしょうか。
class ArticlesController < ApplicationController
def show
# ここに閲覧履歴を保存するコードを書く?
@article = Article.find(params[:id])
end
end
この場合、インスタンス変数が宣言される前なので、閲覧履歴に必要な記事のIDを引っ張ってくるのが面倒になりますし
存在しない記事IDが入力された場合に、その存在しないIDがそのまま保存されるリスクもありそうです。
逆に、インスタンス変数を定義した後に閲覧履歴を保存するアクションを書けば、取得済の記事IDを
@article.idという形で使えるので、ぐっと楽になりそうです。
というわけで
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
# ここから閲覧履歴を保存するコードを書く
end
end
閲覧履歴を保存するタイミングは、インスタンス変数の宣言後とします。
保存するアクションの作成
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
# ここから閲覧履歴を保存するコードを書く
new_history = @article.browsing_histories.new
new_history.user_id = current_user.id
new_history.save
end
end
手順としては、
new_history = @article.browsing_histories.new
この一文の意味は
- 記事情報(
@article
)に紐づいた - 閲覧履歴レコード(
browsing_histories
)の - 空レコードを新規作成(
new
)
という意味になります。
次は、この変数に、ログイン中のユーザーのIDを入れていきます。
new_history.user_id = current_user.id
current_userは、deviseのヘルパーメソッドで、ログイン中のユーザーの情報を格納しています。
これで記事ID、ユーザーIDが出揃ったので、saveで保存します。
new_history.save
これで閲覧履歴の作成機能は完成です。
問題の抽出
機能としてはこれで完成なのですが、このままだといくつか問題があります。
- 同じユーザーが同じページを何回も続けて見た場合、見た回数だけ閲覧履歴に追加される
- 上限がないので、閲覧履歴が際限なく追加される
上記のやり方だと、showアクションを実行するたびに閲覧履歴が保存されるので、
例えばAという記事を表示した後にBという記事を表示、その後にAを再び表示した場合
閲覧履歴にはA→B→Aの順番でレコードが残ることになります。
ユーザーのログを取るという意図があるのであれば、これでも問題は無いと思うのですが、今回はそのあたりの意図は無いものとして、あくまでユーザーが直近の閲覧履歴を見返せるようにする、という目的にフォーカスし、ページの重複をなくすように実装します。
また、二つ目の問題、閲覧履歴が際限なく追加される点についても、上記と同様の理由で、1ユーザーあたりの閲覧履歴レコード数に上限を設けたいと思います。
同一ユーザーの閲覧履歴に同一ページがある場合の処理
先ほどのアクションに、下記のようにコードを追加します。
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
# ここから閲覧履歴を保存するコードを書く
new_history = @article.browsing_histories.new
new_history.user_id = current_user.id
# ここから追加
if current_user.browsing_histories.exists?(article_id: "#{params[:id]}")
old_history = current_user.browsing_histories.find_by(article_id: "#{params[:id]}")
old_history.destroy
end
# ここまで
new_history.save
end
end
まず、current_user.browsing_histories.exists?(article_id: "#{params[:id]}")
で
- ログイン中のユーザー(
current_user
)の - 閲覧履歴(
browsing_histories
)の中で - 記事ID(
article_id
)がURLに入力されているID(params[:id])と同じものが - すでに存在しているか?(
exists?
)
ということを調べています。
結果的に、ユーザーIDも記事IDも同じになっている閲覧履歴は存在しているか?ということを調べています。
もし存在した場合は、if文内の処理に移ります。
old_history = current_user.browsing_histories.find_by(story_id: "#{params[:id]}")
ここでは、
- ログイン中のユーザー(
current_user
)の - 閲覧履歴(
browsing_histories
)の中で - 記事ID(
article_id
)がURLに入力されているID(params[:id]
)と同じものを - すでに存在する古い履歴(
old_history
)として定義する
という処理になっています。
こうして定義した古い履歴を、削除します。
old_history.destroy
その後、新しい履歴を追加することで、同じ記事の履歴が重複せずに済む、ということになります。
同一ユーザーの閲覧履歴の件数が上限を超えた場合の処理
最後に、同一ユーザーの閲覧履歴の上限に達した場合、古い履歴を順番に消していくようにします。
先ほどのアクションに、下記のようにコードを追加します。
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
# ここから閲覧履歴を保存するコードを書く
new_history = @article.browsing_histories.new
new_history.user_id = current_user.id
# 同一記事の重複チェック・重複時は古い履歴を削除
if current_user.browsing_histories.exists?(article_id: "#{params[:id]}")
old_history = current_user.browsing_histories.find_by(article_id: "#{params[:id]}")
old_history.destroy
end
new_history.save
# ここから追加
histories_stock_limit = 10
histories = current_user.browsing_histories.all
if histories.count > histories_stock_limit
histories[0].destroy
end
# ここまで
end
end
まず、histories_stock_limit
で、上限とする記事数を決めます。
ここでは、1ユーザーが見ることができる閲覧履歴は10件までとします。
histories_stock_limit = 10
続いて、ログイン中のユーザーの閲覧履歴を、新しい順に並び替えたものを変数histories
として宣言します。
- ログイン中のユーザー(
current_user
)の - 閲覧履歴(
browsing_histories
)を - 全て取得したもの(
all
)を - 変数に入れて宣言(
histories
)
という処理の流れになります。
if histories.count > histories_stock_limit
この時点でログイン中ユーザーの閲覧履歴を全て取得しているので
上記のようにレコード数(histories.count
)が上限(histories_stock_limit
)を上回っていないか確認します。
上回っていた場合、一番古いデータを削除する必要があります。
ここで、histories
は、閲覧履歴の全データがレコードごとに配列で格納されていますが、
all
で全レコードを取得した場合、一番古いデータは配列の一番最初に格納されています。
したがって、
histories[0]
をdestroy
することで、一番古いレコードが削除されます。
おわりに
いかがだったでしょうか。
そもそも閲覧履歴をデータベースで管理する事例がどれだけ存在するのか、私自身把握できてないのですが
このアプローチが、何かの一助になればと思います。