-
Notifications
You must be signed in to change notification settings - Fork 1
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
メモ一覧・詳細APIのレスポンスにタグ名を追加 #115
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kuri0616
コメントしました! 🙏
@@ -10,7 +10,7 @@ def index | |||
|
|||
# GET /memos/:id | |||
def show | |||
@memo = Memo.find(params[:id]) | |||
@memo = Memo.includes(:tags).find(params[:id]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nits
includesはeager_loadにもpreloadの挙動にもなり得るので、この場合はpreloadを使用するようにしましょう!
@memo = Memo.includes(:tags).find(params[:id]) | |
@memo = Memo.preload(:tags).find(params[:id]) |
へえ、直接関連していなくても、has_many throughの記述があれば先読みできるんですね!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
includesはクエリによって挙動がかわるのですね!
知らなかったです。
結合で一括取得か、テーブル毎にin句で取得か、今回は、関連先の要素で絞り込みを行ったりしないし、preloadのが良いということですかね!
めちゃくちゃ勉強なりました!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
結合で一括取得か、テーブル毎にin句で取得か、今回は、関連先の要素で絞り込みを行ったりしないし、preloadのが良いということですかね!
確かに基本的にLEFT JOINすると重複が生まれるためにpreloadの方が速そうに思えますが、
必ずしもそうでないわけではないので、毎回preloadがいいわけではないとは思います!
また、あまりにもIN句が大きすぎるとSQL文のbyte数が大きくなっちゃってのlimitに引っかかることもあります。
Ref: https://moroto1122.hatenadiary.org/entry/20100805/1280992251
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preloadとeager_loadの速度を比較したいなら、benchmarkとってみるといいと思います。
(「推測するな、計測せよ」の精神です。)
https://qiita.com/scivola/items/c5b2aeaf7d67a9ef310a
backend/app/models/memo.rb
Outdated
@@ -37,7 +37,11 @@ def call(filter_collection:, params:) | |||
params: params | |||
) | |||
memo_count = memo_relation.count | |||
{ memos: PageFilter.resolve(scope: memo_relation, params: params), | |||
paginated_memos = PageFilter.resolve(scope: memo_relation, params: params) | |||
memo_with_tags = paginated_memos.includes(:tags).map do |memo| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nits/IMO
controller側でMemo.allする代わりにMemo.preload(memo_tags: :tags)
するようにしましょうか。
また、jbuilderを使用するようにしましょう。
# GET /memos
def index
@memos = Memo::Query.call(filter_collection: preload(memo_tags: :tags), params: params), status: :ok
rescue TypeError
render json: { error: 'ページパラメータが無効です' }, status: :bad_request
end
(ちなみにhashに変換するのは完全な悪手というわけではなくて、場合によってはメモリ効率的に良かったりすることもあります。)
なので、ここではmapを実行する必要はないかなと思っていまして、
以下のようにMemoTagクラスにdelegateの記述をするといいと思っています。
MemoTagの記述
class MemoTag
delegate :name, to: :tag, prefix: true
end
jbuilderの記述
json.tag_names @memo.tags.map(&:tag_name)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jbuilderの存在、完全に忘れておりました・・・
モデルのメソッド側でフォーマットしなくて良いのも利点ですね!
delegate :name, to: :tag, prefix: true
delegateも恥ずかしながら初めて知りました・・・
指定したモデルのメソッドを譲渡できるんですね、prefixもオプション指定で自動でつけてくれるのも便利ですね
@@ -8,6 +8,8 @@ json.memo do | |||
json.created_at @memo.created_at | |||
json.updated_at @memo.updated_at | |||
|
|||
json.tag_names @memo.tags.map(&:name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delegate使うと以下のような記述になるかなと。
json.tag_names @memo.tags.map(&:name) | |
json.tag_names @memo.tags.map(&:tag_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
こちら
delegate :name, to: :tag, prefix: true
の対応なんですが
memoTagモデルで上記を定義すれば
下記のようにmemoTagモデルからtagモデルのnameメソッドをtag_nameというメソッドで呼べるようになるという感じですかね?
memo.memo_tags.map(&:tag_name)
今回はおそらくhas_many throughの定義によってmemoのインスタンスから直接tagsにアクセスかつnameメソッドを呼べるからいらないという認識であっていますかね?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ごめんなさい、コメント雑にしちゃってました!
今回はこのままで良いですね!
backend/spec/models/memo_spec.rb
Outdated
let(:firts_expected_memo) do | ||
{ | ||
'id' => memos[0].id, | ||
'title' => memos[0].title, | ||
'content' => memos[0].content, | ||
'poster' => memos[0].poster, | ||
'created_at' => memos[0].created_at, | ||
'updated_at' => memos[0].updated_at, | ||
:tag_names => memos[0].tags.map(&:name) | ||
} | ||
end | ||
let(:second_expected_memo) do | ||
{ | ||
'id' => memos[1].id, | ||
'title' => memos[1].title, | ||
'content' => memos[1].content, | ||
'poster' => memos[1].poster, | ||
'created_at' => memos[1].created_at, | ||
'updated_at' => memos[1].updated_at, | ||
:tag_names => memos[1].tags.map(&:name) | ||
} | ||
end | ||
let!(:third_expected_memo) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO/Nits
わざわざletで定義するほどでもないかなとおもいました。
また、letはどちらかというと、テストに必要な初期データを用意するイメージで、期待する結果をletで用意するのはあまり見ないかなと思ってます。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
letはどちらかというと、テストに必要な初期データを用意するイメージで、期待する結果をletで用意するのはあまり見ないかなと思ってます。
たしかに、今までも期待値はcontext内で作っていましたね💦
tag_nameの配列もcallメソッドではなくjbuilderでするようになったので、モデルのテストではなくコントローラーのテストで検証するようにしました!
backend/spec/factories/memos.rb
Outdated
after :create do |memo| | ||
create_list(:memo_tag, 3, memo: memo) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
traitで定義してあげたほうがいいと思いました。
(optionalでmemo_tagを作成するようにした方がパフォーマンスがいい。)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
たしかに全てのテストケースでtagのレコード作成する必要ないですもんね
trait 使えば指定したシンボルを記載したときだけ、作成するようにできるんですね!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kuri0616
コメントしましたが、LGTMです!
|
||
context 'ログイン中かつメモが存在し、パラメータが指定されていない場合' do | ||
before { sign_in(user) } | ||
|
||
it '降順で、1ページ目に10件のメモが返ること' do | ||
aggregate_failures do | ||
get '/memos' | ||
get '/memos', headers: { Accept: 'application/json' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
質問
このリクエストヘッダー、必要ですかね?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このヘッダー僕も不要だと思っていたのですが、Rspecのテスト&Postmanで動作確認したときにPostmanでは、正常に成功、Rspecでは、500が返ってきまして💦
binding.pryでデバックしたところ
get '/memos'
でエンドポイント叩いたとき
headers明示的に指定しなかった場合
indexアクションの
render 'index'
のところでjbuilderのファイルを探すことができずなっていたんですよね
その時のログ
1. MemosController GET /memos ログイン中かつメモが存在し、パラメータが指定されていない場合 降順で、1 ページ目に 10 件のメモが返ること
Failure/Error: assert_response_schema_confirm(200)
Committee::InvalidResponse:
Expected `200` status code, but it was `500`.
# /usr/local/bundle/gems/committee-5.1.0/lib/committee/test/methods.rb:32:in `assert_response_schema_confirm'
# ./spec/requests/memos_spec.rb:16:in `block (5 levels) in <top (required)>'
# ./spec/requests/memos_spec.rb:13:in `block (4 levels) in <top (required)>'
render 部分のログ
[1] pry(#<MemosController>)> render 'index'
ActionView::MissingTemplate: Missing template memos/index, application/index with {:locale=>[:ja], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :jbuilder]}.
Searched in:
- "/usr/src/app/app/views"
- "/usr/local/bundle/gems/actiontext-7.2.1/app/views"
- "/usr/local/bundle/gems/actionmailbox-7.2.1/app/views"
ここの
formats=>[:html]
がhtmlになっているので
index.html.[handlersの配列の中にいずれか]
のパス内を探しているため、index.json.jbuilderが見つからず500になっていると判断して、明示的に指定するようにしました💦
before do | ||
memos_data = Array.new(20) do | ||
{ | ||
title: Faker::Lorem.sentence(word_count: 3), | ||
content: Faker::Lorem.paragraph(sentence_count: 5), | ||
poster: Faker::Name.name, | ||
created_at: Time.current, | ||
updated_at: Time.current | ||
} | ||
end | ||
|
||
described_class.bulk_import!(memos_data) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ついでにパフォーマンス向上させてます。
# そこそこ重いSQLを発行するので、一回だけ呼ばれるようにしたい。 | ||
# ref: https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md | ||
let_it_be(:memos) do | ||
memos_data = Array.new(20) do | ||
{ | ||
title: Faker::Lorem.sentence(word_count: 3), | ||
content: Faker::Lorem.paragraph(sentence_count: 5), | ||
poster: Faker::Name.name, | ||
created_at: Time.current, | ||
updated_at: Time.current | ||
} | ||
end | ||
|
||
Memo.bulk_import!(memos_data) | ||
Memo.order(:id).last(20) | ||
end | ||
|
||
# そこそこ重いSQLを発行するので、一回だけ呼ばれるようにしたい。 | ||
# ref: https://github.com/test-prof/test-prof/blob/master/docs/recipes/before_all.md | ||
before_all do | ||
# Tagの生成 | ||
tag_data = Array.new(3) do |n| | ||
{ | ||
name: "tag-#{n + 1}", | ||
priority: n + 1, | ||
created_at: Time.current, | ||
updated_at: Time.current | ||
} | ||
end | ||
Tag.bulk_import!(tag_data) | ||
tags = Tag.pluck(:id) | ||
|
||
# MemoTagの生成 | ||
memo_tags_data = memos.flat_map do |memo| | ||
tags.map do |tag_id| | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ここもついでにパフォーマンス向上させてます。
全体のテスト実行時間は大体半分くらいになってるはずです!
@kuri0616 テスト実行速度の比較Before
After
やったこと
|
対応するissue
対応内容
確認したいこと、聞きたいこと
context毎に期待値を定義する方法も検討したのですが、全てのcontextブロックに記載するとコード量も多くなってしまうかつ他の解決策が思いつかなかったため、とりあえず3件にしています。