20211013のRubyに関する記事は7件です。

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で続きを読む

exists? メソッドを使った簡単な条件分岐

初めてexistsメソッドを使ったのでメモ existsメソッドとは exists?メソッドとは指定した条件のレコードがデータベースに存在するかどうかを真偽値で返すメソッドです。存在すればtrueを存在しなければfalseを返します。 書き方は、 オブジェクト.exists?(条件)     モデル名や、モデルクラスのインスタンスなどを指定できます。 何故使おうと思ったか 結論から言うとコメントの表示で気になることがあったため。 写真投稿サイトをつくっており、投稿に対するコメントを追加できるようにしています。 コメントが多くなることを想定しているのでページネーションを用いています(gem 'kaminari') コメントは @comments = Commnet.all で取得してそれをviewで対応するidだけを表示するようにしています。 ページネーションを作っていなければ、今回の問題に当たっていなかったので作っていてよかったと思いました。 問題としましては投稿にコメントが追加されていないのにページネーションが表示されている点です。 これを投稿にコメントが追加されてから表示するようにしたいので考えました。 試したこと viewでは以下のようにしていますがこの"each"の中にページネーションの記載をするとコメントの数だけページネーションが表示されてしまいます。 それではいけないのですが、指定する条件はこれが理想なんですよね〜、 <% @comments.each do |comment| %> <% if @post.id == comment.post_id %> ~ #コメントにある外部キーと投稿のidが同じものだけ表示している <% end %> どうしたもんか。。 ググってみました。 どう検索したらいいかわかんなかったのでデータベースの有無に関するメソッドを見てました。 (present? とか blank? とか) それみてたら existsを見つけました! このメソッドは条件に対して true か falseを返すので、 User.exists?(name: "田中") Userのnameカラムに田中があればtrue, なければfalse => true みたいに使うことができます。 僕が考えている条件としては、 コメントをつける投稿の主キー id と コメントが持つ、外部キー post_idが同じ というものです。 つまり,,,,,, Comment.allの情報を入れた @comments の post_idカラムのなかに コメントをつける投稿のidがあるかないかの処理を書けばいいのです。 post_idはcommentをcreateした時に追加されるのでコメントがなければpost_idは追加されません。 書いてみる <div> <% if @comments.exists?(post_id: "#{@post.id}") %> #commentsのpost_idを全検索して投稿ごとのidを調べてtrueかfalseを返す ##trueなら表示 <%= paginate @comments %> <% else %> ##falseなら表示 <p>コメントはまだありません。</p> <% end %> </div> これで他の投稿と競合せずに1投稿ごとに情報の有無で表示を変えることができました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rbenvと環境構築

rbenvの個人的な疑問点を纏めたり、環境構築で引っ掛かったところをまとめた備忘録です。 rbenvを使う理由 互換性の少ないrubyは、プロジェクトごとに色んなrubyのバージョンを使う。(常に最新版を使う訳ではないという事) 全部のrubyのバージョンをpcに入れて運用するのは大変。 そこでrbenvを使う。 コマンド解説 rbenv local 3.0.2  #特定のディレクトリでこれを行うとそのディレクトリのみバージョンを指定できる 逆にglobalはPC全体のRubyのバージョンを指定。 rbenv exec gem install bundler bundleコマンドはgemコマンドでインストールします。 "rbenv exec" は「システムrubyのgem」ではなく「rbenvで現在有効なrubyのgem」であることを明記する書き方。 つまり、rvenv execをつけると、rbenvのrubyバージョンに合わせたgemをインストールできる。 rbenv rehash # これでパスがshimsの方にコピーされ使えるようになる https://mogulla3.tech/articles/2020-12-29-01より抜粋 rbenv rehash コマンドを実行すると、大まかには ~/.rbenv/versions/*/bin/ 以下のファイルを ~/.rbenv/shims/ 以下にコピーする。 なぜコピーする必要があるかというと、通常PATHが通っているのは ~/.rbenv/shims 以下となっているからで、ここにコピーしないと rspec とか rubocop といったGemが提供するコマンドを実行できないからである。 これでちゃんと使えるようになったーと思ってたら・・・ PC再起動後にまたPATHが初期値になってたし、ディレクトリのrubyが変更前にになってた。 which gemで表示されるのがusr/binに・・・ bundle installしても、Gemfileに書いてるversionと、ローカルに適用されてるrubyのversion違うよと怒られた。 rbenv local 3.0.2 やってもruby -vの結果かわらず・・・ source ~/.bash_profile コマンド打ったら治った なぜか? sourceコマンドは、ファイルに書かれたコマンドを現在のシェルで実行するコマンド。 環境構築方法を説明した多くの記事はbash(.bash_profile等)にパス設定する手順が書かれています。 ですが、MacOS Catalinaからはデフォルトのログインシェルが変更されており、ログイン時に読み込まれるのはzshの設定ファイルです。 $ echo $SHELL  #シェル何使ってるか確認するコマンド これで出てくる結果がbashじゃないと、シェル起動する度に毎回sourceコマンド打ち込まないといけない。 MacOSはある時を境にbash⇨zshにデフォルトのシェルが変わったらしい。 zshのままでもsourceコマンド毎回打たなくていいようにできるらしいけど、とりあえずはbashの方が今後やりやすそうなんでそっちに変更します。 参考 https://blog.officekoma.co.jp/2020/05/macos-catalinabash.html?m=1 rbenvによるRubyのインストールからHello, World!まで - Qiita
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

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

①.比較演算子(>, >=, <, <=, ==) 比較演算子は、値同士を比較したいときに使用します! 書き方はシンプルで、値と値の間に演算子を記述するだけです! 以下のようになっています! 比較演算子 使い方 説明 > 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で続きを読む

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で続きを読む