みんなの「作ってみた」

【10日間でポートフォリオ作成に挑戦】7日目:検索機能〜いいね機能の実装

2019/05/03

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日目:テストコードの実装

今日一日の作業内容

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

テストコードの修正

6日目でテストコードの実装を行いましたが、そのコードに対して、伊藤さん(@jnchito)からコメントでフィードバックを頂いたので、その修正を行いました。
(ちなみに、これでQiitaでの初コメント初アドバイスも、両方とも伊藤さんから頂いた事になります。なんとも有難い話:sob:

具体的なフィードバックの内容は、主に下記の3点です。

  • idに依存するコードは書かない
  • ドキュメントとしての読みやすさを意識する(I18nではなく、表示そのままの文言を使用)
  • letとlet!の適切な使い分け

詳しくは下記の動画で解説して下さっています。
Qiitaに載っていたRSpecのコードを勝手にコードレビューしてみた

なお、I18nについては、まだフロントのイメージが固まっていないので、一旦そのままにして、フロント実装後に、テストも表示の文言に書き換えようと思います。

かなり耳が痛いご指摘でしたが、おかげでRspecに対する理解が今まで以上に深まりました。
改めて、Qiitaに投稿して良かったと感じています!

検索機能の実装

検索機能の実装には、gemのransackを利用しています。

gem 'ransack'

また、どのページからでも検索出来る様に、ヘッダーに検索フォームを埋め込む事にしました。

その為、検索のロジックはapplication_controller.rbに記述しています。

controllers/application_controller.rb
before_action :set_search

def set_search
  @search = Post.ransack(params[:q])
  @posts = @search.result.page(params[:page]).per(10)
end

そして、application.html.hamlに、下記のコードを記述して、検索フォームを実装しました。

views/application.html.haml
= search_form_for @search, url: posts_path do |f|
  %dt= f.text_field :title_cont ,placeholder: t('common.message.search')
  %dd= f.submit t('common.button.search')

ルーティングはpostコントローラーのindexアクションに飛ぶ様に、`posts_path`を指定しています。
これでindexと同じビューを共有します。

なお、indexアクション内でも、postインスタンスを生成しているので、検索して既にインスタンスが存在する場合は、上書きされない様に、演算子をnilガードに変更しています。

controllers/posts_controller.rb
def index
  @posts ||= Post.page(params[:page]).per(10)
end

こうして出来上がったフォームは、こちらです。

ログイン・ログアウト両方表示されてしまってますが、フロントの実装を本格的に開始したら、ここも切り替えが出来る様にする予定です。

いいね機能の実装

続いて、記事に対する「いいね」機能を実装して行きます。

まずは、「いいね」を記録する為の、PostLikeのモデルとコントローラーを作成します。

rails g model PostLikes
rails g controller post_likes

一人のユーザーが同じ記事に何度もいいねが出来ない様に、バリデーションを設定します。

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

コントローラーでは、いいねを付けるアクションと、消すアクションの二つを定義します。

controller/post_likes_controller.rb
class PostLikesController < ApplicationController
  def create
    post_like = current_user.post_likes.build(post_id: params[:post_id])
    post_like.save!
    redirect_to "/posts/#{params[:post_id]}"
  end

  def destroy
    post_like = current_user.post_likes.find_by(post_id: params[:post_id])
    post_like.destroy!
    redirect_to "/posts/#{params[:post_id]}"
  end
end

ルーティングはpostにネストさせます。

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

最後にビューにいいねのリンクを追加します。
ログインしているか?いいね済みか?で、表示が切り替わる様にしています。

views/posts/_show.html.haml
.content-like
  - if user_signed_in?
    - if @post_like.nil?
      = @post_likes.count
      = link_to t('common.button.like'), post_post_likes_path(@post), method: :post
    - else
      = @post_likes.count
      = link_to t('common.button.unlike'), post_post_like_path(@post, @post_like), method: :delete

これで一先ず完成です。

今日の失敗

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

いいね機能の実装方法に確信が持てない

※追記(5/4):コメントにて、伊藤さん(@jnchito)からアドバイスを頂いています。本当に有り難過ぎる:sob:

いいね機能については、先程のコードで一先ず狙い通りの動作をしてくれる様になりましたが、完全に我流で考えたコードなので、正直これが最適解では無いのではないか?という疑念を抱いています。

特に気になっているのが、下記のコードです。

redirect_to "/posts/#{params[:post_id]}"

これはいいねの処理が完了した後に、postコントローラーに戻して、再度showアクションを読み込ませる記述ですが、パスでは無く、下記の様に指定が出来ると考えています。

redirect_to controller: 'posts', action: 'show'

しかし、これで指定すると、下記の様なエラーが発生します。

ActionController::UrlGenerationError:
 No route matches {:action=>"show", :controller=>"posts", :post_id=>"1"}

ルーティングは以下の通り。

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

結局原因が特定出来なかった為、直接パスを指定する方法で実装しています。
こちらについては、後日調査して、より効果的な実装方法が無いか探ります。

他にも、「いいねの有無で表示を切り分ける部分」や「いいねを作成・削除するコントローラーのロジック」など、もっと良い書き方がありそうなので、他の方の実装例も参照して、修正を加えて行きたい。

テストの抜け漏れ

現在実装されている機能は、単体テスト・統合テストともに記述したのですが、削除機能のテストコードが漏れていたりなど、テストの抜け漏れがいくつか見つかりました。

正直、テスト設計的な知見が現状無い為、開発がひと段落したら、そのあたりの知見も取り入れて行きたい。

明日の予定

  • 記事のストック機能(自身の記事も可)
  • 別ユーザーのフォロー機能
  • フロントサイドの実装

明日にフロント実装に入ることが出来れば、デプロイで大きく躓かない限り、GW中に最低限の仕様の実装は終えられると考えている。
後、3日間、なんとか乗りきろう!

※追記:八日目を投稿しました
【10日間でポートフォリオ作成に挑戦】8日目:記事ストック機能〜ユーザーフォロー機能の実装