20211013のRailsに関する記事は15件です。

devise ストロングパラメータの仕組みを調べてみた。

はじめに サインアップページにdeviseのデフォルトで許可されている要素ではない、"username"を追加することになり、deviseでのストロングパラメータの設定方法を調べた時のことを備忘録として投稿します。 ストロングパラメータとは 特定のパラメーターのみを許可することで、モデルへの不正なパラメータの流入を防ぐ仕組みのこと。MassAssingment脆弱性というセキュリティ上の問題に対処するための仕組みとしてRails4系から導入されましたものです。 deviseにおいては、モデルにパラメータを送ることのできるアクションは、3つに限定されており、パラーメータも以下のように限定されています。 action allowed params sign_in authentication keys sign_up authentication keys, password,password_confirmation account_update authentication keys,password, password_confirmation,current_password 参考:Strong Parameters サインアップ時のストロングパラメータの設定 サインアップ時にデフォルトで許可されているパラメータは、以下の3つです。 authentication keys(認証キー) password(パスワード) password_confirmation(パスワード確認用) ※authentication keys(認証キー)のデフォルト値は、emailです。 config/initializers/divise.rb # ==> Configuration for any authentication mechanism # Configure which keys are used when authenticating a user. The default is # just :email. ....中略..... # config.authentication_keys = [:email] これら以外を許可したい場合は、ApplicationControllerに設定する必要があります。以下は、サインアップアクションでusernameの値をパラメータとして許可する設定です。 application_controller.rb class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:username]) end end これで一見落着! と思ったのですが、一つ気になることがありました。 ログイン時のストロングパラメータの設定 何が気になったのかというと、ログイン(sign_in )アクションの許可されているパラメータについてです。もう一度見てみます。 action allowed params sign_in authentication keys 参考:Strong Parameters あれ??authentication keysだけなの??? 先述した通り、authentication keysのデフォルト値は、:emailです。つまり、許可されたパラメータは、:emailのみということでしょうか。 しかし、ログイン認証時には、サインアップの際に登録したパスワードと、ログインフォームから送られてきたパスワードを照合して、ログイン認証を行います。とすると、passwordも許可されているはずです。 このモヤモヤを解消するべく、deviseのストロングパラメータもう少し詳しく確認しました。 1.configure_permitted_parametersメソッドの確認 application_controller.rb def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:username]) end 1. devise_parameter_sanitizer devise/lib/devise/controllers/helpers.rb # Set up a param sanitizer to filter parameters using strong_parameters. See # lib/devise/parameter_sanitizer.rb for more info. Override this # method in your application controller to use your own parameter sanitizer. def devise_parameter_sanitizer @devise_parameter_sanitizer ||= Devise::ParameterSanitizer.new(resource_class, resource_name, params) end Set up a param sanitizer to filter parameters using strong_parameters. See lib/devise/parameter_sanitizer.rb for more info. Override this method in your application controller to use your own parameter sanitizer. (ストロングパラメータを使用してパラメータをフィルタリングするための、パラメータのサニタイザーを設定します。詳細については、lib/devise/parameter_sanitizer.rbをご覧ください。ご自身で設定したパラメータのサニタイザーを使いたい場合は、このメソッドをapplication controllerでオーバーライドしてください。) メソッドの処理は、 Devise::ParameterSanitizerクラスをインスタンス化しています。 詳細については、lib/devise/parameter_sanitizer.rbをご覧ください。とのことでしたので、確認してみます。 2. lib/devise/parameter_sanitizer.rb すると、デフォルトの設定が見つかりました!! devise/lib/devise/parameter_sanitizer.rb DEFAULT_PERMITTED_ATTRIBUTES = { sign_in: [:password, :remember_me], sign_up: [:password, :password_confirmation], account_update: [:password, :password_confirmation, :current_password] } ちゃんとpasswordが許可リストに入っています。それとremember_meもデフォルトで許可されているようです。 ちなみに、認証キーの情報については、以下の @auth_keys にありました。 devise/lib/devise/parameter_sanitizer.rb def initialize(resource_class, resource_name, params) @auth_keys = extract_auth_keys(resource_class) @params = params @resource_name = resource_name @permitted = {} DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys| permit(action, keys: keys) end end ここまでで、deviseのデフォルトのストロングパラメータがわかりました。 ストロングパラメータがどのように追加されるのかを確認してみましたので、以下に記載します。 devise ストロングパラメータの仕組み 1.Devise::ParameterSanitizerクラスからインスタンス作成 devise/lib/devise/controllers/helpers.rb def devise_parameter_sanitizer @devise_parameter_sanitizer ||= Devise::ParameterSanitizer.new(resource_class, resource_name, params) end 引数について 引数を理解するために、まずdevise_mappingについて確認します。 devise/app/controllers/devise_controller.rb # Attempt to find the mapped route for devise based on request path def devise_mapping @devise_mapping ||= request.env["devise.mapping"] end リクエストパスに基づいたマッピングルートを探してくれるメソッドです。 request.env["devise.mapping"]には、deviseのマッピング情報が格納されています。具体的には、routes.rbの"devise_for"で作成された、マッピングオブジェクトの情報が格納されています。 今回は、devise_for :usersとしています。 map.devise_for :users mapping = Devise.mappings[:user] mapping.name #=> :user # is the scope used in controllers and warden, given in the route as :singular. mapping.as #=> "users" # how the mapping should be search in the path, given in the route as :as. mapping.to #=> User # is the class to be loaded from routes, given in the route as :class_name. mapping.modules #=> [:authenticatable] # is the modules included in the class 参考:Class: Devise::Mapping それでは、引数を確認していきます。 1.resource_class devise/app/controllers/devise_controller.rb # Proxy to devise map class def resource_class devise_mapping.to end このメソッドでは、クラスネームが返されます。 Userが返されます。 2.resource_name devise/app/controllers/devise_controller.rb # Proxy to devise map name def resource_name devise_mapping.name end alias :scope_name :resource_name このメソッドでは、コントローラーで使用されているスコープ名が「:単数形」で返されます。 :userが返されます。 3.params リクエストからのパラメータ情報 2.ストロングパラメータの初期化処理 クラスからインスタンスが作成(new)されたことがトリガとなって、initializeメソッドが呼び出されます。newメソッドの引数をそのまま受け取ります。 まず、インスタンス変数の値をそれぞれ確認していきましょう。 devise/lib/devise/parameter_sanitizer.rb def initialize(resource_class, resource_name, params) @auth_keys = extract_auth_keys(resource_class) @params = params @resource_name = resource_name @permitted = {} DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys| permit(action, keys: keys) end end 1. @auth_keys extract_auth_keys(resource_class)の値が入っています。 devise/lib/devise/parameter_sanitizer.rb def extract_auth_keys(klass) auth_keys = klass.authentication_keys auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys end auth_keysに引数のauthentication_keysの値を代入しています。 引数(klass)の値は、resource_classの値(User)です。 devise/lib/devise/strategies/authenticatable.rb def authentication_keys @authentication_keys ||= mapping.to.authentication_keys end @authentication_keysにマッピングオブジェクトの認証キー情報を代入しています。 つまり、Userクラス(モデル)の認証キー情報を取得しているということですね。 ※認証キーのデフォルト値は、:emailです。 The default is # just :email. 2.@params 引数のparamsの情報(リクエストパラメータ)が入っています。 3.@resource_name 引数のresource_nameの値(:user)が入っています。 4.@permitted 空の配列が入ります。この配列に、この後アクション名をキーにした、ストロングパラメータの値がこの配列の中に格納されます。 3.パラメータのホワイトリスト(ストロングパラメータ)の作成 initializeメソッドの以下の部分がストロングパラメータの設定の部分にあたります。 devise/lib/devise/parameter_sanitizer.rb DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys| permit(action, keys: keys) end 1.each_pairメソッドについて each_pairメソッド は eachメソッドの別名(エイリアス)です。 ハッシュのキーと値を引数としてブロックを評価します。 DEFAULT_PERMITTED_ATTRIBUTESはハッシュ形式でデータが格納されていますので、ハッシュ用のeachメソッドを用います。 参考:instance method Hash#each 2.permitメソッドについて devise/lib/devise/parameter_sanitizer.rb # Add or remove new parameters to the permitted list of an +action+. # # === Arguments # # * +action+ - A +Symbol+ with the action that the controller is # performing, like +sign_up+, +sign_in+, etc. # * +keys:+ - An +Array+ of keys that also should be permitted. # * +except:+ - An +Array+ of keys that shouldn't be permitted. # * +block+ - A block that should be used to permit the action # parameters instead of the +Array+ based approach. The block will be # called with an +ActionController::Parameters+ instance. # # === Examples # # # Adding new parameters to be permitted in the `sign_up` action. # devise_parameter_sanitizer.permit(:sign_up, keys: [:subscribe_newsletter]) # # # Removing the `password` parameter from the `account_update` action. # devise_parameter_sanitizer.permit(:account_update, except: [:password]) # # # Using the block form to completely override how we permit the # # parameters for the `sign_up` action. # devise_parameter_sanitizer.permit(:sign_up) do |user| # user.permit(:email, :password, :password_confirmation) # end # # # Returns nothing. def permit(action, keys: nil, except: nil, &block) if block_given? @permitted[action] = block end if keys.present? @permitted[action] ||= @auth_keys.dup @permitted[action].concat(keys) end if except.present? @permitted[action] ||= @auth_keys.dup @permitted[action] = @permitted[action] - except end end 説明書きによると、 アクションごとに、パラメータの許可リストに新たなパラメータを追加または、削除するメソッドです。 引数については、以下の4つをとります。 action(シンボル形式のアクション名) keys( 許可するパラメータの配列) except(許可しないパラメータの配列) block(アクション自体の許可パラメータをオーバーライドする用) 戻り値は、何もありません。内部的に許可パラメータを設定する処理を行うものということですね。 処理としては、 引数の条件によって、initializeメソッドで定義した、@permittedの空配列にアクション名をキーに、許可するパラメータを値としてデータをハッシュ形式で格納します。 1.blockの場合 ブロックの値をそのまま配列に格納 2.keysが存在する場合(許可パラメータを追加) @permitted[action]の"action"にDEFAULT_PERMITTED_ATTRIBUTESのアクション名を代入。 @permitted[action]に、dupメソッドで@auth_keys(認証キー)から複製したオブジェクトを代入。 @permitted[action]に、concatメソッドで配列を追加 例(sign_inアクション) @permitted[:sign_in] = [:email,:password,:remember_me] hashにすると以下のようになる。 {:sign_in=>[:email,:password,:remember_me]} 3.exceptが存在する場合(許可パラメータを削除) @permitted[action]の"action"にDEFAULT_PERMITTED_ATTRIBUTESのアクション名を代入。 @permitted[action]に、dupメソッドで@auth_keys(認証キー)から複製したオブジェクトを代入。 ※認証キーは必須のため。 @permitted[action]から引数exceptの値を控除する。 まとめ devise/lib/devise/parameter_sanitizer.rb DEFAULT_PERMITTED_ATTRIBUTES = { sign_in: [:password, :remember_me], sign_up: [:password, :password_confirmation], account_update: [:password, :password_confirmation, :current_password] } devise/lib/devise/parameter_sanitizer.rb def initialize(resource_class, resource_name, params) @auth_keys = extract_auth_keys(resource_class) @params = params @resource_name = resource_name @permitted = {} DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys| permit(action, keys: keys) end end deviseのデフォルトのストロングパラメータの値は、ハッシュ形式でDEFAULT_PERMITTED_ATTRIBUTESに格納されている。 deviseインストールをすると、Devise::ParameterSanitizerクラスからインスタンスが作成され、それがトリガとなり、initializeメソッドが実行される。 initializeメソッドでは、スコープの認証キーの情報を取得し、permitメソッドで、取得した認証キーの情報と、DEFAULT_PERMITTED_ATTRIBUTESのデータからパラメータの許可リストを作成する。ここで作成される許可リストは、アクション名をキーに持ったハッシュ形式のリストである。 例(sign_inアクション) @permitted[:sign_in] = [:email,:password,:remember_me] hashにすると以下のようになる。 {:sign_in=>[:email,:password,:remember_me]} 参考 heartcombo/devise Strong Parameters Class: Devise::Mapping Class: Devise::ParameterSanitizer instance method Hash#each dup concat
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

I18n機能 日本語化対応

1.デフォルト言語の設定 デフォルト言語を日本語に変更するため、下記のコードをconfig/application.rbに追加 config/application.rb module I18nApp class Application < Rails::Application config.i18n.default_locale = :ja # 追加 end end 2.ja.ymlの作成
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ある日MySQLが急に動かなくなった時のデバッグ( The server quit without updating PID file )

はじめに ある日突然、rails s でエラーが発生したので解決した方法を記録します。 誰かの参考になることを祈ります。 開発環境 macOS BigSur 11.6 zsh: 5.8 (x86_64-apple-darwin20.0) Ruby: 2.6.3 Rails: 6.1.4.1 発生したエラー localhost:3000: $ mysql.server start #出力 Starting MySQL . ERROR! The server quit without updating PID file (/usr/local/var/mysql/*******.local.pid). その後、brew doctor、アクセス権限の再付与、エラーログのファイルを覗いて対策したり小手先のデバッグを色々してみたが結局似たようなエラー。 前日まで元気に動いていたのに、、、(涙目) ということで1度再インストールし直すのが良さげなので実践。 原因? MySQLのバージョン切り替え時に出るエラーらしい。 バージョンは変更していないのでHomebrewのアップデートでMySQLまでバージョンが切り替わり、前のバージョンの使用したデータが残っていたためエラーが起こった??? 対策(解決) 手順 - 現在のMySQL をアンインストール - プロセスの削除 - 新しくMySQL をインストール 上記の通り進めていきます。 1. 現在のMySQL をアンインストール brew uninstall mysql brew uninstall --force mysql brew cleanup -s mysql #brew uninstall mysql では削除できないファイル群の削除 rm -rf /usr/local/mysql rm -rf /Library/StartupItems/MYSQL rm -rf /Library/PreferencePanes/MySQL.prefPane rm -rf /Library/Receipts/mysql-.pkg rm -rf /usr/local/Cellar/mysql* rm -rf /usr/local/bin/mysql* rm -rf /usr/local/var/mysql* rm -rf /usr/local/etc/my.cnf rm -rf /usr/local/share/mysql* rm -rf /usr/local/opt/mysql 以下のコマンドで削除できているか確認します。 mysql --version which mysql 消えてればOK。 2. プロセスの削除 削除したMySQLとバージョンの違うMySQLがインストールされたとき、もとのバージョンのプロセスが残っているとエラーが発生することがあるらしいので削除しておく。 #プロセスの確認 ps -ef | grep mysql #出力 501 41981 1 0 8:19PM ttys002 0:00.02 /bin/sh /usr/local/Cellar/mysql/8.0.26/bin/mysqld_safe --datadir=/usr/local/var/mysql --pid-file=/usr/local/var/mysql/UsersMacBook-Pro.local.pid #表示されたプロセスを削除 kill -9 41981 killコマンドのオプション-9で強制終了。上記の41981がプロセスIDです。 3. 新しくMySQL をインストール #brewコマンドで新しくMySQLをインストール brew install mysql #インストール完了したら mysql.server start mysql -u root #ログインが確認できたらexit #Railsアプリに必要なら必要に応じて rails db:create rails db:migrate rails db:seed 参考文献 ・The server quit without updating PID file mysqlエラー解決方法 ・MacでHomebrewを使ってinstallしたMySQL5.6とMySQL5.7を切り替えて使う ・MySQL起動エラーの対処の仕方【Can't connect to local MySQL server through socket '/tmp/mysql.sock' (38)】 ・mysqlが死んだ日 終わりに 無事解決してよかった、、、。(切実) Twitterで日々の学習をアウトプットしています。 未熟者ですが、ご興味あれば覗いてやってください。→Twitter
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 既存カラムにdefault オプションを追加

1.使用環境 mac.os Catalina バージョン10.15.7 Ruby 3.0.1 Rails 6.0.4.1 psql (PostgreSQL) 13.4 node 14.18.10 npm 7.24.0 2.既存のカラムにdefaultオプションついか 1.追加用のテーブルを新たに作成します usersテーブルのadminカラムに追加する例です。 rails g migration change_admin_to_users 2. 先ほど作成したdbファイルに change_column :テーブル名,  :カラム名,  :データ型, null: false, default: false class ChengeAdminToUsers < ActiveRecord::Migration[6.0] def change change_column :users, :admin, :boolean, null: false, default: false end end ターミナルで下記を実行 rails db:migrate カラム追加・変更・削除は こちら を参考にしてみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

current_userとcorrect_userの違い

こんにちは!テックアカデミーのWebアプリコースを受講している駆け出し高校生エンジニアの安田駿介です。 今回は、「current_userとcorrect_userの違い」というテーマで解説していきます。 current_user 現在ログインしているユーザを取得するメソッド correct_user そのレコードを本当にログインユーザの所有しているものかを判別するメソッド レコードの編集、更新、削除など、持ち主しかやってはいけない機能を作るときによく使います。 まとめ current_userとcorrect_userは、JavaとJavaScriptのように、全く違うメソッドです。 開発環境 ・Cloud9 ・Ruby 3.0.0 ・Rails 6.1.3.1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

比較演算子と論理演算子について!

①.比較演算子(>, >=, <, <=, ==) 比較演算子は、値同士を比較したいときに使用します! 書き方はシンプルで、値と値の間に演算子を記述するだけです! 以下のようになっています! 比較演算子 使い方 説明 > A > B AはBより大きいか >= A >= B AはB以上か < A < B AはBより小さいか <= A <= B AはB以下か == A == B AとBは等しいか 比較演算子を使った場合、その条件が正しいかどうかによって、 trueもしくはfalseと呼ばれる値になります! 下記が例となっています! # 1は20より小さいか? irb(main):001:0> 1 < 20 => true # 5は5以下か? irb(main):002:0> 5 <= 5 => true # 1は20より大きいか? irb(main):003:0> 1 > 20 => false # 5は5以上か? irb(main):004:0> 5 >= 5 => true # 2 × 5は10と等しいか? irb(main):005:0> 2 * 5 == 10 => true # 10は20と等しいか? irb(main):006:0> 10 == 20 => false 比較演算子を使った場合は、式が正しければtrue、間違っていればfalseという結果になります! 文字列の"true"や"false"とは別の意味であり、真偽値を表していることは理解しておきましょう! ②.論理演算子(!) 式の真偽の確認や、真偽値に対しての演算を行うことができる演算子を論理演算子と呼びます! !(エクスクラメーションマーク)は not演算子 と呼ばれ、否定の意味で使われます! !と=を合わせた!=は、==と反対の意味で 値同士が等しくない 場合にtrueを返します! 下記が例となっています! # 2 × 3 は6ではないか? irb(main):001:0> 2 * 3 != 6 => false # 2 × 3 は10ではないか? irb(main):002:0> 2 * 3 != 10 => true このようになります! ③.まとめ 簡単に言うと、 比較演算子とは、値同士の比較ができる演算子のこと! 真偽値とは、true/falseのように真か偽かを扱う値のこと! 論理演算子(!)とは、真偽値に対しての演算を行うことができる演算子のこと! と言う感じですね! 日本語を正しく読んで理解するって言う感じですかね汗 地味に覚えにくいので、再度自分でも覚えるようにします^^; なにか説明が間違っていたら教えてください(_ _)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

<%= render 'パーシャルのパス', パーシャルで使う変数名: 変数に代入する値 %>

<%= render 'パーシャルのパス', パーシャルで使う変数名: 変数に代入する値 %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

undefined method "〇〇" for nil:NilClassのエラーの原因を実例を元に解説

こんにちは!テックアカデミーのWebアプリコースを受講している駆け出し高校生エンジニアの安田駿介です。 今回は、「undefined method "〇〇" for nil:NilClassのエラーの原因を実例を元に解説」というテーマで解説していきます。 エラー内容 解説 このエラーは、nilが〇〇を呼び出そうとしているからエラーになっています。 nil.each ~ という状態です。 今の状態だと、記載してあるはずの@cabinetsがnilになっています。 つまり、@cabinetsメソッドが未定義ということになります。 この場合だと、@cabinetsはcontroller#indexに記載するはずですが、それが記載していないということでした。 開発環境 ・Cloud9 ・Ruby 3.0.0 ・Rails 6.1.3.1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails x Herokuでheroku run rails db:migrateした時にError: Duplicate column name "email"となった時の対処方法

エラー内容 git push heroku masterしてheroku run rails db:migrateしたら以下エラーが発生 Mysql2::Error: Duplicate column name 'email' ↓このファイルがおかしいとのこと db/migrate/20211003044016_add_email_to_inquiries.rb class AddEmailToInquiries < ActiveRecord::Migration[6.1] def change add_column :inquiries, :email, :string end end ↓追加先テーブルを見たらすでにemailが追加されている db/migrate/20211002233717_create_inquiries.rb class CreateInquiries < ActiveRecord::Migration[6.1] def change create_table :inquiries do |t| t.string :name, null: false t.string :email, null: false t.text :message, null: false t.timestamps end end end 既に追加しているカラムをまた追加しようとしてエラー発生 結論 rollbackして再度migrateする 具体的な手順 1.migrateがどこまで進んだか確認する $ rails db:version Current version: 20211005113650 20211005113650_とついているファイルまでmigrateできている 2.戻したい所のバージョン確認 どこまで戻すか確認するためrails db:migrate:status実行 $ rails db:migrate:status  Status   Migration ID    Migration Name --------------------------------------------------    up     20210924214139  Create posts      up     20211002233717  Create inquiries   down    20211003044016  Add email to inquiries   down    20211005113650  Create relationships →downの所がおかしいのでその前(最後のup)まで戻す 3.バージョン指定してrollback 2個分もどす(=3個前まで実行された状態にしたい)ので、STEP=2を追加してrollback $ rails db:rollback STEP=2 4.migrateされてないファイルを確認 $ rails db:abort_if_pending_migrations You have 2 pending migrations: 20211003044016 AddEmailToInquiries 20211005113650 CreateRelationships 5.migrationファイル削除 私の場合、add_emailは不要なので削除する rm -rf db/migrate/20211003044016_add_email_to_inquiries.rb 6.ver指定してmigrate(不要かもしれません add_emailする必要はなくcreate_relationshipだけ実行したい →先ほど実行したrails db:migrate:statusでMigration IDを確認して以下コマンド実行 rails db:migrate:up VERSION=20211005113650 ※普通にrails db:migrateでも良かったかもしれません。。 ここまでやったらgitとherokuにpushして再度heroku run rails db:migrate実行 →マイグレーション成功! まとめ エラーを乗り越えるほど原因の心当たりが正確になるので解決が早くなると思うので頑張ります!! ※間違いなどありましたらコメントいただけますと幸いです。 参考サイト ・マイグレーションをロールバック - Railsドキュメント ・マイグレーションの履歴 - Railsドキュメント ・マイグレーションの実行 - Railsドキュメント ・【Rails】$rails db:rollbackしたい時の間違えない手順 - Qiita ・【Rails】マイグレーションファイルの削除 - Qiita
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] テーブル間にテーブルを作成し設定する方法

初めに なぜこの記事を書きたかったのか or この記事の対象者 テーブルとテーブルの間のテーブルの設定と仕組みが未だに自信ないから一度言語化させたかった。 特に理解が追いつけないのが、テーブル間のテーブルの設定になぜ別のモデルを作成しなければならないのか。 環境 ・Macbook Air (Retina, 13-inch,2019) ・プロセッサ 1.6GHz デュアルコアIntel Core i5 ・メモリ 8GB 2133 Mhz LPDDR3 ・MacOS Big Sur バージョン 11.5.2 記事の目次 1)フリマアプリを使って言語化してみる! 2)[したいこと] 3)[need1→how1]必要なテーブルの作成 4)[need2→how2]viewで記入欄の作成,controllerで各アクションの設定 5)[need3→how3]modelの設定 6)[need4→how4]viewで置き場所の設定 7)最後に フリマアプリを使って言語化してみる! [したいこと]ユーザが購入したものを履歴に残したい。 ※商品の出品、商品一覧ページ、商品詳細ページを省く [why]購入されたものをリストから除くように表示させることができるorユーザの購入傾向が分析できる [need1]ユーザ、商品、注文、購入履歴のデータのテーブルが必要 [need2]各テーブルの情報を保存するためのシステムが必要 [need3]必要な情報のみを保存するようなシステムが必要 [need4]保存した情報を出力させたい場所を作る [need1→how1]必要なテーブルの作成 USERS TABLE(個人の特定のため) PRODUCTS TABLE(商品の登録のため) ORDERS TABLE(注文するため) PURCHASE_HISTORIES TABLE(商品の有無のため) [need2→how2]viewで記入欄の作成,controllerで各アクションの設定 商品の購入ができる画面 <div class='transaction-contents'> <div class='transaction-main'> <h1 class='transaction-title-text'> 購入内容の確認 </h1> <div class='buy-item-info'> <% @product.images.each do |image| %> <%= image_tag image, class: 'images'%> <% end %> <div class='buy-item-right-content'> <h2 class='buy-item-text'> <%= @product.name %> </h2> <div class='buy-item-price'> <p class='item-price-text'>¥<%= @product.price %></p> <p class='item-price-sub-text'><%= @product.shipping_charges.name %></p> </div> </div> </div> <div class='item-payment'> <h1 class='item-payment-title'> 支払金額 </h1> <p class='item-payment-price'> ¥<%= @product.price %> </p> </div> <%= form_with model: @perchase_order ,url: product_orders_path, id: 'charge-form', class: 'transaction-form-wrap',local: true do |f| %> <%= render 'shared/error_messages', model:@perchase_order %> <div class='credit-card-form'> <h1 class='info-input-haedline'> クレジットカード情報入力 </h1> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">カード情報</label> <span class="indispensable">必須</span> </div> <%= f.text_field :credit_number, class:"input-default",id:"card-number", placeholder:"カード番号(半角英数字)", maxlength:"16" %> <div class='available-card'> <%= image_tag 'card-visa.gif', class: 'card-logo'%> <%= image_tag 'card-mastercard.gif', class: 'card-logo'%> <%= image_tag 'card-jcb.gif', class: 'card-logo'%> <%= image_tag 'card-amex.gif', class: 'card-logo'%> </div> </div> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">有効期限</label> <span class="indispensable">必須</span> </div> <div class='input-expiration-date-wrap'> <%= f.text_area :expire_month, class:"input-expiration-date",id:"card-exp-month", placeholder:"例)3" %> <p>月</p> <%= f.text_area :expire_year, class:"input-expiration-date",id:"card-exp-year", placeholder:"例)23" %> <p>年</p> </div> </div> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">セキュリティコード</label> <span class="indispensable">必須</span> </div> <%= f.text_field :security_code,class:"input-default",id:"card-cvc", placeholder:"カード背面4桁もしくは3桁の番号", maxlength:"4" %> </div> </div> <div class='shipping-address-form'> <h1 class='info-input-haedline'> 配送先入力 </h1> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">郵便番号</label> <span class="indispensable">必須</span> </div> <%= f.text_field :zip_code, class:"input-default", id:"postal-code", placeholder:"例)123-4567", maxlength:"8" %> </div> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">都道府県</label> <span class="indispensable">必須</span> </div> <%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, {}, {class:"select-box", id:"prefecture"}) %> </div> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">市区町村</label> <span class="indispensable">必須</span> </div> <%= f.text_field :municipality, class:"input-default", id:"city", placeholder:"例)横浜市緑区"%> </div> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">番地</label> <span class="indispensable">必須</span> </div> <%= f.text_field :adress, class:"input-default", id:"addresses", placeholder:"例)青山1-1-1"%> </div> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">建物名</label> <span class="form-any">任意</span> </div> <%= f.text_field :building_name, class:"input-default", id:"building", placeholder:"例)柳ビル103"%> </div> <div class="form-group"> <div class='form-text-wrap'> <label class="form-text">電話番号</label> <span class="indispensable">必須</span> </div> <%= f.text_field :phone_number, class:"input-default", id:"phone-number", placeholder:"例)09012345678",maxlength:"11"%> </div> </div> <div class='buy-btn'> <%= f.submit "購入" ,class:"buy-red-btn" %> </div> <% end %> </div> </div> 商品購入画面のアクション設定 class OrdersController < ApplicationController before_action :authenticate_user!, only: [:index, :create] before_action :move_to_index, only: [:index, :create] before_action :payed_redirect, only: [:index, :create] def index @perchase_order = PerchaseOrder.new end def create @perchase_order = PerchaseOrder.new(order_params) if @perchase_order.valid? paying @perchase_order.save redirect_to root_path else render :index end end private def order_params params.require(:perchase_order).permit(:zip_code, :prefecture_id, :municipality, :adress, :building_name, :phone_number).merge(user_id: current_user.id, token: params[:token], product_id: params[:product_id]) end def paying Payjp.api_key = ENV['PAYJP_SECRET_KEY'] Payjp::Charge.create( amount: @product.price, card: order_params[:token], currency: 'jpy' ) end def move_to_index @product = Product.find(params[:product_id]) redirect_to '/' if @product.user.id == current_user.id end def payed_redirect redirect_to '/' unless @product.purchase_history.nil? end end ファイルに名前をつけて、機能を追加する記述方法↓ class モデル名 機能,設定、ルール end 他のテーブル/モデルからカラムの取得する記述方法↓ include ActiveModel::Model altr_accessor :カラム名, :カラム名, :カラム名 [perchase_order.rbの画像] 注文するときに,情報を制限して保存する機能を持つファイル これで、クラス名を宣言すれば別のファイルにも使えるようになる [need3→how3]modelの設定 [user.rb画像] validationで保存するデータの制限+アソシエーション [product.rbの画像] validationで保存するデータの制限+アソシエーション(+画像の添付) [order.rbの画像] テーブル間のテーブル:perchase_historyとアソシエーションを組む [perchase_history.rbの画像] 間にいるテーブルが結んでいるテーブルとアソシエーションを組む [need4→how4]viewで置き場所の設定 [product/index.html.erbの画像] 商品の有無で表示方法を変える [products_controller.rbの画像] orders_controllerで取得したデータをproducts_controllerで使用できるように設定 最後 言語化してみてどこらへんが不安なところなのかも特定できてよかったと思う. 特定できた部分: ・なぜ間にテーブルを作成したのか。→多対多防止 ・なぜactive_hashでもないのに別ファイルでモデルを作ったのか→注文時に必要なデータを制限したかった and 注文後の処理を設定したかったから ・アソシエーションが組まれていないのになぜ使えるのか→class名で機能を配置することができる わかりにくいと感じましたら、ココ直したほうがいいよーなどのtipsをお願いします☺ ココまで読んでくださいましてありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rsepc】実行日時によってテストの挙動が変わってしまうテストはtravel_toを使おう

環境 Ruby 3.0.2 Rails 6.1.4.1 travel_toメソッド travel_toブロックの中だけは指定した日時でテストを実行できる。 rails_helperの設定も忘れずに rails_helper.rb RSpec.configure do |config| config.include ActiveSupport::Testing::TimeHelpers end context 'travel_toメソッドテスト' do travel_to Time.zone.local(2021, 1, 1) do p Date.today #=> Fri, 01 Jan 2021 end end 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】実行日時によってテストの挙動が変わってしまうテストはtravel_toを使おう

環境 Ruby 3.0.2 Rails 6.1.4.1 travel_toメソッド travel_toブロックの中だけは指定した日時でテストを実行できる。 rails_helperの設定も忘れずに rails_helper.rb RSpec.configure do |config| config.include ActiveSupport::Testing::TimeHelpers end context 'travel_toメソッドテスト' do travel_to Time.zone.local(2021, 1, 1) do p Date.today #=> Fri, 01 Jan 2021 end end 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker環境でvscodeのrubocop拡張が反応しない時の対処法

はじめに vscode の rubocop 拡張 ruby-rubocop を Docker 環境で使用したところかなり詰まったのでその備忘録です。 ホストに rubocop をインストールすれば良いという記事が散見されましたが、根本的な解決になっていません。ここではホストを汚さずコンテナ内の rubocop を参照するような解決方法をあげます。 一人でも多くの方の参考になれば幸いです。 何も設定していない時のエラー ( rubocop 拡張入れただけ) command /Users/horisho/.rbenv/shims/rubocop returns empty output! please check configuration. ホストに rubocop は存在しないので、 docker コンテナに実行先を向けないといけません。 初期設定 こちらのIssueコメントを参考に設定をしました。 rubocop の実行ファイルを作成し、実行環境を Docker コンテナ内に向けている形です。 ディレクトリ構成は以下のようです。 $ tree -L 2 -a . ├── .vscode │ └── settings.json ├── api │ ├── .rubocop.yml │ ├── rubocop │ └── 略 ├── docker-compose.yml └── 略 ファイルの中身は以下のようです。 api/rubocop #!/bin/bash docker-compose exec -T api bundle exec rubocop -c .rubocop.yml "$@" docker-compose.yml version: "3" services: api: build: context: ./api command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" depends_on: - db - redis volumes: - './api:/myapp' ports: - '3010:3000' tty: true stdin_open: true env_file: - .env # 以下略 .vscode/settings.json { "ruby.rubocop.executePath": "./api/" } rubocop 周りの設定ファイルはapiディレクトリに含め、起動コンテナにマウントしている状態です。また、Issueコメントとは異なり、 rubocop 拡張の実行パスを./apiに向けています。 遭遇したエラーメッセージ command ./api/rubocop returns empty output! please check configuration. ./rubocop is not executable 結論(修正方法) rubocop 実行ファイルをdocker-compose.ymlと同階層に配置し、実行パスをIssueコメント同様./に修正すれば良い。 $ tree -L 2 -a . ├── .vscode │ └── settings.json ├── api │ ├── .rubocop.yml │ └── 略 ├── docker-compose.yml ├── rubocop └── 略 .vscode/settings.json { "ruby.rubocop.executePath": "./" } 原因 rubocop 実行ファイル(/apiディレクトリ)で docker-compose コマンドを実行しています。当然 docker-compose は同階層の docker-compose.yml を参照しにいきますが、 /api ディレクトリには存在せず、ルートディレクトリに存在します。そのため、コンテナが見つからずエラーというわけです。 注意点 rubocop実行ファイルでdocker-compose exec api ...とexecコマンドを実行しているため、コンテナがすでに立ち上がっていないとエラーで怒られます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails の finder 系メソッドをちゃんと理解する

はじめに 普段空気のように finder 系メソッドを使っているけれど、改めて finder 系メソッドは何があったっけ?というのと内部実装について見ていき、何となくしか理解していない部分をちゃんと理解したいと思います。 finder 系メソッドの整理 今回は 2001/10/09 時点の最新の安定バージョン v6.1.4.1 のソースを元に書いていこうと思います。 finder 系メソッドは https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/core.rb https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/relation/finder_methods.rb あたりに詰まっています。 core.rb の方は Person.find(1) のようにモデルのクラスメソッドとして呼び出す場合で、 finder_methods.rb の方は Person.all.find(1) のようにリレーションのメソッドとして呼び出す場合ですが、どちらに定義されているメソッドも使い方は同じです。 定義されている public メソッドを列挙すると find find_by/find_by! take/take! first/first! last/last! second/second! third/third! fourth/fourth! fifth/fifth! forty_two/forty_two! third_to_last/third_to_last! second_to_last/second_to_last! exists? include? (member?) raise_record_not_found_exception! です。 42 番目のレコードを取得する forty_two は用途不明なのと、raise_record_not_found_exception! は直接は使ったことなかったので後で用途など探っていきたいと思います。 また、ここには where は定義されていません。 where は https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/relation/query_methods.rb#L634 に定義されており今回紹介する finder 系メソッドの各所で利用されています。 where は今回は事前知識としてあるものとして進めるが、where の引数として指定する条件は 文字列 + プレースホルダ形式 キーワード引数形式 arel_table 形式 辺りで指定するかと思いますのでサンプルだけ載せておきます。 (arel_table 形式の利用については賛否ある1のでここではそういうのもあると触れるだけです) # 文字列 + プレースホルダ形式 Person.where('age >= ?', 20).to_sql # => => "SELECT `people `.* FROM `people` WHERE (age >= 20)" # 文字列 + 名前付きプレースホルダ形式 Person.where('age >= :age', age: 20).to_sql # => => "SELECT `people `.* FROM `people` WHERE (age >= 20)" # キーワード引数形式 Person.where(age: 20).to_sql # => => "SELECT `people `.* FROM `people` WHERE `people`.`age` = 20" # arel_table 形式 Person.where(Person.arel_table[:age].gteq(20)).to_sql # => => "SELECT `people `.* FROM `people` WHERE `people`.`age` >= 20" find 定義 def find(*args) 使い方 引数に指定されたプライマリーキーのレコードを取得する。 プライマリーキーは通常多くの場合では id をプライマリーキーとしていると思うが、 Person.primary_key # => "id" で取得されるカラムがプライマリーキーです。 また、引数は配列や 2 引数以上で指定することも可能で、その場合には、レコードが配列で返ってくる。 Person.find(1, 2) の時にも配列で返ってくるが、 Person.find([1]) の時にも 1 レコードではあるが配列で返ってくる。 指定されたプライマリーキーが 1 つでも存在しない場合 指定されたプライマリーキーが 1 つでも存在しなかった場合には ActiveRecord::RecordNotFound が raise されるため、レコードが存在場合の処理も処理を継続したい場合には rescue するか、後述する find_by を利用する必要がある。 find_by/find_by! 定義 def find_by(arg, *args) def find_by!(arg, *args) 使い方 指定された条件にマッチするレコードを 1 つ取得する。 条件は where で紹介している引数の形式が利用できる。 Person.find_by(email: 'test@example.com') Person.find_by!(email: 'test@example.com') もちろん id でも利用できるため Person.find_by(id: 1) も問題ない。 指定された条件のレコードが存在しなかった場合 それが find_by と find_by! の違いでもあるが、指定された条件のレコードが取得出来なかった場合に - find_by => 指定された条件のレコードが取得出来なかった場合に nil が返る - find_by! => 指定された条件のレコードが取得出来なかった場合に ActiveRecord::RecordNotFound が raise される という違いがある。指定されたレコードが存在しない場合にも処理を継続したい場合には find_by を、後よろで ActiveRecord::RecordNotFound を raise したい場合には find_by! を使っておけば良い。 逆に、 find_by を利用した場合には nil が返る可能性があるので、必ずこの後には nil チェックが必要になってきます。無ければ臭いコード認定をしますので、忘れずにチェックするようにしましょう。 また、前述でも触れたように find ではレコードが存在しない場合には ActiveRecord::RecordNotFound が raise されるため、find_by で書き換えをすれば良い。 指定された条件に複数のレコードがマッチする場合 指定された条件に複数のレコードがマッチする場合は、最初に取得できたレコードが採用される。これは後述の take の実装でもあるが、順序関係なく DB から取得した際に最初に取得できるレコードになる。そのため DB の実装によっては変わってくる部分でもあるため、ただ 1 つのレコードのみが仕様的に取得出来るような条件で利用すべきです。 以後説明するメソッドでも `!` の有無についてのメソッドの動きは同様なので、これ以降は変わった動きが無い限りはあまり詳しくは触れない。 実装的にも def find_by(arg, *args) where(arg, *args).take end def find_by!(arg, *args) where(arg, *args).take! end のようになっているので、 ! の動作の違いは take と take! の違いなので次に行きます。 take/take! 定義 def take(limit = nil) def take! 使い方 レコードの順序に関係なく、レコードを 1 件 (limit が指定された場合は N 件) 取得する。 take! には limit は指定できない。 Person.take Person.take(3) Person.take! 指定した件数分取得できない場合 これは find_by で説明した時と同様です。 また、 take! の実装ではこの記事の最後に説明する raise_record_not_found_exception! が使われており、このメソッドがレコードが存在しない場合に ActiveRecord::RecordNotFound を raise してくれる君のようです。 def take! take || raise_record_not_found_exception! end limit と何が違うの問題 take も limit もどちらも SQL 的には LIMIT が発行されます。 Person.take(20) # => SELECT `people`.* FROM `people` LIMIT 20 Person.limit(20) # => SELECT `people`.* FROM `people` LIMIT 20 違いとしては、 take => Array が返される limit => ActiveRecord::Relation が返される かの違いになります。なので、続いてクエリメソッド等をチェインしたい場合には limit を利用する必要があります。 first/first! 定義 def first(limit = nil) def first! 使い方 プライマリーキーで並べ替え (ASC) された先頭 1 件 (limit が指定された場合は N 件) 取得する。 first! には limit は指定できない。 Person.first Person.first(3) Person.first! 以後は、take と同様なので省略します。 last/last! 定義 def last(limit = nil) def last! 使い方 プライマリーキーで並べ替え (ASC) された末尾 1 件 (limit が指定された場合は N 件) 取得する。 last! には limit は指定できない。 Person.last Person.last(3) Person.last! 以後は、take と同様なので省略します。 second/second! 定義 def second def second! 使い方 プライマリーキーで並べ替え (ASC) された先頭から 2 件目を取得する。 Person.second Person.second! 以後は、take と同様なので省略します。 また、 third, fourth, fifth, forty_two についても 3 件目 4 件目 5 件目 42 件目なので同様です。 ただ、 forty_two って何やねん コメントに書いてありました # Find the forty-second record. Also known as accessing "the reddit". # If no order is defined it will order by primary key. 「銀河ヒッチハイク・ガイド」 2 というアメリカのSFが元ネタで、42は「生命、宇宙、そして万物についての究極の疑問の答え」である数字らしいです。 (だから、、? third_to_last/third_to_last! 定義 def third_to_last def third_to_last! 使い方 プライマリーキーで並べ替え (ASC) された末尾から 3 件目を取得する。 Person.third_to_last Person.third_to_last! 以後は、take と同様なので省略します。 また、 second_to_last についても末尾から 2 件目なので同様です。 exists? 定義 def exists?(conditions = :none) 使い方 引数で指定された条件のレコードが存在するかを確認します。 また、クエリメソッドのチェーンでも使われ、引数なしの場合にはそれまでのメソッドチェーンのでのクエリ条件のレコードが存在するかという利用が可能。 この利用の仕方を知らなかったのだけど、Integer や String でただ一つの引数を指定すると、プライマリーキーのレコードが存在するかという利用も可能なようでした。 # プライマリーキーが 5 のレコードが存在するか? Person.exists?(5) Person.exists?('5') # 条件に一致するレコードが存在するか? Person.exists?(['name LIKE ?', "%#{query}%"]) Person.exists?(id: [1, 4, 8]) Person.exists?(name: 'David') # 常に false を返す (何のため? Person.exists?(false) # テーブルにレコードが存在するか? Person.exists? # where の条件のレコードが存在するか? Person.where(name: 'Spartacus', rating: 4).exists? include? 定義 def include?(record) 使い方 Array#include? と同じように、引数に指定したレコードが存在するか。 正確に言うと、引数に指定した引数レコードの id のレコードが存在するかを確認しています。 また、 member? にエイリアスされているので member? と書いても良い。 record = Person.first Person.where(name: 'David').include?(record) 実装を見ると def include?(record) if loaded? || offset_value || limit_value || having_clause.any? records.include?(record) else record.is_a?(klass) && exists?(record.id) end end のようになっていて、何故プライマリーキーではなく id 固定なのかは謎です。 raise_record_not_found_exception! 定義 def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key, not_found_ids = nil) 使い方 指定された引数にあったメッセージの ActiveRecord::RecordNotFound を raise してくれるメソッド。 このメソッドを直接呼ぶ機会が無さそうなのに public なメソッドになっているので一応載せています。 finder 系メソッドでもちょいちょい使われているのでそこでの使われ方を拝借すると raise_record_not_found_exception! raise_record_not_found_exception!(id, 0, 1) raise_record_not_found_exception!(ids, result.size, expected_size) のような感じ。有用な利用ユースケースあれば教えてほしいです。 まとめ 個人的には take, first, limit の動作の違いが分かっているようで怪しい線だったので、たまには理解しているつもりの箇所も実装を読んでみるのも良さそうと感じた。 あとは forty_two はただのジョークネタだと解決出来たのでひとまず寝れそう。 Arelでクエリを書くのはやめた方が良い5つの理由 - Qiita ↩ Phrases from The Hitchhiker's Guide to the Galaxy - Wikipedia ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails N+1問題への対処

課題 RailsのActiveRecordはRubyでデータベースとの問い合わせができる便利なツールである一方、発行されるSQLを意識しなくても済んでしまうため、効率の悪いクエリを発行していても気づかない危険性があります。Railsにおいては特にN+1問題という必要以上にデータベースに問い合わせをしてしまい、パフォーマンスを低下させてしまう問題がよく知られています。 N+1問題とは? N+1問題とは、「ループ処理の中で都度SQLを発行してしまい、大量のSQLが実行されパフォーマンスが低下する」という問題のことです。100件あれば100回、1000件あれば1000回、ループ処理の中で問い合わせすることになるので、いかにも非効率的です。馬鹿馬鹿しい初歩的なミスに聞こえますが、Railsを使用していると往々にしてこの問題が生じます。 では具体的なN+1問題の例を見ていきましょう。 まず、usersテーブルとpostsテーブルがあると仮定しましょう。 groupsテーブル id name 1 A 2 B 3 C usersテーブル id group_id name 1 1 一郎 2 2 二郎 3 3 三郎 4 2 四郎 UserモデルとGroupモデルは一対一の関係で成り立っています。 app/models/group.rb class Group < ApplicationRecord has_many :users end app/models/user.rb class User < ApplicationRecord belongs_to :group end では全てのグループに所属する人を表示するプログラムを書いてみます。 Group.all.each do |group| user_names = comp.users.pluck(:name).join(",") p "#{group.name}: #{user_names}" end 上記のコードでは、まずGroup.allでgroupsテーブルから全件レコードをとってきて、その結果に対しそれぞれ紐づけられたusersの情報を表示しています。結果は以下のようになります。 Group Load (0.3ms) SELECT `groups`.* FROM `groups` (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`group_id` = 1 "A: 一郎" (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`group_id` = 2 "B: 二郎、四郎" (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`group_id` = 3 "C: 三郎" まず一回目のSQLでgroupsテーブルの全ての情報を問い合わせ、その後各グループごとに所属ユーザーの問い合わせをしていることがわかります。n回ループをした場合、N+1回の問い合わせが生じてしまうため、N+1問題と言われています。 SQLを見れば非効率的なDB問い合わせをしているのがわかるのですが、このとおりRailsではSQLを意識することなくコードがかけてしまうため、知らず知らずのうちにこういった問題が起こります。今回の例ではデータが少ないためそこまでパフォーマンスに問題が出るわけでないですが、データ量が大きくなってくると無視できないレベルまでパフォーマンスが悪化してしまいます。特にRailsを使うようなWebアプリケーションではミリ単位のパフォーマンスが重視されるため、気をつけて実装する必要があります。 解決方法 先ほどのuserとgroupの例で言うと、Group.allでgroupの情報をとってくる段階でuserの情報を取って来れれば問題は解決しそうですね。そのためにはincludes()と言うメソッドを使います。 Group.all.includes(:users).each do |group| user_names = comp.users.pluck(:name).join(",") p "#{group.name}: #{user_names}" end SELECT `group`.* FROM `groups` SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' IN (1,2,3,4,5) 今度はSQLが二つだけで済みました。 N+1問題を検知するgem「bullet」 今回のように単純な構造のRDSなら自分でもログを見ていれば気がつけますが、複雑なシステム伴ってくると見落とす可能性もありますし、解決するのに時間がかかる可能性があります。 そこでN+1問題を自動で検知して解決方法を自動で提案してくれるgem「bullet」を使うことによって開発を効率化することができます。 Gemfile group :development do gem 'bullet' end Gemfileの開発環境のグループにbelletを記述しbundle installします。 次にbundle exec rails g bullet:installをターミナルで走らせると自動でconfig/environments/development.rbに詳細な設定が追記されます。 config/environments/development.rb config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.console = true # Bullet.growl = true Bullet.rails_logger = true Bullet.add_footer = true end それぞれの項目については公式のdocumentを読んで適宜調整してください。 これで準備完了です。これでアプリを走らせれば、N+1問題が発生した時に自動でアラートで知らせてくれるようになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む