質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.47%
Devise

Deviseとは、Ruby-on-Railsの認証機能を追加するプラグインです。

Ruby on Rails 6

Ruby on Rails 6は、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

Q&A

解決済

1回答

722閲覧

「カレントユーザーの情報のみ」かつ「DBでカウントした数」をモデルに記述したい。(コントローラーの肥大化を解消したい)

cherry2020

総合スコア10

Devise

Deviseとは、Ruby-on-Railsの認証機能を追加するプラグインです。

Ruby on Rails 6

Ruby on Rails 6は、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

0グッド

0クリップ

投稿2021/02/11 08:16

##実現したいことについて

  • オリジナルアプリを作っています。
  • 現在、チェックボタンにチェックをし、上の「SAVE」ボタンを押すとenumを使ってデータベースに保存されるようになっています。(チェックしたものは「1」、チェックがないものは「0」として、保存される)
  • 下の丸い部分について、「カレントユーザーの情報のみ」かつ「今までのカウント合計をカテゴリーごとに出す」実装を行っています。
  • 今回悩んでいることは、コントローラーが肥大化しており、カウント数を取得する部分についてモデルに記述したいものの、実装が上手くいかないことです。

イメージ説明

##現在のコントローラー(カウント数を取得することも記述しており、肥大化している・・改善できないか??????)

ruby

1class HabitsController < ApplicationController 2 before_action :authenticate_user! 3 4 def index 5 @habit = Habit.new 6 @count_1 = Habit.includes(:habits).where(user_id: current_user.id).where(count_1: 1).size 7 @count_2 = Habit.includes(:habits).where(user_id: current_user.id).where(count_2: 1).size 8 @count_3 = Habit.includes(:habits).where(user_id: current_user.id).where(count_3: 1).size 9 @count_4 = Habit.includes(:habits).where(user_id: current_user.id).where(count_4: 1).size 10 @count_5 = Habit.includes(:habits).where(user_id: current_user.id).where(count_5: 1).size 11 @restart_1 = Habit.includes(:habits).where(user_id: current_user.id).where(restart_1: 1).size 12 @restart_2 = Habit.includes(:habits).where(user_id: current_user.id).where(restart_2: 1).size 13 @restart_3 = Habit.includes(:habits).where(user_id: current_user.id).where(restart_3: 1).size 14 @restart_4 = Habit.includes(:habits).where(user_id: current_user.id).where(restart_4: 1).size 15 @restart_5 = Habit.includes(:habits).where(user_id: current_user.id).where(restart_5: 1).size 16 end 17 18 def new 19 end 20 21 def create 22 habit = Habit.create(habit_params) 23 end 24 25 private 26 27 def habit_params 28 params.require(:habit).permit(:count_1, :count_2, :count_3, :count_4, :count_5, :restart_1, :restart_2, :restart_3, :restart_4,:restart_5).merge(user_id: current_user.id) 29 end 30end

##現在のモデル(アソシエーションを組んでいる)

ruby

1# ユーザーモデル 2class User < ApplicationRecord 3 has_many :habits 4end

ruby

1# habitモデル 2class Habit < ApplicationRecord 3 belongs_to :user 4end

##現在のマイグレーションファイル

ruby

1class CreateHabits < ActiveRecord::Migration[6.0] 2 def change 3 create_table :habits do |t| 4 t.references :user,null: false, foreign_key: true 5 t.boolean :count_1 6 t.boolean :restart_1 7 t.boolean :count_2 8 t.boolean :restart_2 9 t.boolean :count_3 10 t.boolean :restart_3 11 t.boolean :count_4 12 t.boolean :restart_4 13 t.boolean :count_5 14 t.boolean :restart_5 15 t.timestamps 16 end 17 end 18end

##調べたこと(仮説と実証しているが上手くいかないこと)

まずはコントローラーの記述をリファクタリングできないか?と考えました。

###仮説1 N+1問題を解消するために、includesを使用する

ビフォー

ruby

1 @count_1 = current_user.habits.where(count_1: 1).size

アフター(現在のコード)

ruby

1 @count_1 = Habit.includes(:habits).where(user_id: current_user.id).where(count_1: 1).size

includesを使用したとしても、カテゴリーごとにcountsizeをしようとすると、複数行になってしまう。

###仮説2 ハッシュを使い、複数行になってしまっているカラムを一行にする

こちらの記事を参考に、下記の通り実装するも、データを取得することができない。

ruby

1 @habits = current_user.habits.includes(:habits{count_1: 1, count_2: 1, count_3: 1, count4: 1, count_5: 1, restart_1: 1,(省略)}).size

(補足)その後、ハッシュを使うのは、ネストされているとき(throughオプションなどを使用して、複数アソシエーションを組んでいるとき)時の話ではないか?と気がつきました。


ここからは、そもそもコントローラーとモデルの役割ってどういう立ち位置?と調べ直しました。その結果、DBのデータを扱うメソッド(検索メソッド、レコード取得メソッド、レコード更新メソッド)などはモデルに記述するのが適切では?と思いました。

###仮説3 モデルに記述する

ruby

1# モデル(まずは一つ目のカテゴリーを取得できるか実験) 2def self.count_habit 3 Habit.includes(:habits).where(user_id: current_user.id).where(count_1: 1).size 4end

ruby

1# コントローラー 2 def index 3 @habit = Habit.new 4 Habit.count_habit 5 end

としてbinding.pryを使用したところ、下記のようなエラー。

terminal

1NameError: undefined local variable or method `current_user' for #<Class:0x00007fc2023e19f0> 2Did you mean? current_scope

terminal

1# カレントユーザーを外すと上手くいく 2[3] pry(Habit)> Habit.includes(:habits).where(count_1: 1).size 3 CACHE (0.0ms) SELECT COUNT(*) FROM `habits` WHERE `habits`.`count_1` = TRUE 4 ↳ (pry):3:in `count_habit' 5=> 146

という状況です・・。

そこから、カレントユーザーに関しては、コントローラーで使えるものであって、モデルでは使用できないという事を知りました。

こちらの記事を参考に、application_controller.rbのビフォーアクションで、「カレントユーザーを前提にする方法」も試してみましたが、思った通りのデータを取得することができません。

##教えていただきたいこと

  • そもそも、上記のコントローラーの状況は良くないのでモデルに書きたいという方向性は合っているのでしょうか?

(初めは、ビューで<%= "通算#{@user.where(count_3: 1).count}回" %>のような表現をしていたのですが、今後JavaScriptで非同期通信を行いたい時に困ると感じ、コントローラーにまず移しました。)

  • そして「カレントユーザーの情報のみ」かつ、「今までのカウント合計をカテゴリーごとに出す」モデルの記述方法がわからず苦戦しております????記述方法のヒントだけでも、教えていただけると嬉しいです????

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

一般的には「モデルの情報で処理できるものはモデルに書く」というのは正しいと思っています。current_userは引数で渡せばよいです。
ただこの場合にそれがリファクタリングとして意味を持つか(すっきりするか)はちとわからない。

indexにかかれているような 一部だけ違って他は同じ、というとき私はこんなことします
@count = %w[count_1 count_2 count_3 count_4 count_5 restart_1 restart_2 restart_3 restart_4 restart_5].map {|clm| [clm, Habit.where(user_id: current_user.id, clm => 1).count ]}.to_h
これで { "count1" => 12 , ,,,,} が得られます。
Habit の検索に includes(:habits) は意味が無いです。多分無視されてます。
%w[count_1 ..... rstart_5] も冗長なので
%w[count_ rstart_].product(%w[1 2 3 4 5]).map(&:join) としたいけど、謎が深まるから良くはないかな。半分になるだけだからやめときますか。

投稿2021/02/11 12:11

winterboum

総合スコア23360

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

cherry2020

2021/02/11 13:00

winterboumさん、回答ありがとうございます。 モデルの情報で処理できることはモデルに書くことや、カレントユーザーをモデルで実装する場合には、引数で渡す方法があることについて勉強になりました。 そして10行あった文がwinterboumさんのおかげで1行で実装できました!!!(;ω;) 本当にありがとうございます!!!!!! コントローラーは 「@count = %w[count_ restart_].product(%w[1 2 3 4 5]).map(&:join).map {|clm| [clm,Habit.where(user_id: current_user.id, clm => 1).count]}.to_h」 ビューは 「<%= "通算#{@count["count_1"]}回" %>」で呼び出す でブラウザ上も反映しました。 配列を%W記法で表現することや、productメソッドやmapメソッドについて、使おうという発想がなかったたです。美しいコードに感動しました。 冗長なコードをすっきりまとめられる知識を身に付けたいと思いました。 winterboumさん本当にありがとうございました。
winterboum

2021/02/11 13:58

甘い! viewも同じ所と違う所を分け <% [ ここに coimt_1 とか並べる].each_with_index do |colmn, idx| %> して、 label[idx] と "通算#{@count[colmn]}回"
cherry2020

2021/02/12 14:26

winterboumさん、有難うございます! 教えていただいたものの、実装に苦慮して時間がかかってしまいました>< 結果、<% @counts.values.each do |val| %>と<%= "#{val}回" %>で 思い通りの文字を表示させることができました^^ (当初の5分の1の記述量になりました!!!) winterboumさんが教えてくださった「_with_index」をつけると 順番を併記することも出来るのですね。 ビューにおいて要素に関しても繰り返し表示させることが出来ると、大変勉強になりました。 解決後もフィードバックいただいてありがとうございました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問