Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

メモの検索をできるようにする #72

Merged
merged 29 commits into from
Sep 9, 2024
Merged

Conversation

shoutarou123
Copy link
Collaborator

対応するissue

closes #20 検索機能の実装

対応内容

検索機能実装が完了しましたので、ご確認お願いします。

Comment on lines 30 to 27
private

def resolve
FILTERS.reduce(filter_collection) do |memo_scope, filter|
const_get(filter).resolve(scope: memo_scope, params: filter_params)
end
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このprivateのresolveメソッドはどこで使用されているのでしょうか?
見た感じ、使用されていないように見えまして!間違っていたらすみません!
もし、不要であれば削除お願いします!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
削除いたしました。

Comment on lines 24 to 28
def self.resolve(filter_collection:, filter_params:)
FILTERS.reduce(filter_collection) do |memo_scope, filter|
const_get(filter).resolve(scope: memo_scope, params: filter_params)
end
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

引数の名前ですが、個人的には
resolveメソッドの引数の名前ですが個人的にはfilter_collectionよりはmemosの方がわかりやすいかなと思いました

      def self.resolve(memos:, filter_params:)
        FILTERS.reduce(memos) do |memo_scope, filter|
          const_get(filter).resolve(scope: memo_scope, params: filter_params)
        end
      end

メモの集合を受け取ってreduceメソッドのブロック引数でmemo_scopeという流れの方が、引数のメモでフィルタリングするという意図が伝わりやすいかなと!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
確認ありがとうございます。修正いたしました。

@kuri0616
Copy link
Collaborator

ブランチ名は

feature/search

ではなくドキュメントの開発フローに沿って命名していただければと思います!(次回からで大丈夫です)
https://github.com/Progaku-copy/progaku-archive/blob/main/docs/getting-started/team_dev_flow.md
今回でいうと

feature/#20-implement-memo-search

とかになるかと思います(あくまで一例ですが)
ブランチ名を明確にすることで
どのissueと紐づいているのか、何を目的としたブランチなのかが明確になります!
そうすることで例えば後から参画した人が仕様を理解するために参照する時、レビュアーがレビューする時など認知負荷の軽減に繋がります!

searchだけだと何の「検索」なのかが一目でわからないため、まずはそこを理解するためにソース読み解く必要があるためです!

Comment on lines 158 to 169
describe 'SearchResolver' do
let!(:test_memo_first) { create(:memo, title: 'テスト タイトル1', content: 'テスト コンテンツ1') }
let!(:another_title_memo) { create(:memo, title: 'その他 タイトル', content: 'その他 コンテンツ') }
let!(:test_memo_third) { create(:memo, title: 'テスト タイトル2', content: 'テスト コンテンツ2') }

context 'タイトルで検索した場合' do
it 'タイトルフィルターが正しく機能することを確認する' do
filter_params = { title: 'テスト' }
result = Memo::SearchResolver.resolve(filter_collection: Memo.all, filter_params: filter_params)
expect(result).to contain_exactly(test_memo_first, test_memo_third)
end
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらに記載された検索機能のテストですが、これだとSearchResolverモジュールのresolveメソッドのユニットテスト(単体テスト)になるように思いました!
メソッド単体が、正常に動作しているかをテストするのであればとても良いのですが、今回はrequest_specで、所定のAPIエンドポイントにリクエストを送り、期待するレスポンスが返ってくるのか?という観点のテストになるのでそのような形式でテストを行うことが望ましいと思います!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

具体的には以下のようになるかと思います(例です)

describe 'GET /memos?title={title}' do
  context 'タイトルで検索した場合' do
    let!(:searchable_memo_1) { create(:memo, title: 'テスト タイトル1', content: 'テスト コンテンツ1') }
    let!(:searchable_memo_2) { create(:memo, title: 'テスト タイトル2', content: 'テスト コンテンツ2') }
    let!(:non_searchable_memo) { create(:memo, title: 'その他 タイトル', content: 'その他 コンテンツ') }

    it 'タイトルフィルターが正しく機能し、期待されるメモが取得できることを確認する' do
      aggregate_failures do
        get '/memos', params: { title: 'テスト' }
        expect(response).to have_http_status(:ok)
        expect(response.parsed_body['memos'].length).to eq(2)

        result_memo_ids = response.parsed_body['memos'].map { |item| item['id'] }
        expected_memo_ids = [searchable_memo_1.id, searchable_memo_2.id]
        expect(result_memo_ids).to eq(expected_memo_ids)
      end
    end
  end
end

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
確認ありがとうございます。また例文も大変助かります。
こちらを参考に修正させていただきました。

@@ -154,4 +154,50 @@
end
end
end

describe 'SearchResolver' do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

describeですが他のテストと合わせて、エンドポイントのパスを記載するようにしていただきたいです!

describe 'GET /memos?title={title}' 

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このファイルはrequests specなので、
describe 'GET /memos' doのブロック内に書きましょうか。
また別のコメントでりくやさんがおっしゃっている通り、SearchResolverのテストはmodels specに書くようにしましょうか〜

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
内容修正させていただきました。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochi-sho-private-study
おつかれさまです。
こちらご指摘いただいた内容について、modelsフォルダ内のmemo_spec.rbに記述するとうことでしょうか?
それともrequestsフォルダ内にmodels_spec.rbを作成し、そちらに記述するということでしょうか?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shoutarou123
modelsディレクトリは単体テストを記載するディレクトリ
requestsディレクトリは結合テストを記載するディレクトリというイメージですね!

なのでSearchResolverの単体の動きを確認するテストは
modelsフォルダ内のmemo_spec.rbに記述する

そして僕が以下の例で記載した処理に関しては、新たにdescribe 'GET /memos?title={title}' というブロックを作るのではなくすでに存在する「describe 'GET /memos' do」のブロック内に記載するという意味かと思います!

describe 'GET /memos?title={title}' do
context 'タイトルで検索した場合' do
let!(:searchable_memo_1) { create(:memo, title: 'テスト タイトル1', content: 'テスト コンテンツ1') }
let!(:searchable_memo_2) { create(:memo, title: 'テスト タイトル2', content: 'テスト コンテンツ2') }
let!(:non_searchable_memo) { create(:memo, title: 'その他 タイトル', content: 'その他 コンテンツ') }
..etc

Comment on lines 159 to 161
let!(:test_memo_first) { create(:memo, title: 'テスト タイトル1', content: 'テスト コンテンツ1') }
let!(:another_title_memo) { create(:memo, title: 'その他 タイトル', content: 'その他 コンテンツ') }
let!(:test_memo_third) { create(:memo, title: 'テスト タイトル2', content: 'テスト コンテンツ2') }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

変数名ですが
test_というプレフィクスはなくても良いかなと思いました!(テストに使用するということは明確なため)

    let!(:searchable_memo_1) { create(:memo, title: 'テスト タイトル1', content: 'テスト コンテンツ1') }
    let!(:searchable_memo_2) { create(:memo, title: 'テスト タイトル2', content: 'テスト コンテンツ2') }
    let!(:non_searchable_memo) { create(:memo, title: 'その他 タイトル', content: 'その他 コンテンツ') }

例えば上記のようにどれが検索対象のデータで、どれが対象外のデータなのか、わかるようにする方が、読んでいてわかりやすいかなと個人的に思いました!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
修正させていただきました。

Copy link
Contributor

@kakeru-one kakeru-one left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shoutarou123
コメントしました!

# @param filter_collection [ActiveRecord::Relation[Memo]]
# @param filter_params [ActionController::Parameters]
# @return [ActiveRecord::Relation[Memo]]
def self.resolve(filter_collection:, filter_params:)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO/Simple

filter_paramsではなくて、シンプルにparamsでいいと思いました!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochi-sho-private-study
ありがとうございます。内容修正させていただきました。

Comment on lines 6 to 7
memos = Memo.all
@memos = Memo::SearchResolver.resolve(filter_collection: memos, filter_params: params)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple/Performant

ここで変数に格納してしまうと、
全てのレコード分のActiveRecordのインスタンスが格納されてしまうので、
メモリをかなり消費してしまいます。
(参考文献にも挙げてますが、ActiveRecordのインスタンスは意外と重いのです。)

また、今回だとmemos = Memo.allという風に変数に格納してしまっているので、
ここでクエリが一度発行されてしまって、
Memo::SearchResolverでも一回発行してしまうので無駄なクエリが発行されてしまうと思います。

したがって、以下のように変数に格納しないように書いて、
メモリに載せないようにしましょう。

Suggested change
memos = Memo.all
@memos = Memo::SearchResolver.resolve(filter_collection: memos, filter_params: params)
@memos = Memo::SearchResolver.resolve(filter_collection: Memo.all, filter_params: params)

参考文献

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochi-sho-private-study
ありがとうございます。
内容修正させていただきました。

# @param filter_params [ActionController::Parameters]
# @return [ActiveRecord::Relation[Memo]]
def self.resolve(filter_collection:, filter_params:)
filter_params[:order] ||= 'desc'
Copy link
Contributor

@kakeru-one kakeru-one Aug 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readable/Simple

ここでfilter_paramsの値を格納するのは、
resolveメソッドの副作用になってしまうので、わかりづらいと思います。
(ここでいう副作用とは、本来期待している動作以外の作用を指します。)

以下のように、OrderFilterに閉じた動作にするのはどうでしょうか?
こうすることで、早期リターンも必要がなくなりシンプルになると思います。

module OrderFilter
  DEFAULT_ORDER = 'desc'
  private_constant :DEFAULT_ORDER

  def self.resolve(scope:, params:)
    scope.order(id: params[:order].presence || DEFAULT_ORDER)
  end
end

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochi-sho-private-study
ありがとうございます。ご指摘のとおり修正させていただきました。

private

def resolve
filter_params[:order] ||= 'desc'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらは二重に実行する必要がないと思います。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochi-sho-private-study
ありがとうございます。削除させていただきました。

Copy link
Contributor

@kakeru-one kakeru-one left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shoutarou123
コメントしました!

Comment on lines 86 to 88
let!(:searchable_memo_one) { described_class.create(title: 'テスト タイトル1', content: 'テスト コンテンツ1') }
let!(:searchable_memo_two) { described_class.create(title: 'テスト タイトル2', content: 'テスト コンテンツ2') }
let!(:non_searchable_memo) { described_class.create(title: 'その他 タイトル', content: 'その他 コンテンツ') }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idiomatic/Simple

factorybotを利用してテストデータを作成するようにしましょう!


以下のように記述できるかと思います。

let!(:memos) do
  {
    '1' => create(:memo, title: 'テスト タイトル1', content: 'テスト コンテンツ1'),
    '2' => create(:memo, title: 'テスト タイトル2', content: 'テスト コンテンツ2'),
    '3' => create(:memo, title: 'その他 タイトル', content: 'その他 コンテンツ')
  }
end

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochi-sho-private-study
修正しました。テストコードがシンプルになりました。勉強になります。

@@ -81,4 +81,55 @@
end
end
end

describe '検索機能のテスト' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idiomatic

テストの対象を具体的にしましょうか!

Suggested change
describe '検索機能のテスト' do
describe 'SearchResolver::resolve(memos:, params:)' do

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochi-sho-private-study
修正いたしました。

@@ -13,4 +13,46 @@
class Memo < ApplicationRecord
validates :title, :content, presence: true
has_many :comments, dependent: :destroy

module SearchResolver
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

提案

ralisのデザインパターン的にはQueryパターンになるので、
Queryモジュールにしてもらってもいいですか?
(最初に私が命名したのがミスリードでした 🙇)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochi-sho-private-study
修正させていただきました。

@kakeru-one kakeru-one changed the title 検索機能実装完了 メモの検索をできるようにする Sep 6, 2024
Comment on lines 6 to 7
@memos = Memo::SearchResolver.resolve(memos: Memo.all, params: params)
render json: { memos: @memos }, status: :ok
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここはインスタンス変数で必要はないと思いました!
Viewなどに値を渡すことがないためです!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
確認ありがとうございます。修正させていただきました。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
確認ありがとうございます。修正させていただきました。

Comment on lines 106 to 108
aggregate_failures do
result = Memo::Query.resolve(memos: described_class.all, params: { content: 'コンテンツ' })
expect(result).to include(memos['1'], memos['2'], memos['3'])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aggregate_failuresは複数の期待値を一度に、検証したい時に使用するのでここでは必要ないかなと思いました!
(1つの期待値しか検証していないため(expectが1回))

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
修正させていただきました。

Comment on lines 125 to 127
aggregate_failures do
result = Memo::Query.resolve(memos: described_class.all, params: { order: 'asc' })
expect(result).to eq([memos['1'], memos['2'], memos['3']])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらも同様です!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
修正させていただきました。

# Foreign Keys
#
# fk_comments_memo_id (memo_id => memos.id)
#
RSpec.describe Comment do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

今回の変更とは関係ない差分が反映されてしまっているため、除外していただきたいです🙇

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git revertでコミット済の変更を破棄することができます!

https://qiita.com/chihiro/items/2fa827d0eac98109e7ee

「アプリ再セットアップ」というコミットが対象のコミットなのでこのコミットIDを指定していただければと思います!
コミットIDはコミットを一意に識別するIDで
git logコマンドか
このGitHubの各コミットのページの右上あたりにも表示されています!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
修正させていただきました。

Comment on lines 5 to 20
# Table name: comments
#
# id :bigint not null, primary key
# content(内容) :text(65535) not null
# content(内容) :string(1024) not null
# created_at :datetime not null
# updated_at :datetime not null
# memo_id(メモID) :bigint not null
# memo_id(メモID) :integer not null
#
# Indexes
#
# index_comments_on_memo_id (memo_id)
#
# Foreign Keys
#
# fk_comments_memo_id (memo_id => memos.id)
#
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらも今回の変更とは関係ない差分が反映されてしまっているため、除外していただきたいです🙇

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
修正させていただきました。

Comment on lines 8 to 20
# content(内容) :string(1024) not null
# content(内容) :string(1024) not null
# created_at :datetime not null
# updated_at :datetime not null
# memo_id(メモID) :bigint not null
# memo_id(メモID) :integer not null
#
# Indexes
#
# index_comments_on_memo_id (memo_id)
#
# Foreign Keys
#
# fk_comments_memo_id (memo_id => memos.id)
#
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらも今回の変更とは関係ない差分が反映されてしまっているため、除外していただきたいです🙇

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikuya98
修正させていただきました。

Copy link
Contributor

@kakeru-one kakeru-one left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kakeru-one kakeru-one merged commit dd9dfd2 into main Sep 9, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

メモの検索機能を実装する
3 participants