みんなの「作ってみた」

【10日間でポートフォリオ作成に挑戦】8日目:記事ストック機能〜ユーザーフォロー機能の実装

2019/05/04

ryoutaku
ryoutaku
2018年12月から3年間「毎日技術ブログ書く」と宣言して現在も継続中。IBMのWatsonきっかけでエンジニアに憧れて、28歳未経験で転職。バックエンドの開発を担当(Ruby,AWS)社会にインパクトを与えるプロダクトの開発に携わりたい一心で、愚直にアウトプットを継続。今の関心事は自然言語処理。

概要

今回は、2019年のGW期間(10日間)を全て費やして取り組むポートフォリオの製作過程を取りまとめた内容を投稿させて頂きます。(投稿は毎日行う予定)

全体通した取り組みの詳細については、前回までの記事をご参照ください。

【10日間でポートフォリオ作成に挑戦】1日目:要件定義〜記事投稿のCRUD
【10日間でポートフォリオ作成に挑戦】2日目:アクセス制限〜コメントのCRUD機能
【10日間でポートフォリオ作成に挑戦】3日目:ページネーション~CKEditorの導入
【10日間でポートフォリオ作成に挑戦】4日目:テーブル分割〜CKEditorのフォームへの反映
【10日間でポートフォリオ作成に挑戦】5日目:CKEditorへ画像アップロード機能を追加
【10日間でポートフォリオ作成に挑戦】6日目:テストコードの実装
【10日間でポートフォリオ作成に挑戦】7日目:検索機能〜いいね機能の実装
【10日間でポートフォリオ作成に挑戦】8日目:記事ストック機能〜ユーザーフォロー機能の実装

今日一日の作業内容

ここからは、今日1日で取り組んだ作業内容をご説明します。

記事ストック機能の実装

まず、記事のストック機能を実装していきます。

中間テーブルがPostLikeからPostStockに切り替わるだけなので、サーバーサイドのロジックは、PostLikeとほぼ同様の内容となります。

$ rails g model PostStocks
$ rails g controller PostStocks

ルーティングは、PostLike同様に、Postにネストさせています。

config/routes.rb
  resources :posts do
    resources :post_comments, except: [:index, :show]
    resources :post_likes, only: [:create, :destroy]
    resources :post_stocks, only: [:create, :destroy]
  end

モデルも全く一緒です。

models/post_stock.rb
class PostStock < ApplicationRecord
  belongs_to :user
  belongs_to :post
  validates_uniqueness_of :post_id, scope: :user_id
end

コントローラーについても、遜色ありません。
(ちなみに、redirect_toのパスの指定方法については、先日に引き続き伊藤さん(@jnchitoからコメントでアドバイス頂いたので、その方法を採用しています。まさかこんなにアドバイス頂けると思っていなかったので、かなり驚いています!!有り難過ぎる:sob:

controller/post_stocks_controller.rb
class PostStocksController < ApplicationController
  def create
    post_stock = current_user.post_stocks.build(post_id: params[:post_id])
    post_stock.save!
    redirect_to post_stock.post
  end

  def destroy
    post_stock = current_user.post_stocks.find_by(post_id: params[:post_id])
    post_stock.destroy!
    redirect_to post_stock.post
  end
end

ストックボタンは、記事の詳細画面で表示させますが、非ログイン時には表示しない様に、deviseのヘルパーであるuser_signed_in?とIF文で、表示を切り替える様にしています。

views/posts/show.html.haml
.content-stock
  - if user_signed_in?
    - if @post_stock.nil?
      = link_to t('common.button.stock'), post_post_stocks_path(@post), method: :post
    - else
      = link_to t('common.button.unstock'), post_post_stock_path(@post, @post_stock), method: :delete

そうして出来上がったものが、こちらです。

ユーザーフォロー機能の実装

次にユーザーのフォロー機能を実装します。
下記のER図で示した様に、DB設計は行っています。

$ rails g model Relationships
$ rails g controller Relationships

カラムには外部キーとなるfollowing_idfollower_idをinteger型で設定します。

maigrate/create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[5.2]
  def change
    create_table :relationships do |t|
      t.integer  :following_id, null: false
      t.integer  :follower_id, null: false

      t.timestamps
    end
  end
end

子モデルのrelationshipには、親モデルのuserを、フォローするユーザーのfollowingと、フォローされるユーザーのfollowerで区別出来る様に、アソシエーションを記述します。

models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :following, class_name: "User"
  belongs_to :follower, class_name: "User"
end

親モデルのuserには、子モデルのrelationshipを、フォローするを側が参照するactive_relationshipと、フォローされる側が参照するpassive_relationshipsで区別出来る様に、アソシエーションを記述します。
それぞれ、followerfollower、どちらの外部きーを参照するのかも明記します。

また既にフォロー済みか判定するfollowed_by?メソッドも記述します。

models/user.rb
  has_many :active_relationships, class_name: "Relationship", foreign_key: :following_id
  has_many :followings, through: :active_relationships, source: :follower

  has_many :passive_relationships, class_name: "Relationship", foreign_key: :follower_id
  has_many :followers, through: :passive_relationships, source: :follower

  def followed_by?(user)
    passive_relationships.find_by(following_id: user.id).present?
  end

ルーティングは、usersにネストさせて、フォローの作成・削除の他に、フォローしているユーザーと、フォローされているユーザーの情報を取得して一覧表示させる為のルートも設定しています。

config/routes.rb
  resources :users , only: [:index, :show] do
    resource :relationships, only: [:create, :destroy]
    get :post_stocks
    get :follows, on: :member
    get :followers, on: :member
  end

そして、フォローの記録を行うコントローラーについては、いいねやストックと、ほぼ同様の内容となります。

controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  def create
    follow = current_user.active_relationships.build(follower_id: params[:user_id])
    follow.save
    redirect_to user_path(follow.following_id)
  end

  def destroy
    follow = current_user.active_relationships.find_by(follower_id: params[:user_id])
    follow.destroy
    redirect_to user_path(follow.following_id)
  end
end

フォローボタンは、ユーザーの詳細画面に表示させる様にしています。

views/users/show.html.haml
.content-follow
  - if user_signed_in?
    - if @user.followed_by?(current_user)
      = link_to t('common.button.unfollow'), user_relationships_path(@user), method: :delete
    - else
      = link_to t('common.button.follow'), user_relationships_path(@user), method: :post

これで完成したものが、こちらです。

これで、必須としていた機能サーバーサイドの実装は概ね完了しました。

今日の失敗

ここからは今日の失敗をまとめます。

IDが際限なく増える事を容認

いいね・ストック・フォロー機能は、いずれも簡単に取り消して、再度登録も出来る様になっています。
その内部での処理は「レコード作成→削除→新たにレコード作成→削除→・・・(ループ)」という流れになっています。

DBはレコードが削除されても、既に割り当てた事があるIDを使い回す事は原則無いので、これを繰り返すと、ID番号が非常に膨大な数になってしまい、それがパフォーマンスに何らかの影響を及ぼさないか?危惧しています。

繰り返しの登録→取り消しで、IDを際限なく増えない様にするには、statusカラムなどを設けて、そこで取り消されたか判定する事で、都度新しいレコードを生成される事を防げますが、不要なデータが大量に残る要因になるので、そちらの方がパフォーマンスへの悪影響があると考えています。

他に良い方法も見つからなかったので、一旦これは容認する事にしました。
正直、SQLなど土台の知識が不十分な為、この辺りをどこまで考慮すれば良いのかが、まだ判断難しい部分ではあります。

明日の予定

  • フロントサイドの実装
  • AWSへのデプロイ

明日にデプロイ出来れば、予定通りGW中に完成させられそうです。
残り2日間、なんとか完成させたいと思います。

※追記:九日目を投稿しました
【10日間でポートフォリオ作成に挑戦】9日目:フロントエンドの実装〜各種機能の修正