Skip to content

UserCoursePractice.unstarted_practicesメソッドのリファクタリング#9000

Merged
komagata merged 3 commits intomainfrom
refactor-unstarted_practices-method
Aug 24, 2025
Merged

UserCoursePractice.unstarted_practicesメソッドのリファクタリング#9000
komagata merged 3 commits intomainfrom
refactor-unstarted_practices-method

Conversation

@jun-kondo
Copy link
Copy Markdown
Contributor

@jun-kondo jun-kondo commented Jul 30, 2025

Issue

概要

UserCoursePractice.unstarted_practicesメソッドのrails-patterns gemのQueryオブジェクトのUserUnstartedPracticesQueryクラスにリファクタリングしました。
戻り値としてプラクティスの配列を返していましたが、ActiveRecord::Relationを返すようにしています。

また変更前はpracticesを下記で取得していましたが、

practices = @user.course.practices

practicesのソートをCoursePracticeの中間テーブルのデフォルトスコープに依存していたので、UserUnstartedPracticesQueryクラス内で明示的に並び順を指定するようにしました。

issueで挙げられていた問題点の改善まとめ

可読性の低さ: 3つのORクエリーがチェーンされて非常に読みにくい

変更後: “OR”でつないでいた複数の条件を、配列を使った“IN”句へと書き換えてシンプルにした

重複したJOIN: 同じpractices.joins(:learnings)が3回繰り返されている

変更後: learningテーブルへのJOINを行ず、条件を満たすpractice_idlearningテーブルから直接取り出すサブクエリにすることでJOINを一箇所にまとめた

保守性: 学習ステータスの追加時に3箇所を修正する必要がある

変更後: ステータスの管理をlearned_practice_idsメソッド1箇所に切り出し

パフォーマンス: 効率的な1つのクエリーにまとめられる可能性

変更後

  • .where.not(id: learned_practice_ids)の利用で「未着手」のみを直接抽出
  • 変更前はすべてのプラクティスと未着手ステータス以外のプラクティスを配列の差集合でunstarted_practicesを計算していたため、クエリが二回発生していた。変更後は一回のクエリで済むよう改善
発行されるSQL

変更前

-- 1つ目のクエリ
SELECT "practices".* 
FROM "practices" 
INNER JOIN "categories_practices" 
  ON "practices"."id" = "categories_practices"."practice_id" 
INNER JOIN "categories" 
  ON "categories_practices"."category_id" = "categories"."id" 
INNER JOIN "courses_categories" 
  ON "categories"."id" = "courses_categories"."category_id" 
WHERE "courses_categories"."course_id" = $1 
ORDER BY 
  "categories_practices"."position" ASC, 
  "courses_categories"."position" ASC;

-- パラメータ
-- [["course_id", 829913840]]



-- 2つ目のクエリ
SELECT "practices".* 
FROM "practices" 
INNER JOIN "categories_practices" 
  ON "practices"."id" = "categories_practices"."practice_id" 
INNER JOIN "categories" 
  ON "categories_practices"."category_id" = "categories"."id" 
INNER JOIN "courses_categories" 
  ON "categories"."id" = "courses_categories"."category_id" 
INNER JOIN "learnings" 
  ON "learnings"."practice_id" = "practices"."id" 
WHERE "courses_categories"."course_id" = $1 
  AND "learnings"."user_id" = $2 
  AND (
    "learnings"."status" = $3 
    OR "learnings"."status" = $4 
    OR "learnings"."status" = $5
  )
ORDER BY 
  "categories_practices"."position" ASC, 
  "courses_categories"."position" ASC;

-- パラメータ
-- [["course_id", 829913840], ["user_id", 253826460], ["status", 1], ["status", 2], ["status", 3]]

変更後

SELECT 
  practices.*, 
  courses_categories.position AS category_position, 
  categories_practices.position AS practice_position 
FROM "practices" 
INNER JOIN "categories_practices" 
  ON "categories_practices"."practice_id" = "practices"."id" 
INNER JOIN "categories" 
  ON "categories"."id" = "categories_practices"."category_id" 
INNER JOIN "courses_categories" 
  ON "courses_categories"."category_id" = "categories"."id" 
WHERE "courses_categories"."course_id" = $1 
  AND "practices"."id" NOT IN (
    SELECT "learnings"."practice_id" 
    FROM "learnings" 
    WHERE "learnings"."user_id" = $2 
      AND "learnings"."status" IN ($3, $4, $5)
  ) 
ORDER BY 
  categories_practices.position ASC, 
  courses_categories.position ASC;

-- パラメータ
-- [["course_id", 829913840], ["user_id", 655153192], ["status", 1], ["status", 2], ["status", 3]]

変更確認方法

ブランチをローカルに取り込む

git fetch origin refactor-unstarted_practices-method 
git checkout refactor-unstarted_practices-method 

ローカルサーバーの立ち上げ

rails db:reset
bin/setup
foreman start -f Procfile.dev
  1. ユーザーhatsuno`でログイン
    • ユーザー名: hatsuno
    • パスワード: testtest
  2. プラクティス一覧ページに行き下記を確認する
    1. 着手ステータスのプラクティスがないことを確認する
    2. Mac OS XカテゴリのOS X Mountain Lionをクリーンインストールするが未着手になっていることを確認する。なっていなければ未着手にしておく。
    3. どこでも良いのでアプリ内別のページに遷移し、サイドナビゲーションバーのプラクティスをクリックしプラクティスページに戻る。
      • Mac OS Xカテゴリのスクロール位置でページが表示されることを確認する
    4. UNIXカテゴリのTerminalの基礎を覚えるが未着手になっていることを確認する。なっていなければ未着手にしておく。
    5. OS X Mountain Lionをクリーンインストールするのプラクティスページに行き、ステータスを修了にする
    6. どこでも良いのでアプリ内別のページに遷移し、サイドナビゲーションバーのプラクティスをクリックしプラクティスページに戻る。
      • UNIXカテゴリにスクロールされた位置でページが表示されることを確認する
        ※ ステータス変更時はナビゲーションバーのリンクが更新されないので、別ページに遷移してからプラクティスページのリンクをクリックするか、リンクを2回クリックするとスクロール位置が更新されます。

Screenshot

Mac OS XカテゴリのOS X Mountain Lionをクリーンインストールするを修了すると、
Image from Gyazo
次回からのプラクティスページ一覧に遷移したときは次のUNIXカテゴリに遷移された状態で表示されます。
Image from Gyazo

Summary by CodeRabbit

  • リファクタリング

    • ユーザーごとの未開始プラクティス取得処理を内部で整理・分離し、責務と可読性を向上しました。動作や利用者向けの挙動には影響ありません。
    • 内部で専用のクエリを導入してキャッシュを活用し、将来的な保守性とパフォーマンスを高めました(実装の詳細は非公開)。
  • テスト

    • 各ユーザーごとに未開始のプラクティスのみが返ることを検証するテストを追加しました。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jul 30, 2025

Walkthrough

UserCoursePractice#unstarted_practices のクエリロジックを専用クエリオブジェクト UserUnstartedPracticesQuery に抽出し、該当クエリクラスとそのテストを追加しました。公開APIに変更はありません。

Changes

Cohort / File(s) Change Summary
UserCoursePracticeのリファクタ
app/models/user_course_practice.rb
unstarted_practices をインラインのActiveRecordフィルタから UserUnstartedPracticesQuery.new(user: @user).call へ置換し結果をキャッシュ。
新規クエリオブジェクト追加
app/queries/user_unstarted_practices_query.rb
新規クラス UserUnstartedPracticesQuery < Patterns::Query を追加。ユーザーの course_id を元に結合・フィルタを行い、ユーザーが started/submitted/complete の学習をした practice を除外して未着手の practices を返す実装。
クエリオブジェクトのテスト追加
test/queries/user_unstarted_practices_query_test.rb
新規テストを追加。特定ユーザーに対して started/submitted/complete の学習済みプラクティスを除外し、未着手プラクティスのみが返ること、ユーザーごとに結果が異なることを検証。

Sequence Diagram(s)

sequenceDiagram
    participant Caller as UserCoursePractice
    participant QueryObj as UserUnstartedPracticesQuery
    participant DB as Practice DB

    Caller->>QueryObj: .new(user: @user).call
    QueryObj->>DB: JOIN categories_practices/courses_categories\nWHERE course_id = user.course_id\nAND id NOT IN (learned_practice_ids)\nORDER BY positions
    DB-->>QueryObj: 未着手プラクティス一覧
    QueryObj-->>Caller: 結果を返却
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

うさぎがそっとクエリを引き出す🐇
役割分けでコードは軽やかに
テストがぴょんと見守る庭で
未着手だけが静かに残るよ
さあ、ぴょんとマージへ跳ねていこう

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 RuboCop (1.76.1)
app/models/user_course_practice.rb

rubocop-minitest extension supports plugin, specify plugins: rubocop-minitest instead of require: rubocop-minitest in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-capybara extension supports plugin, specify plugins: rubocop-capybara instead of require: rubocop-capybara in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
Unable to find gem rubocop-fjord; is the gem installed? Gem::MissingSpecError
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:309:in rescue in gem_config_path' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:293:in gem_config_path'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:84:in block (2 levels) in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:82:in reverse_each'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:82:in block in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:76:in each_pair'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:76:in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader.rb:58:in load_file'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_store.rb:29:in options_config=' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:162:in act_on_options'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:47:in block in run' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:82:in profile_if_needed'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:43:in run' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/exe/rubocop:19:in <top (required)>'
/usr/local/bin/rubocop:25:in load' /usr/local/bin/rubocop:25:in

'

app/queries/user_unstarted_practices_query.rb

rubocop-minitest extension supports plugin, specify plugins: rubocop-minitest instead of require: rubocop-minitest in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-capybara extension supports plugin, specify plugins: rubocop-capybara instead of require: rubocop-capybara in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
Unable to find gem rubocop-fjord; is the gem installed? Gem::MissingSpecError
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:309:in rescue in gem_config_path' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:293:in gem_config_path'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:84:in block (2 levels) in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:82:in reverse_each'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:82:in block in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:76:in each_pair'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:76:in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader.rb:58:in load_file'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_store.rb:29:in options_config=' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:162:in act_on_options'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:47:in block in run' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:82:in profile_if_needed'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:43:in run' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/exe/rubocop:19:in <top (required)>'
/usr/local/bin/rubocop:25:in load' /usr/local/bin/rubocop:25:in

'

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 541f458 and ed289be.

📒 Files selected for processing (2)
  • app/models/user_course_practice.rb (1 hunks)
  • app/queries/user_unstarted_practices_query.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/queries/user_unstarted_practices_query.rb
  • app/models/user_course_practice.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build_and_test
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-unstarted_practices-method

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@jun-kondo jun-kondo self-assigned this Jul 30, 2025
@jun-kondo jun-kondo marked this pull request as ready for review July 30, 2025 18:27
@github-actions github-actions bot requested a review from komagata July 30, 2025 18:28
@jun-kondo jun-kondo requested a review from tyrrell-IH July 30, 2025 18:29
@jun-kondo
Copy link
Copy Markdown
Contributor Author

@tyrrell-IH
お疲れ様ですー
お手隙の際で構いませんので、レビューお願い出来ますでしょうか?🙏
よろしくお願い致します🙇‍♂️

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
test/queries/user_unstarted_practices_query_test.rb (1)

21-46: 個別ステータステストの統合を検討してください

既に包括的なテストが lines 6-19 で実装されているため、個別のステータステスト(lines 21-28, 30-37, 39-46)は重複している可能性があります。包括的なテストで十分なカバレッジが得られている場合は、これらの個別テストを削除してテストファイルを簡潔にすることを検討してください。

以下のdiffで個別テストを削除できます:

-  test 'should exclude practices with started status' do
-    user = users(:komagata)
-    started_practice = practices(:practice1)
-
-    result = UserUnstartedPracticesQuery.new(user:).call
-
-    assert_not_includes result, started_practice
-  end
-
-  test 'should exclude practices with submitted status' do
-    user = users(:komagata)
-    submitted_practice = practices(:practice4)
-
-    result = UserUnstartedPracticesQuery.new(user:).call
-
-    assert_not_includes result, submitted_practice
-  end
-
-  test 'should exclude practices with complete status' do
-    user = users(:komagata)
-    completed_practice = practices(:practice2)
-
-    result = UserUnstartedPracticesQuery.new(user:).call
-
-    assert_not_includes result, completed_practice
-  end
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c868887 and 32cbe98.

📒 Files selected for processing (3)
  • app/models/user_course_practice.rb (1 hunks)
  • app/queries/user_unstarted_practices_query.rb (1 hunks)
  • test/queries/user_unstarted_practices_query_test.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rb

⚙️ CodeRabbit Configuration File

**/*.rb: # refactoring

  • まずFat Controllerを避け、次にFat Modelを避ける。
  • Serviceクラスの乱用を避ける。
  • controller concernを作ろうとしたらPORO(Plain Old Ruby Object)やActiveRecordモデルでの実装で代替できないか検討する。

Rails Patterns

  • ViewHelperにメソッドを実装する時にはまずDecoratorパターンを使うことを検討する。(active_decorator gemを導入しているのでそれを使う)
  • 複雑なActiveRecordクエリがあり、再利用できそうな場合はQueryObjectパターンを検討する。(rails-patterns gemを導入しているのでそれのQuery機能を使う)
  • Viewにpartialを作る場合はViewComponentを使うことを検討する。
  • 複数のActiveRecordモデルを操作する1つの責務がある時や外部APIとやりとりする処理がある場合にはInteractorオブジェクトパターンを検討する。(interactor gemを導入しているのでそれを使う)
  • 複数のInteractorを実行するような処理がある場合Organizerオブジェクトパターンを検討する。(interactor gemを導入しており、その中にOrganizerの機能があるので使う)

Files:

  • app/models/user_course_practice.rb
  • test/queries/user_unstarted_practices_query_test.rb
  • app/queries/user_unstarted_practices_query.rb
test/**/*

⚙️ CodeRabbit Configuration File

test/**/*: # Test

  • TestCase名は英語で書く。
  • どうしても避けられない時以外にsystem testでsleepは使わない。

Unit Test

model, helper, decorator, view_componentについてはメソッドを追加した場合は必ず対応したUnit TestのTestCaseを1つは書く。

Files:

  • test/queries/user_unstarted_practices_query_test.rb
🧠 Learnings (2)
📓 Common learnings
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: PRのスコープを維持することが重要であり、バグ修正PRにおいて、修正対象以外の機能(例:ルーティングテスト)の追加提案は適切ではない。そのような改善が必要な場合は別PRで対応すべきである。
Learnt from: kitarou888
PR: fjordllc/bootcamp#8277
File: test/models/searcher_test.rb:326-331
Timestamp: 2025-07-10T12:59:27.807Z
Learning: モデルテストにおいて、1つのクラスメソッドの複数の挙動を検証する場合、機能の異なる側面を同じテストメソッドでテストすることは、包括的なテストとして適切である。特に`only_me`のような機能では、異なる検索条件(空文字と具体的な検索語)を使い分けることで、より広範囲な動作保証が可能となる。
Learnt from: reckyy
PR: fjordllc/bootcamp#8356
File: db/fixtures/users.yml:823-826
Timestamp: 2025-07-15T12:33:04.885Z
Learning: fjordllc/bootcampプロジェクトでは、`trainee: true`は「研修に所属していたユーザー」全体を示すフラグであり、研修中のユーザーと研修を終了したユーザーの両方を含む。そのため、`trainee: true`と`training_completed_at`の併存は意図された仕様であり、排他的な関係ではない。
test/queries/user_unstarted_practices_query_test.rb (2)

Learnt from: kitarou888
PR: #8277
File: test/models/searcher_test.rb:326-331
Timestamp: 2025-07-10T12:59:27.807Z
Learning: モデルテストにおいて、1つのクラスメソッドの複数の挙動を検証する場合、機能の異なる側面を同じテストメソッドでテストすることは、包括的なテストとして適切である。特にonly_meのような機能では、異なる検索条件(空文字と具体的な検索語)を使い分けることで、より広範囲な動作保証が可能となる。

Learnt from: reckyy
PR: #8356
File: db/fixtures/users.yml:823-826
Timestamp: 2025-07-15T12:33:04.885Z
Learning: fjordllc/bootcampプロジェクトでは、trainee: trueは「研修に所属していたユーザー」全体を示すフラグであり、研修中のユーザーと研修を終了したユーザーの両方を含む。そのため、trainee: truetraining_completed_atの併存は意図された仕様であり、排他的な関係ではない。

🔇 Additional comments (7)
app/queries/user_unstarted_practices_query.rb (4)

3-4: Query オブジェクトパターンが適切に適用されています

Patterns::Query を継承し、queries Practice でターゲットモデルを明示的に指定している実装は rails-patterns gem の規約に従っており、良い設計です。


8-12: 初期化処理が適切に実装されています

ユーザーとコースの設定が正しく行われており、親クラスの super(relation) 呼び出しも適切です。


14-21: クエリロジックが大幅に改善されています

以下の改善点が確認できます:

  • 複数のORクエリから単一のNOT INクエリへの変更で可読性が向上
  • 明示的なソート順の指定によりデフォルトスコープへの依存を解消
  • JOINの最適化により重複が削減

ソート順の practice_position ASC, category_position ASC により、プラクティスの位置とカテゴリの位置で適切に並び順が制御されています。


23-26: 学習ステータスの管理が一元化されています

学習済みプラクティスIDの取得ロジックが1箇所に集約され、保守性が向上しています。Learning.statuses.values_at を使用することで、ステータス値のハードコーディングを避けている点も良い実装です。

test/queries/user_unstarted_practices_query_test.rb (2)

6-19: 包括的なテストケースが適切に実装されています

1つのテストメソッドで複数の学習ステータス(started、submitted、complete、unstarted)の挙動を検証しており、retrieved learnings に基づくと、このような包括的なテストアプローチは適切です。未着手プラクティスの取得という1つの機能の異なる側面を効率的にテストできています。


48-56: ユーザー固有の動作テストが適切に実装されています

異なるユーザー間で結果が異なることを確認するテストにより、クエリがユーザー固有の学習状況を正しく反映していることが検証されています。

app/models/user_course_practice.rb (1)

96-98: クエリオブジェクトパターンへのリファクタリングが適切に実装されています

複雑なActiveRecordクエリを UserUnstartedPracticesQuery に抽出し、コーディングガイドラインに従ってrails-patterns gemのQuery機能を活用しています。メモ化も維持されており、パフォーマンスへの配慮も適切です。

このリファクタリングにより以下が達成されています:

  • Fat Model の回避
  • 複雑なクエリロジックの再利用可能性向上
  • 可読性と保守性の向上

@tyrrell-IH
Copy link
Copy Markdown
Contributor

@jun-kondo
お疲れ様です〜。
8/8(金)期限とかでよければレビューできますが、いかがでしょうか?

@jun-kondo
Copy link
Copy Markdown
Contributor Author

@tyrrell-IH
大丈夫ですー🙌
よろしくお願いします🙇‍♂️

@jun-kondo jun-kondo force-pushed the refactor-unstarted_practices-method branch from 32cbe98 to e1fd2d5 Compare August 6, 2025 17:47
@jun-kondo
Copy link
Copy Markdown
Contributor Author

@tyrrell-IH
お疲れ様ですー
本日変更部分を更新してプッシュしたところCIのテストで落ちてしまいました😭
変更とは関係ない箇所で落ちているので影響はないとは思うのですが、一旦中断にしたほうが良いでしょうか?
期限直前のご連絡でご迷惑をおかけしてしまい、本当に申し訳ありません🙇‍♂️

@tyrrell-IH
Copy link
Copy Markdown
Contributor

@jun-kondo
すいません、まだ見れてないので修正入れてもらって全然問題ないです👍

変更とは関係ない箇所で落ちているので影響はないとは思うのですが、一旦中断にしたほうが良いでしょうか?

現在の状態でレビューしますので大丈夫です。何かあったら連絡します〜

@tyrrell-IH
Copy link
Copy Markdown
Contributor

@jun-kondo
すいません、別件で対応したいことがあって期限を8/11(月)に延ばしていただけないでしょうか?

もし難しければ、何とか時間を見て8/8(金)中にレビューします

@jun-kondo
Copy link
Copy Markdown
Contributor Author

@tyrrell-IH
お疲れ様ですー
期限の件、承知しましたー
急ぎませんので、のんびりお待ちしております🙌

Copy link
Copy Markdown
Contributor

@tyrrell-IH tyrrell-IH left a comment

Choose a reason for hiding this comment

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

@jun-kondo
コメントしました!
間違ってる点、何を言ってるかよくわからない点があったら気兼ねなくおっしゃってください!

以下余談です。
今回の実装上は問題なく、あくまで仕様についての私見ですが、そもそもcategories_practices.positionの昇順が第一優先なのは少し変だなと思いました。

Mac OS Xカテゴリーの最初のプラクティスOS X Mountain Lionをクリーンインストールするを修了にしても、同カテゴリーにはまだ未着手のプラクティスが残っているので、アンカーの場所はMac OS Xのままで良いのでは?とちょっと思いました。

Comment on lines +16 to +20
.joins(categories: :courses_categories)
.where(courses_categories: { course_id: @course.id })
.where.not(id: learned_practice_ids)
.select('practices.*')
.order('categories_practices.position ASC, courses_categories.position ASC')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

categories_practices.positionを使うならcategories_practicesテーブルもJOINしていいような気がしました。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

こちら修正しました。
アソシエーション経由でSQLではJOINされているのですが、明示的に書いたほうがわかりやすいですね👀
ac987ca

Comment on lines +23 to +26
def learned_practice_ids
statuses = Learning.statuses.values_at(:started, :submitted, :complete)
Learning.where(user_id: @user.id, status: statuses).select(:practice_id)
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

サブクエリにするととても読みやすいと思いました。勉強になりました🙏

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ありがとうございます😄


require 'test_helper'

class UserUnstartedPracticesQueryTest < ActiveSupport::TestCase
Copy link
Copy Markdown
Contributor

@tyrrell-IH tyrrell-IH Aug 11, 2025

Choose a reason for hiding this comment

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

未着手のプラクティスの並び(最初の要素が意図したものになっているか)を保証するようなテストも必要かなと思いました。

UserCoursePractice#unstarted_practicesが使われる箇所は

# UserCoursePractice#category_active_or_unstarted_practic

def category_active_or_unstarted_practice
  if @user.active_practices.present?
    category_having_active_practice
  elsif unstarted_practices.present?
    category_having_unstarted_practice
  end
end

# UserCoursePractice#category_having_unstarted_practice

def category_having_unstarted_practice
  unstarted_practices&.first&.categories&.first 
end

となっており、
unstarted_practices&.first&.categories&.first
unstarted_practices内の最初のプラクティスの属するカテゴリーの位置をアンカーにしているので、意図した並びになっていないとアンカーになるカテゴリーの位置も変わってくるのかなと思いました🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます🙇‍♂️
UserCoursePractice#category_active_or_unstarted_practicには既存のモデルテストがありまして、こちらで目的を満たせていると考えたので今回のQueryObjectクラスのテストには含みませんでしたが如何でしょうか?👀

bootcamp/test/models/user_course_practice_test.rb at 1adb919575bdb27bd8bad627c1d00c88aadfcc2f · fjordllc/bootcamp · GitHub

(以下補足)
テストは下記のようになっており、
1つめのアサーションではuser(:komagata)は着手中のプラクティスを持っているので、category_having_active_practiceを検証しています。
2つめのアサーションではusers(:machida)は着手中ステータスのプラクティスを持っていないので、category_having_unstarted_practiceの値、つまり先頭のプラクティスが所属するcategoryのidを検証しています。

  test 'get category active or unstarted practice' do
    komagata = @user_course_practice_komagata
    assert_equal 917_504_053, komagata.category_active_or_unstarted_practice.id
    machida = @user_course_practice_machida
    practice1 = practices(:practice1)
    user = users(:machida)
    create_checked_product(user, practices(:practice1))
    Learning.create!(
      user: users(:machida),
      practice: practice1,
      status: :complete
    )
    assert_equal 685_020_562, machida.category_active_or_unstarted_practice.id
  end

Copy link
Copy Markdown
Contributor

@tyrrell-IH tyrrell-IH Aug 14, 2025

Choose a reason for hiding this comment

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

すいません、そちらのテストは確認していませんでした🙏

おっしゃる通り、test 'get category active or unstarted practice' user: machidaのテストにおいて
test/fixtures/practices.ymlに登録されているデータ + テスト中に追加したpractice1(status: :complete)を合わせた上で、
どの未着手プラクティスが先頭→それがどのカテゴリーに属するかを検証してますね。
OKです🙆

Comment on lines +10 to +17
@user = user
@course = user.course
end

def query
relation
.joins(categories: :courses_categories)
.where(courses_categories: { course_id: @course.id })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

user.courseのように関連で引くと
SELECT "courses".* FROM "courses" WHERE "courses"."id" = $1 LIMIT $2 [["id", 829913840], ["LIMIT", 1]]
のように(初回だけ)クエリが発生するようです。

@course_id = user.course_idにして
.where(courses_categories: { course_id: @course_id })
でもいいのかなと思いました。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

こちら修正しました。
関連を使わずUserレコードのカラムの値を参照すれば余計なクエリ発行を減らせますね👀
25de41a

@jun-kondo jun-kondo force-pushed the refactor-unstarted_practices-method branch 2 times, most recently from 39a2c4c to ac987ca Compare August 11, 2025 21:43
@jun-kondo
Copy link
Copy Markdown
Contributor Author

@tyrrell-IH
お疲れ様です🍵
レビュー、コメントありがとうございます🙇‍♂️
修正を行い、コメント返信致しましたー
ご確認お願い致しますー🙇‍♂️

余談について

こちらもコメント頂きありがとうございます。この仕様に対するご指摘は正しいです🙆‍♂️
私も実装中に同じ様に思いまして、komagataさんとmachidaさんに確認したところ、categories_practices.positionではなくcourses_categories.positionが第一優先でソートされる方が正しいことがわかりました。

そのため別途issue#8998を作成し、私の方で修正対応します。
現状プルリクエストまでは作成出来ております。
プラクティス一覧ページを表示した際、適切なカテゴリにスクロールされないバグを修正 by jun-kondo · Pull Request #9014 · fjordllc/bootcamp

@tyrrell-IH tyrrell-IH self-requested a review August 12, 2025 01:57
Copy link
Copy Markdown
Contributor

@tyrrell-IH tyrrell-IH left a comment

Choose a reason for hiding this comment

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

確認しました、OKです!
1件コメント残しました。#9000 (comment)

test 'get category active or unstarted practice'については見ていなかったです。教えていただきありがとうございました🙏

@jun-kondo jun-kondo force-pushed the refactor-unstarted_practices-method branch from ac987ca to 541f458 Compare August 15, 2025 17:29
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/models/user_course_practice.rb (1)

96-98: 引数は@userではなくuserを使うとクラス内の表記が一貫します(軽微なnit)

このファイル内ではattr_reader :userを多用しているので、ここも合わせると読みやすくなります。

-    @unstarted_practices ||= UserUnstartedPracticesQuery.new(user: @user).call
+    @unstarted_practices ||= UserUnstartedPracticesQuery.new(user: user).call
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ac987ca and 541f458.

📒 Files selected for processing (3)
  • app/models/user_course_practice.rb (1 hunks)
  • app/queries/user_unstarted_practices_query.rb (1 hunks)
  • test/queries/user_unstarted_practices_query_test.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/queries/user_unstarted_practices_query.rb
  • test/queries/user_unstarted_practices_query_test.rb
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rb

⚙️ CodeRabbit Configuration File

**/*.rb: # refactoring

  • まずFat Controllerを避け、次にFat Modelを避ける。
  • Serviceクラスの乱用を避ける。
  • controller concernを作ろうとしたらPORO(Plain Old Ruby Object)やActiveRecordモデルでの実装で代替できないか検討する。

Rails Patterns

  • ViewHelperにメソッドを実装する時にはまずDecoratorパターンを使うことを検討する。(active_decorator gemを導入しているのでそれを使う)
  • 複雑なActiveRecordクエリがあり、再利用できそうな場合はQueryObjectパターンを検討する。(rails-patterns gemを導入しているのでそれのQuery機能を使う)
  • Viewにpartialを作る場合はViewComponentを使うことを検討する。
  • 複数のActiveRecordモデルを操作する1つの責務がある時や外部APIとやりとりする処理がある場合にはInteractorオブジェクトパターンを検討する。(interactor gemを導入しているのでそれを使う)
  • 複数のInteractorを実行するような処理がある場合Organizerオブジェクトパターンを検討する。(interactor gemを導入しており、その中にOrganizerの機能があるので使う)

Files:

  • app/models/user_course_practice.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build_and_test
🔇 Additional comments (2)
app/models/user_course_practice.rb (2)

96-98: Queryオブジェクト化とメモ化の導入は適切です/LGTM

複雑なクエリをUserUnstartedPracticesQueryへ委譲し、Relationを返す形にしたのは、可読性・保守性・パフォーマンスの観点で良い判断です。単一クエリ化+明示的なORDERも◎。


96-98: 確認結果 — 問題なし(Array専用の差集合演算は未検出)

軽くリポジトリ内を検索しました。unstarted_practices の利用箇所は下記のみで、Array 固有の差集合演算(-)は検出されませんでした。現状のままで影響はありません。

  • app/models/user_course_practice.rb:38 — elsif unstarted_practices.present?
  • app/models/user_course_practice.rb:97 — def unstarted_practices (実装: UserUnstartedPracticesQuery.new(user: @user).call)
  • app/models/user_course_practice.rb:105 — unstarted_practices&.first&.categories&.first

@jun-kondo
Copy link
Copy Markdown
Contributor Author

@tyrrell-IH
レビューありがとうございました🙌

@jun-kondo
Copy link
Copy Markdown
Contributor Author

@komagata
お疲れ様です!!
チームメンバーから承認を頂けましたので、レビューよろしくお願い致しますー🙇‍♂️

- 変更前: user.courseの関連で参照していた
- 変更後: user.course_idでuserレコードの外部キーを参照する
- これにより関連をロードするためのクエリ発行を省略する
- CategoriesPracticeテーブルをjoinsメソッドの引数に追加
- 中間テーブルの関連付けをアソシエーションによる暗黙的なものから明示的に記載する
@jun-kondo jun-kondo force-pushed the refactor-unstarted_practices-method branch from 541f458 to ed289be Compare August 20, 2025 14:19
Copy link
Copy Markdown
Member

@komagata komagata left a comment

Choose a reason for hiding this comment

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

確認させて頂きました。OKです〜🙆‍♂️

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