20210604のRailsに関する記事は14件です。

壊れていたRailsが直ったから、メモを書く

DockerでRailsの環境構築をしようとして、色々環境をいじったらRails s が動かなくなった。 それから3日間こねくり回して、ようやく解決した。 nokogiriエラー、 msgpackのエラー、 色々とあったが、Gemfileを壊してしまった。 要はバージョンエラー。 railsのバージョン、ruby、環境変数、パスなどのバランスが崩れた。 確か、rbenvを追加したところで、おかしくなった。 railsもrubyもアンインストールをして、環境パスも消して、 それで全部、インストールをして再構築できた。 dockerにはまた挑戦したい!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル(第6版) 第11章 アカウントの有効化

第11章 現時点のアプリケーションでは、新規登録したユーザーが最初からすべての機能にアクセスできるようになっている。 今回は、アカウント有効化を差し込み、メールアドレスが持ち主かどうかを確認する。 流れ ①有効化トークンやダイジェストに関連付けた状態にする ②有効化トークンを含めたリンクをユーザーにメール送信 ③ユーザーがそのリンクをクリックすると有効化できるようにする アカウント有効化のステップ 1.ユーザーの初期状態は「有効化されていない」(unactivated)にしておく。 2.ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。 3.有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく3 。 4.ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。 5.ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated)に変更する。 以前やったパスワードや記憶トークンの仕組みと似ている。 検索キー string digest authentication email password password_digest authenticate(password) id remember_token remember_digest authenticated?(:remember, token) email activation_token activation_digest authenticated?(:activation, token) email reset_token reset_digest authenticated?(:reset, token) 参照:railsチュートリアル AccountActivationsリソース 第8章でやったセッション機能を使い、アカウントの有効化作業を「リソース」としてモデル化する。(Active Recordのモデルとは関係ないため、両者を関連付けしない。代わりに、必要なデータをUserモデルに追加する) 有効化用のリンクにアクセスし有効化のステータスを変更するなら、RESTに準拠すればPATCHリクエストとupdateアクションになるべき。 しかし、有効化リンクはめーづで送られ、リンクをクリック= =ブラウザでクリックした時と同じであり、この場合ブラウザから発行されるのはGETリクエストになる。 なので、ユーザーからのGETリクエストを受けるため、updateのところeditアクションに変更する。 AccountActivationsコントローラ AccountActivationsコントローラーを生成する $ rails generate controller AccountActivations editアクションへの名前付きルートを扱うため、ルーティングにアカウント有効化用のresources行を追加する。 config/routes.rb resources :account_activations, only: [:edit] HTTPリクエスト URL Action 名前付きルート GET /account_activation/トークン/edit edit edit_account_activation_url(token) 参照:railsチュートリアル これで名前付きルートの edit_account_activation_url(activation_token, ...) が使えるようになった。 AccountActivationのデータモデル 有効化のメールには一意の有効化トークンが必要。 一瞬、送信メールと同じ文字列をデータベースに保管しておけばよくねと思うが、万が一データベースの内容が漏れた時大変なことになる。 新規登録されたユーザーアカウントのトークンを盗み、不正にそのトークンを使われてしまう危険性がある。 なので、仮想的な属性を使ってハッシュ化した文字列をデータベースに保存し、次のように仮想属性の有効化トークンにアクセス user.activation_token 下のようなコードでユーザー認証できるようになる。 user.authenticated?(:activation, token) 参照:railsチュートリアル 上の画像のようにデータモデルを追加するので、マイグレーションを実行する。 $ rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime admin属性と同様にactivated属性のデフォルト論理値をfalseにする。 db/migrate/[timestamp]_add_activation_to_users.rb add_column :users, :activated, :boolean, default: false あとはマイグレートする $ rails db:migrate Activationトークンのコールバック 以前メールアドレスを保存する前に、メールアドレスを小文字に変換するとかでbefore_saveコールバックを使った。before_saveは、オブジェクトが保存される直前、オブジェクト作成時や更新時にコールバックが呼び出される。 今回は、オブジェクトが生成された時だけ、コールバックを呼び出したいのでbefore_createコールバックが必要になる。 before_create :create_activation_digest このコードはメソッド参照と呼ばれ、Railsはcreate_activation_digestメソッドを探して、ユーザー作成前に実行する。 create_activation_digestメソッドはUserモデル内でしか使わないので、privateキーワードを指定する。 private def create_activation_digest # 有効化トークンとダイジェストを作成および代入する end before_createコールバックの目的は、トークンとそれに対応するダイジェストを割り当てるため。割り当ては下のように self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) 記憶トークンや記憶ダイジェストのために作ったメソッドを使いまわしている。 ここでrememberメソッドと見比べる # 永続セッションのためにユーザーをデータベースに記憶する def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end update_attributeの使い方に違いがあるが、記憶トークンやダイジェストは既にデータベースにいるユーザーのために作成されるのに対し、before_createコールバックはユーザーが作成される前に呼び出される。 User.newで新しいユーザーが定義されると、activation_token属性やactivation_digest属性が得られる。 後者のactivation_token属性は既にデータベースとの関連付けが出来上がっているため、ユーザーが保存される時に同時に保存される。 Userモデルに実装するとこんな感じ app/models/user.rb attr_accessor :remember_token, :activation_token before_save :downcase_email before_create :create_activation_digest private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end サンプルユーザーの生成とテスト サンプルデータとfixtureを更新する。 サンプルとユーザーを事前に有効化しておく。 db/seeds.rb # メインのサンプルユーザーを1人作成する User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true, activated: true, activated_at: Time.zone.now) # 追加のユーザーをまとめて生成する 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password, activated: true, activated_at: Time.zone.now) test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true activated: true activated_at: <%= Time.zone.now %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% end %> データベースを初期化&サンプルデータを再度生成し、変更を反映する。 $ rails db:migrate:reset $ rails db:seed アカウント有効化のメール送信 Action Mailerライブラリを使って、Userのメイラーを追加する。 これをUsersコントローラのcreateアクションで有効化リンクをメール送信するために使う。 メイラー構成は、コントローラのアクションと似てて、メールテンプレートをビューと同じ要領で定義できる。ここに、有効化トークンとメールアドレスのリンクを含める。 送信メールのテンプレート メイラーは、rails generateで生成する。 $ rails generate mailer UserMailer account_activation password_reset これで、account_activationメソッドとpassword_resetメソッドの原型ができた。 生成したメイラーごとに、ビューのテンプレートは2つずつ生成され、1つはテキスト用メールのテンプレート、2つ目はHTMLメール用のテンプレートになる。 テキストビュー app/views/user_mailer/account_activation.text.erb UserMailer#account_activation <%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb app/views/user_mailer/account_activation.html.erb <h1>UserMailer#account_activation</h1> <p> <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb </p> 生成されたメイラーの動作を解説する。 app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout 'mailer' end デフォルトのfromアドレスがあり、メールのフォーマットに対応するメイラーレイアウトが使われている。 app/mailers/user_mailer.rb class UserMailer < ApplicationMailer def account_activation @greeting = "Hi" mail to: "to@example.org" end def password_reset @greeting = "Hi" mail to: "to@example.org" end end @greetingというインスタンス変数があり、ビューでコントローラのインスタンス変数を利用できるのと同じで、メイラービューで利用できる。 fromアドレスのデフォルト値を更新する。 default from: "from@example.com" から default from: "noreply@example.com" にする。 アカウント有効化リンクをメール送信する。 app/mailers/user_mailer.rb def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end この時点でaccount_activationに引数を与えたので、テストはパスしない。 メールには、挨拶文にユーザー名とカスタムの有効化リンクを追加する。 流れとしては、Railsサーバーでユーザーをメールアドレス検索し、有効化トークンを認証できるようにするので、リンクにメールアドレスとトークンを両方含める。 edit_user_url(user) は https://www.example.com/users/1/edit となる。(urlは絶対パス) これに対応するアカウント有効化リンクのURLはこんな感じになる。 https://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit ハッシュの部分はnew_tokenメソッドで生成されたもので、Base64でエンコードされている。 ハッシュの部分は、/users/1/editの「1」のユーザーIDと同じ役割になる。 このトークンは、AccountActivationsコントローラのeditアクションでは、paramsハッシュでparams[:id]として参照可能。 クエリパラメータ クエリパラメータを使用し、URLにメールアドレスを組み込める。 クエリパラメータは、URLの末尾に疑問符「?」に続けてキーと値のペアを記述する。 account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com メールアドレスの「@」記号はURLにおいて「%40」となる。 これはエスケープという手法で、URLで扱えない文字を扱えるようにするためのもの。 クエリパラメータを設定するのに、名前付きルートに対して次のハッシュを追加する。 edit_account_activation_url(@user.activation_token, email: @user.email) 名前付きルートでクエリパラメータを定義すれば、Railsが特殊文字を自動でエスケープする。 コントローラーでparams[:email]とすれば、自動的にエスケープを解除する。 (頭がいい) アカウント有効化のテキストビューとHTMLビュー テキストビュー↓ app/views/user_mailer/account_activation.text.erb Hi <%= @user.name %>, Welcome to the Sample App! Click on the link below to activate your account: <%= edit_account_activation_url(@user.activation_token, email: @user.email) %> HTMLビュー↓ app/views/user_mailer/account_activation.html.erb <h1>Sample App</h1> <p>Hi <%= @user.name %>,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %> 送信メールのプレビュー テンプレートを作ったので、メールプレビューをやってみる。 これは、メールを実際に送信しなくても、その場でプレビューすることができる。 config/environments/development.rb config.action_mailer.raise_delivery_errors = false host = 'example.com' # ここをコピペすると失敗します。自分の環境のホストに変えてください。 # クラウドIDEの場合は以下をお使いください config.action_mailer.default_url_options = { host: host, protocol: 'https' } # localhostで開発している場合は以下をお使いください # config.action_mailer.default_url_options = { host: host, protocol: 'http' } 今回はcloud9なので、ホスト名は自分のcloud9のURLにすればOK 続いて、Userメイラープレビューファイルの更新が必要。 account_activationの引数に有効なUserオブジェクトを渡すため、user変数が開発用データベースの最初のユーザーになるよう定義する。 それをUserMailer.account_activationの引数として渡す。 test/mailers/previews/user_mailer_preview.rb class UserMailerPreview < ActionMailer::Preview def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end end このときに、user.activation_tokenの値にも代入している。理由としてテンプレートでは、アカウント有効化のトークンが必要なので、代入している。また、activation_tokenは仮の属性なので、データベースのユーザーはこの値を持っていない。 プレビューがみれた。 プレビューを見る際には、URLの末尾に「/rails/mailers/user_mailer/account_activation」を付ければOK 送信メールのテスト メールプレビューのテストを作成し、プレビューをダブルチェックする。 Rialsによってテスト例が自動生成されてるので、これを利用する。 test/mailers/user_mailer_test.rb require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do mail = UserMailer.account_activation assert_equal "Account activation", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end test "password_reset" do mail = UserMailer.password_reset assert_equal "Password reset", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end end だが、この時点でテストは失敗する。 テストでは、assert_matchという強力なメソッドが使われている。 下の方法だと正規表現で文字列をテストできる。 assert_match 'foo', 'foobar' # true assert_match 'baz', 'foobar' # false assert_match /\w+/, 'foobar' # true assert_match /\w+/, '$#!*+@' # false test/mailers/user_mailer_test.rb require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end end 上のテストでは、assert_matchメソッドで、名前、有効化トークン、エスケープ済メールアドレスがメール本文にあるかみている。 CGI.escape(user.email) というのを使えばテストユーザーのメールアドレスをエスケープできる。 テストをパスさせるため、ドメイン名を正しく設定する。 config/environments/test.rb config.action_mailer.default_url_options = { host: 'example.com' } これでテストは成功する。 ユーザーのcreateアクションを更新 createアクションにユーザー登録を行うコードを数行追加すれば、メイラーをアプリケーションで使える。 app/controllers/users_controller.rb class UsersController < ApplicationController def create @user = User.new(user_params) if @user.save UserMailer.account_activation(@user).deliver_now flash[:info] = "Please check your email to activate your account." redirect_to root_url  else render 'new' end end end 変更前は、ユーザーのプロフィールページにリダイレクトしてたが、今はリダイレクト先をルートURLに変更している redirect_to @userからredirect_to root_urlに変更。 リダイレクト先の変更とユーザーは以前のようにログインしないようになったので 、アプリケーションは正しく動作してもテストは失敗する。 一旦テストはコメントアウトする。 test/integration/users_signup_test.rb # assert_template 'users/show' # assert is_logged_in? 実際にサインアップでユーザーを登録して、サーバーログを確認すればメールの例が出てくる。 登録後リダイレクトされ、ホームページににアカウント有効化の確認メッセ―ジが出ている。 アカウントを有効化する やること ・AccountActivationsコントローラのeditアクションを書く。 ・アクションへのテストを書く ・テスト確認後、AccountActivationsコントローラからUserモデルにコードを移す(リファクタリング) authenticated?メソッドの抽象化 有効化トークンとメールはそれぞれ ・params[:id] ・params[:email] で参照できる。 ユーザーを検索して認証は以下のコードだった。 user = User.find_by(email: params[:email]) if user && user.authenticated?(:activation, params[:id]) authenticated?メソッドは、アカウント有効化のダイジェストと、渡されたトークンが一致するかどうかをチェックしている。 しかし、これは記憶トークン用なので、今は動作しない。 # トークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end Userモデル属性のremember_digestは、self.remember_digestに書き換えられる。 rememberの箇所を変数にして扱いたい。 理由として、状況に応じて呼び出すメソッドを切り替えたいから。 self.FOOBAR_digestのように これを実装するには、受け取ったパラメータに応じて呼び出すメソッドを切り替える方法を取る。 これは、「メタプログラミング」と呼ばれている。これは、プログラムでプログラムを作成するということ。 ここで重要なことは、sendメソッドの機能。 これは、渡されたオブジェクトに対しメッセージを送ることで、呼び出すメソッドを動的に決めることが可能。 例↓ $ rails console >> a = [1, 2, 3] >> a.length => 3 >> a.send(:length) => 3 >> a.send("length") => 3 sendを通して渡したシンボル(:length)や文字列("length)は、どちらもlengthメソッドと同じ結果になった。 どちらもオブジェクトにlengthメソッドを渡しているので、等価になる。 他の例↓ >> user = User.first >> user.activation_digest => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> user.send(:activation_digest) => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> user.send("activation_digest") => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> attribute = :activation >> user.send("#{attribute}_digest") => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" ①シンボル:activationと等しいattribute変数を定義 ②文字列の式展開を使い、引数を組み立ててsendに渡す。 このステップでも同じ。 また、Rubyではシンボルの方が一般的に使われる。 なお、シンボルと文字列どちらでも上のようなコードは文字列に変換される。 "activation_digest" →シンボルは式展開されると文字列になる。 authenticated?メソッドを書き換える。 def authenticated?(remember_token) digest = self.send("remember_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(remember_token) end ①各引数を一般化 ②文字列の式展開も利用 結果↓ def authenticated?(attribute, token) digest = self.send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end 2番目の引数tokenの名前を一般化した。 また、このコードはモデル内の為selfは省略可能 結果↓ def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end これで、呼び出す時は、下のように呼び出せば従来の振る舞いができる。 user.authenticated?(:remember, remember_token) Userモデルに実装するとこんな感じ app/models/user.rb class User < ApplicationRecord . . . # トークンがダイジェストと一致したらtrueを返す def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end . . . end この時点ではテストが失敗する。 理由は、current_userメソッドとnilダイジェストのテストの両方において、authenticated?が古いまま。 現時点で、引数は2つではなく1つの状態になっている。 current_user内のauthenticated?メソッドを抽象化 app/helpers/sessions_helper.rb # 現在ログイン中のユーザーを返す(いる場合) def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user && user.authenticated?(:remember, cookies[:remember_token]) log_in user @current_user = user end end end Userテスト内のauthenticated?メソッドを抽象化 test/models/user_test.rb test "authenticated? should return false for a user with nil digest" do assert_not @user.authenticated?(:remember, '') end これでテストは成功する。 editアクションで有効化 authenticated?メソッドを抽象化できたので、これでやっとこさeditアクションを書ける。 editアクションの処理 ・paramsハッシュで渡されたメールアドレスに対応するユーザーを認証する。 ユーザーが有効化どうかの確認は↓ if user && !user.activated? && user.authenticated?(:activation, params[:id]) !user.activated?に関して、論理値を追加している。これは、既に有効になっているユーザーを再度有効化しないために必要な物。 もしこのコードがなければ、攻撃者がユーザーの有効化リンクを後から盗み出してクリックするだけで、本当のユーザーとしてログインできてしまう。 上の論理値に基づきユーザー認証を行うには、ユーザー認証後にactivated_atタイムスタンプを更新する。 user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) このコードをeditアクションに入れる。 app/controllers/account_activations_controller.rb class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end もし仮にトークンが無効だった場合には、ルートURLにリダイレクトされる。 しかしこの時点では、ユーザーのログイン方法を変更してないので、ユーザー有効化には意味がない。 ユーザーが有効である場合にのみログインできるようにする。 これを実装するには、user.activated?がtrueの場合にのみログインを許可、そうでなければルートURLにリダイレクトさせ警告を表示する。 app/controllers/sessions_controller.rb def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) if user.activated? log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else message = "Account not activated. " message += "Check your email for the activation link." flash[:warning] = message redirect_to root_url end else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end 登録した際のメール(サーバーログ)に <a href="https://fc20e7ef873e419a9249c4e46d05a152.vfs.cloud9.us-east-2.amazonaws.com/account_activations/eQyFGNJ9HGuy0Z3GYnNC7g/edit?email=4%40example.com">Activate</a> URLに有効化トークンがある。 上記のURLを開いた結果 改良点として、有効化されていないユーザーが表示されないようにする必要がある。これは後で行う。 演習 > user = User.find(108) User Load (16.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 108], ["LIMIT", 1]] => #<User id: 108, name: "aaa", email: "4@example.com", created_at: "2021-06-03 13:23:58", updated_at: "2021-06-03 13:24:07", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: "$2a$12$XZt0LzqR3yWf5Ce3OzymFe/dyvKfIdvwAx5qW5BQjRk...", activated: true, activated_at: "2021-06-03 13:24:07"> > user.activated => true 有効化のテストとリファクタリング アカウント有効化の統合テストを追加する。 第7章で書いたテストに若干の手を加える。 test/integration/users_signup_test.rb require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear end test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' assert_select 'div#error_explanation' assert_select 'div.field_with_errors' end test "valid signup information with account activation" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end assert_equal 1, ActionMailer::Base.deliveries.size user = assigns(:user) assert_not user.activated? # 有効化していない状態でログインしてみる log_in_as(user) assert_not is_logged_in? # 有効化トークンが不正な場合 get edit_account_activation_path("invalid token", email: user.email) assert_not is_logged_in? # トークンは正しいがメールアドレスが無効な場合 get edit_account_activation_path(user.activation_token, email: 'wrong') assert_not is_logged_in? # 有効化トークンが正しい場合 get edit_account_activation_path(user.activation_token, email: user.email) assert user.reload.activated? follow_redirect! assert_template 'users/show' assert is_logged_in? end end テストは長いが今までやってきたことがほとんど。 また、重要な部分は下の一行 assert_equal 1, ActionMailer::Base.deliveries.size これは配信されたメッセージが1つかどうかを確認している。 配列deliberiesは変数の為、setupメソッドでこれを初期化しておかないと、他でもテストでメール配信された時にエラーが発生する。 assignsメソッドというものがある。 これは、assignsメソッドに対応するアクション内のインスタンス変数にアクセスできるようになる。 例えば、Usersコントローラーのcreateアクションでは@userというインスタンス変数が定義されており、テストでassigns(:user)と書けば、インスタンス変数にアクセスできるようになる。 この時点でテストはパスするので、ユーザー操作の一部をコントローラからモデルに移動するリファクタリングを行う。 手順 ・activateメソッドを作成、ユーザーの有効化属性を更新する ・send_activation_emailメソッドを作成、有効化メールを送信する。 Userモデルにユーザー有効化メソッドを追加する。 app/models/user.rb # アカウントを有効にする def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now end user.という記法を使っていないが、Userモデルにそのような変数はない。これがあるとエラーになる。 また、userをselfに切り替えるという方法もあるが、selfはモデル内では必須ではない。そして、Userメイラー内の呼び出しでは、@userがselfに変更されている。 -user.update_attribute(:activated, true) -user.update_attribute(:activated_at, Time.zone.now) +update_attribute(:activated, true) +update_attribute(:activated_at, Time.zone.now) ユーザーモデルオブジェクトからメールを送信する。 app/controllers/users_controller.rb def create @user = User.new(user_params) if @user.save @user.send_activation_email flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end ユーザーモデルオブジェクト経由でアカウントを有効化する app/controllers/account_activations_controller.rb def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.activate log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end 演習 activateメソッドでupdate_attributeを2回呼び出している。これは各行で1回ずつデータベースに問い合わせしているということになる。 これをupdate_columsに変更する。これは呼び出しを1回にできる。 # アカウントを有効にする def activate update_columns(activated: true, activated_at: Time.zone.now) end 現在/usersのユーザーindexページを開くとすべてのユーザーが表示される。なので、有効でないユーザーは非表示にする。 app/controllers/users_controller.rb def index @users = User.where(activated: true).paginate(page: params[:page]) # 演習で変更 # @users = User.paginate(page: params[:page]) end def show @user = User.find(params[:id]) redirect_to root_url and return unless @user.activated? end ここまでの演習課題で変更したコードをテストするために、/usersと/users/:idの両方に対する統合テストを作成 users.yml non_activated: name: Non Activated email: non_activated@example.gov password_digest: <%= User.digest('password') %> activated: false activated_at: <%= Time.zone.now %> users_controller_test.rb set @non_activated_user =users(:non_activated) end test "should not allow the not activated attribute" do log_in_as (@non_activated_user) assert_not @non_activated_user.activated? get users_path assert_select "a[href=?]", user_path(@non_activated_user), count: 0 get user_path(@non_activated_user) assert_redirected_to root_url end 本番環境でのメール送信 production環境で実際にメールを送信できるようにする。 手順 ・まず、無料のサービスを利用してメール送信の設定を行う ・アプリケーションの設定とデプロイを行う 本番環境からメールを送信するには、「Mailgun」というHerokuアドオンを利用しアカウント検証をする。 (Herokuのstarterというプランだとメール数が最大400通までOKらしい) Mailgunアドオンを使うには、production環境のSMTPに情報を記入する。 本番Webサイトのアドレスをhost変数に定義する。 config/environments/production.rb Rails.application.configure do . . . config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :smtp host = '<あなたのHerokuサブドメイン名>.herokuapp.com' config.action_mailer.default_url_options = { host: host } ActionMailer::Base.smtp_settings = { :port => ENV['MAILGUN_SMTP_PORT'], :address => ENV['MAILGUN_SMTP_SERVER'], :user_name => ENV['MAILGUN_SMTP_LOGIN'], :password => ENV['MAILGUN_SMTP_PASSWORD'], :domain => host, :authentication => :plain, } . . . end Gitのトピックブランチをmasterにマージ $ rails test $ git add -A $ git commit -m "Add account activation" $ git checkout master $ git merge account-activation リモートリポジトリにプッシュ&Herokuにデプロイ $ rails test $ git push $ git push heroku $ heroku run rails db:migrate MailgunのHerokuアドオン追加コマンド $ heroku addons:create mailgun:starter Herokuの環境変数を表示したい場合には、下のコマンドを実行する。 $ heroku config:get MAILGUN_SMTP_LOGIN $ heroku config:get MAILGUN_SMTP_PASSWORD 注意事項 user_nameやpasswordといった情報は、絶対にソースコードに直接書かない事! そのような情報は環境変数「ENV」に設定し、アプリケーションに読み込ませるようにする。 今回は、そのような変数はMailgunのアドオンが自動的に設定しているが、第13章では環境変数を自分で設定することになる。 受信メールの認証コマンド。MailgunダッシュボードのURLが表示されるのでブラウザを開く。 $ heroku addons:open mailgun サインアップからアカウント作成を押してメール確認したら、、、 アカウントが有効になった。 最後に 本章はアカウント有効化を実装した。 WEBアプリケーションで大切な機能なので、他でも使えるように覚えていきたい。 第12章ではパスワード再設定機能を追加する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MacのターミナルとVSCodeのターミナルで動作が違っている現象を解決する

開発環境 macOS Catalina Rails 6.1.3.2 Ruby 2.6.7 VSCodeのターミナルがうまく動作しない 一から調べてRailsの環境構築を終えて、Macのターミナルからrails sを実行すると成功。 開発していく上で、VSCodeのターミナル(Cmd+Jで起動)を使った方が便利と思い、そちらでrails s。 すると... % rails s Rails is not currently installed on this system. To get the latest version, simply type: $sudo gem install rails You can then rerun your "rails" command. Macのターミナルだとちゃんと動いているのに、VSCodeのターミナルからだと動かない... 解決するためにやったこと1 VSCodeの設定を変える VSCodeの設定を開き、設定の検索欄に「terminal.integrated.inheritEnv」と入力し表示されたチェックボックスのチェックを外す。 VSCodeを再起動する。 解決するためにやったこと2 パスの記述ファイルを変更する VSCodeのターミナル上でwhich rubyを実行すると/usr/bin/rubyとなり、環境構築で行ってきた.rbenv下ではなくなっている。(Macのターミナルからは元々.rbenv下となっていたが、PCの再起動などをしているうちにいつの間にかこちらも/usr/bin/rubyとなっていた...) パス通したはずなのにおかしいなと思いながら調べていると、macOS Catalinaからは標準のシェルがzshになったそう。(参考にした記事はこちら) 環境構築で参考にしていた記事では、.bash_profileにパスを記述しており同じくそうしていました... そこで % vi ~/.zprofile export PATH=~/.rbenv/bin:$PATH eval "$(rbenv init -)" PATH=/usr/local/bin/git:$PATH export PATH とパスを.zprofileに記述。(下2行はGitのパスです。) .bash_profileに記述していたものは全て消しました。 そして変更を反映。 % source ~/.bash_profile % source ~/.zprofile 再度、VSCodeからrails sを実行... できた!!!安心するいつもの画面! (RailsにMySQLを使用しており、rails sした時にMySQLを起動できないよ!と言われて解決するまでに参考にさせていただいた記事を下記に貼っておきます。こちらで無事に起動することができました。) mysqlが起動できない(Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)) mysql 起動時のThe server quit without updating PID file エラーの回避法 まとめ しかるべきファイルにパスを通せていなかったことが原因でした。 開発環境をしっかりとチェックして、記事を参考にすることが大事だなと改めて感じました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vagrant】vagrantをコピーして2つ同時に立ち上げたい

環境 MacOS VirtualBox Vagrant Problem vagrantの環境を作成したプロジェクトファイル丸ごとコピーして同時に立ち上げたい! という場面がどれだけあるのか分かりませんが、私は今回その場面に直面しましたので残しておきます。 具体的にはRailsのプロジェクトをコピーして、片方をアプリケーション用。片方をRDS用として相互通信させたかった。 プロジェクトフォルダをコピーして「vagrant up」するとオリジナルと同じVMで立ち上がってしまいます。 これでは共存(同時に立ち上げ)出来ません。 Solve 同一人物とされているのならコピー先をユニークにしてあげましょう。 PRIVATE_IP = '192.168.33.10' # オリジナルとは別のIPにしましょう Vagrant.configure(2) do |config| config.vm.define "I_am_copy" # 名前を定義してあげることでユニークとなる end あとはそれぞれ立ち上げてあげると同時に2つのvmが立ち上がると思います。 vagrant up まとめ やりたいことによって様々な最適解があると思います。(オリジナルにdefine2つ書いて立ち上げるとか) ぶっちゃけRDS用はオリジナルをパッケージ化して新たにinitしたほうがスッキリした気がする。 どうせVagrantfileとかコピーするし、手間は少ないからまぁいいかな。。 サクッと同時に立ち上げたいときはオススメです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローカル環境で環境変数を使ったBasic認証が通らない件

タイトルの通りである。application_controller.rbにユーザー名とパスワードを直接設定してた時は問題なく動作したんだが、.zshrcの中で環境変数を利用して設定した途端に動作しなくなった。クッキーやキャッシュを削除したりブラウザを変えたり訳もなくファイルの中身を編集したりしたんだが決してうまくいかなかった... 結論 こちらの方の記事に全てが書いてありました。もう自分の方で書くことは何もありません...(笑) 前提条件 macOS Catalina ( 10.15.7 ) Visual Studio Code.app( 1.56.2 ) Terminal.app ( 2.10 ) Rails ( 5.2.6 ) ※執筆時のバージョン 本題 要は、vim ~/.zshrcでファイルを編集した後に、source ~/.zshrcでリロードして定義した環境変数を使えるようにするのだが、rails sでサーバーを起動するターミナルで.zshrcが更新されてなけりゃ結局のところそれらは無いに等しい。サーバー起動用のターミナルが既に存在しているという事は、言い換えればログインシェルである『zsh』が既に一度実行された事を意味する。そのため編集したファイルに環境変数が追加されているのなら、それらを反映させる必要があるのでsourceコマンドを使ってもう一度.zshrcを実行する必要があるのである。 参考記事 Basic認証導入の際に起きた症状 - Qiita
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsのerbファイルでRailsヘルパーにjavascript変数を使う方法。

何をしたいのか <script> Javascriptの変数 = "テスト" $('.image-upload-box').append(`<%= f_img.hidden_field :image, value: Javascriptの変数 %>`); </script> というふうに使いたかったのですが これではエラーが発生してしまい使用できません。 解決方法 <script> Javascriptの変数 = "テスト" $('.image-upload-box').append(`<%= f_img.hidden_field :image, value: '${Javascriptの変数}' %>`); </script> このようにしたらvalueにjavascriptの変数を使うことができました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WSL2でDockerを使ってRails 6 + MySQL 8の環境構築をしよう

下準備 WSLをインストールする WSLのバージョンを2にアップグレードする Microsoft StoreからDebian系OSをダウンロードし、インストールする Docker Desktopのインストーラをダウンロードし、それを使ってDocker Desktopをインストールする 下準備が終わったら 下準備が終わったらディストリビューションを開きます 1. myappディレクトリを作成して、そこに移動する cd ~ mkdir myapp cd myapp 2. エディタとDockerをインストールする 好みのエディタを使いましょう sudo apt update sudo apt install -y nano docker-compose 3. Dockerfileを作成する まず、Dockerfileを開きます sudo nano Dockerfile 以下のコードをコピペして保存し、終了します FROM ruby:3.0.0 RUN apt-get update && apt-get install -y curl apt-transport-https wget && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y yarn RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - && \ apt-get install -y nodejs RUN mkdir /myapp WORKDIR /myapp COPY Gemfile /myapp/Gemfile COPY Gemfile.lock /myapp/Gemfile.lock RUN bundle install COPY . /myapp # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] 4. GemfileとGemfile.lockを作成する まず、Gemfileを開きます sudo nano Gemfile 以下のコードをコピペして保存し、終了します source 'https://rubygems.org' gem 'rails' gem 'mysql2' 次に、空のGemfile.lockを作成します touch Gemfile.lock 5. entrypoint.shを作成する プロセスを管理するためのシェルスクリプトを作成します #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" 6. docker-compose.ymlを作成する まず、docker-compose.ymlを開きます sudo nano docker-compose.yml 以下のコードをコピペして保存し、終了します version: "3.7" services: web: build: . container_name: web volumes: - .:/myapp command: rails s -b 0.0.0.0 ports: - "3000:3000" networks: - myapp depends_on: - mysql mysql: image: mysql container_name: mysql command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: password ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql - ./mysql-files:/var/lib/mysql-files networks: - myapp networks: myapp: external: true volumes: mysql-data: 7. mysql-filesディレクトリを作成する mkdir mysql-files 8. Dockerのネットワークを作成する sudo docker network create myapp 9. Railsのプロジェクトを作成する sudo docker-compose run web rails new . -d mysql --skip-bundle --force --no-deps 10. コンテナを作成する sudo docker-compose build 11. DBに接続する際の設定を変更する まず、config/database.ymlを開きます sudo nano config/database.yml 以下のコードで上書きして保存し、終了します default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: mysql username: root passowrd: password development: <<: *default database: myapp_development test: <<: *default database: myapp_test production: <<: *default database: myapp_production username: myapp password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %> 12. Webpackerをインストールする sudo docker-compose run web rails webpacker:install 13. DBを作成する sudo docker-compose run web rails db:create 14. RailsとMySQLを起動する sudo docker-compose up Railsが起動した旨のメッセージが表示されたら、ブラウザでlocalhost:3000にアクセスします ここまでの手順が上手くいっていれば、Yay! You're on Rails!という言葉が表示されます その言葉が表示されれば、Dockerを使ったRailsの環境構築は終了です、お疲れ様でした!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

<input type="datetime-local">にcapybaraで値を入力する方法

実行環境 macOS 10.15.7 (19H1217) Ruby 2.6.3p62 Rails 6.0.3.7 RSpec 3.10 Capybara 3.35.3 <input type="datetime-local"> <input> 要素の datetime-local 型は、ユーザーが簡単に日付と時刻、つまり年、月、日と時、分を入力することができる入力コントロールを生成します。ユーザーのローカルタイムゾーンが使用されます。 ↑に例が載っています。 Capybaraで<input type="datetime-local"> に値を入力する Capybaraを使ってsystem specを書いている状況を想定します。 例えば次のようなspecを書いたとすると期待どおり動くでしょうか。 sometime.html ... <input type="datetime-local" id="sometime-input"> ... spec/system/sometime_spec.rb ... sometime = DateTime.now find('input[id="sometime-input"]').set(sometime) ... 動く"時"もあるかもしれませんが,多くの人は動かないと思います。 <input type="datetime-local">のstep属性 動かない原因はstep属性にありました。 <input type="datetime-local">にstep属性の値を明記しない場合,デフォルト値の"60"が使用されます。 これは,デフォルトでは,秒以下の単位は入力できないということです。 先程の例では,step属性の値を明記していない(秒単位の入力はできない)にも関わらず,DateTime.nowの値を入力しようとしたため,DateTime.nowの値がキリのいい値でない(12時30分20秒など秒の単位が00でない)場合は入力できなかったということです。 Re: Capybaraで<input type="datetime-local"> に値を入力する 以上で見てきた問題点を解消する方法は複数あります。 以下ではその例を2つ紹介します。 <input type="datetime-local">そのものを秒単位で入力できるようにする sometime.html ... <input type="datetime-local" id="sometime-input" step="1"> ... このようにすれば入力できます。 テスト時に秒単位を入力しない 例えば, spec/system/sometime_spec.rb ... sometime = DateTime.now.beginning_of_hour find('input[id="sometime-input"]').set(sometime) ... のようにして,テスト時に秒単位を捨てると入力できます。 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】カスタムバリデーションを作成してAncestyに階層制限をかける

はじめに 現在、共同開発にてドキュメント作成アプリを開発中です。 その中でAncestryを用いた階層構造を実装しており、機能としてはドキュメント作成時にタイトル部分に/ を入力して、文章を区切ることによりディレクトリが作成できるようになっています。 今回はディレクトリの階層を5階層までにしたかったので、バリデーションをかけようと思います。しかし、デフォルトのバリデーションオプションでは難しそうでした。 そこで色々調べていくとActiveModel::EachValidator を使うことで、独自のバリデーションを追加することが分かり便利だなと思ったため、備忘録として使い方を残しておきます。 カスタムバリデーターの作成 カスタムバリデーターとは ActiveModel::ValidatorやActiveModel::EachValidator を使って自作のバリデーションを作成することです。 独自のバリデーションルールを追加しようとする時に、modelに書くと可読性が悪くなり、更には管理も大変になってしまいます。それを防ぐためにバリデートクラスと呼ばれる、バリデーションを自作するためのクラスを継承したクラスを作ります。 今回は使いやすいということで、ActiveModel::EachValidator を継承したクラスを作成して、そこに独自のバリデーションルールを書いていきます。 ファイルの作成 app/配下にvalidatorsディレクトリを作成します。 app/validators/<検証名>validator.rbの形式でファイルを作成 ActiveModel::EachValidator を継承したクラスを作成 app/validators/ancestry_validator.rb class AncestryValidator < ActiveModel::EachValidator end ここで指定したクラス名が、modelでカスタムバリデーターを呼び出す際に使われます。 これでカスタムバリデーションを実装する準備ができたため、後はこのファイルにロジックを書いてあげれば完了です。 バリデーションルール作成 まずはカスタムバリデーション用にRailsが用意してくれている、validate_eachメソッドを追加しましょう。 app/validators/ancestry_validator.rb class AncestryValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) end end validate_each メソッドの3つの引数 validate_each メソッドの3つの引数の中身にはそれぞれ、以下の値が入ります。 record ここにはオブジェクトが入ります。 attribute これには属性が入ります。 今回で言えば:ancestyが返ってきます。 value ユーザーが入力した値が入ります。 文字数検証などに使います。 以上のことを踏まえた上で、カスタムバリデーションを設定していきましょう! カスタムバリデーション設定 今回は5階層以降のディレクトリを作成したくないため、以下のように設定しました。 class AncestryValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) if value.count("/") > 3 record.errors.add(attribute, "ディレクトリは6つ以上作成できません!") end end end / の数により何階層目かを判断して、3つ以上になると6階層目になるためバリデーションに引っかかるようにしています。 record.errors.add(属性, メッセージ) 上記のようにすることでバリデーションを発生させて、メッセージを追加することができます。 属性を指定することで、今回なら「ancestryディレクトリは6つ以上作成できません!」と表示されるようになります。 呼び出し 最後に設定したバリデーションを使えるように、対象のモデルでカスタムバリデーションを呼び出す記述を追加しましょう! validates :ancestry, ancestry: true この記述により、ancestryカラムに対してAncestryValidatorクラスのカスタムバリデーションを適応させることができます。 まとめ 今回はActiveModel::EachValidator を使った、独自のバリデーションを作成する方法について紹介しました。 初めて自作のバリデーションを作成しましたが、これを使いこなせるようになれば何でも検証できて便利だなと思ったので、ぜひ使いこなせるようになりたいと思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フォロー・フォロワー機能

Rails5使用の、フォロー・フォロワー機能についてのまとめです。 UserとBookを作成しており、 1対多 となっています。 modelとcontrollerの作成 rails g controller Relationships rails g model relationchip follower_id:integer foolowed_id:integert カラム確認後 rails db:migrate ※ follower する   followed される モデルのアソシエーション relationship.rb belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" class_name: 関連先のモデル参照する際の名前の変更 user.rb has_many :relationships,class_name: "Relationship", foreign_key: "follower_id",dependent: :destroy has_many :passive_relationships,class_name: "Relationship", foreign_key: "followed_id",dependent: :destroy has_many :followings, through: :relationships,source: :followed has_many :followers, through: :passive_relationships,source: :follower def follow(user_id) unless self == user_id self.relationships.find_or_create_by(followed_id: user_id.to_i, follower_id: self.id) end end def unfollow(user_id) relationships.find_by(followed_id: user_id).destroy end def following?(user) followings.include?(user) end has_manyで条件を記載 has_many :relationships, class_name:"Relationship" ↑relationshipsをRelationship名に変更 foreign_key(入口):"follower_id" user.idがfollowerなのかfollowedなのか指定 has_many~~~~~~~through(経由)~~~~~source(出口) userがfollower_idからrelationshipsを経由してfollowerに出る 簡潔にすると、フォローしている人の一覧 コントローラーとルーティング [controller] relationships_controller.rb def create current_user.follow(params[:user_id]) redirect_to request.referer end def destroy current_user.unfollow(params[:user_id]) redirect_to request.referer end def followings user = User.find(params[:user_id]) @users = user.followings end def followers user = User.find(params[:id]) @users = user.followers end [routes] routes.rb resources :users do resouce :relationships, only: [:create, :destroy] get 'relationships/followers' => 'relationships#followers', as: 'followers' get 'relationships/followings' => 'relationships#followings', as: 'followings' end userとネストします。 get で followers,followingsのuser一覧表示をさせる viewの作成 <td>フォロー数: <%= user.followings.count %></td> <td>フォロワー数: <%= user.followers.count %></td> <td> <% unless current_user == user %> <% if current_user.following?(user) %> <%= link_to "フォロー外す", user_relationships_path(user.id), method: :delete %> <% else %> <%= link_to "フォロー", user_relationships_path(user.id), method: :POST %> <% end %> <% end %> </td> user.rbでfollowの表示は自分には不要(フォローするボタン) unless self == user_id と記載済み viewに <% unless current_user == user %> と記載することで 自分のプロフィールにはフォローリンクがなくなります。 フォロー数、誰がフォローしているのか <tr> <th>follows</th> <th><%= link_to user.followings.count, user_followings_path(user.id) %></th> </tr> <tr> <th>followers</th> <th><%= link_to user.followers.count, user_followers_path(user.id) %></th> </tr> link_toでcount数にリンクがつき、 数字を押すと誰がフォローしているのかのviewページに飛ぶ user_followings_pathは rails/info で root の確認 user.idは "誰が" userのidを拾う [count数のリンク先のview] <tr> <% if @user.exists? %> <th>name</th> <th></th> </tr> <tbody> <% @users.each do |user| %> <tr> <td><%= user.name %></td> <td>フォロー数: <%= user.followings.count %></td> <td>フォロワー数: <%= user.followers.count %></td> <td><%= link_to "Show", user %></td> <tr> <% end %> </tbody> <% else %> <th>ユーザーはいません</th> <% end %> exists?メソッド => 指定した条件のレコードがデータベースに存在するかどうかを 真偽値で返すメソッド。 存在すれば、trueを、しなければfalseを返す。 each文で繰り返し処理をし、 user.nameで誰がフォローしているのか user.nameさんは何人フォローしていて、何人フォロワーがいるのか link_to "Show"でuser.nameさんのプロフィール画面へ遷移させる。 以上でフォロー・フォロワー機能の実装が終わりました。 足りない部分や、違うところがありましたら教えてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuデプロイ奮闘記(後編)

これは、度重なるエラーに苦戦しながらも初めてのHerokuデプロイに成功した、ある一人の男の物語である。 結論(デプロイまでの過程) Rubyの3.0.1をインストールしてバージョン変更。 更にRubyの2.7.3をインストールしてもう一度バージョン変更。Bundlerの2.2.17をインストールしてこっちもバージョン変更。 更にBundlerの2.2.16をインストールしてこっちももう一度バージョン変更(Heroku環境に合わせるため)。このバージョンでbundle install。 BundlerをPCの環境に合わせるために、コマンドbundle lock --add-platform x86_64-linuxを実行。 deviseとkaminariの各Gemを開発・テスト・本番の全ての環境で使えるようにする。 config/environments/production.rbのconfig.assets.js_compressor = :uglifierの行をコメントアウトして、テンプレートリテラルを認識させる。 app/assets/stylesheets/style.scss先頭の@import compass;をコメントアウト。 app/assets/stylesheets/style.scss先頭の@import compass;のコメントを外して(元に戻して)、Gemのcompass-railsを開発・テスト・本番の全ての環境で使えるようにする。 デプロイ完了!! ※今回も長くなりそう(なった)なので、前半と後半に記事を分けて書きます。 前提条件 macOS Catalina ( 10.15.71 ) Rails ( 5.2.61 ) Terminal.app ( 2.101 ) Ruby ( 2.7.32 ) Bundler ( 2.2.162 ) Google Chrome.app( 91.0.4472.771 ) Visual Studio Code.app( 1.56.21 ) 本題 5. deviseとkaminariの各Gemを全環境で使えるようにする なかなかそう簡単にはデプロイさせてくれない... terminal.app remote: ! remote: ! Precompiling assets failed. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed 出力されるエラーメッセージは多種多様だが、英単語を知ってれば自ずと読めるようになる。大事な部分は違う色などで強調して教えてくれるから、その付近を注視すれば成功したのか失敗したのかを把握する事は可能(但し、必ずしもそうとは限らないので注意)。上の6行だと、先頭3行が remote: ! remote: ! Precompiling assets failed. remote: ! というように、赤色で表示されていた。何だかよく分からんけど、「アセットのプリコンパイルに失敗した」から結果的にプッシュ(デプロイ)に失敗したと言っている事が分かる。ところで、この「プリコンパイル」とは一体何なのか?こちらのIT用語辞典では、 プリプロセッサとは、ソフトウェアの役割による分類の一つで、ある中心的な処理を行うプログラムに対して、その前処理(preprocess)3を行うプログラムのこと。プログラミング言語のコンパイラの前処理を行うものが非常に有名。 と説明している。この通りなんだろうが、要はコンパイラがコンパイル4しやすいように、ちょっとソースコードを改変する作業のこと。 外部ファイルの読み込み 文字列の置換 条件分岐処理 などなど。大学でCやJavaを少し習ったけど、その時に起こしてたコンパイルエラーは、このプリコンパイルが正しく処理されたのを前提とした上で発生したものだったのだろうか...?だとしたら解決策も変わってくる筈だ。出力されたメッセージを上から注意して見ていくと、 terminal.app remote: Bundle completed (4.09s) remote: Cleaning up the bundler cache. remote: -----> Installing node-v12.16.2-linux-x64 remote: -----> Detecting rake tasks remote: -----> Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: rake aborted! remote: NameError: uninitialized constant Devise (以下略) エラーの発生箇所を発見!「Bundle completed」とあるから、Gemの読み込みは完了したっぽい。問題はその後に起きたようだ。ここで「NameError: uninitialized constant Devise」でググってみたら、見事に解決方法が記述されているこちらの方の記事を見つけた。どうやら本番環境でGemが使えるように設定されてなかったのが原因だったようだ。そんな馬鹿な...と思ってGemfileを見てみたら、group :development, :test do ... endの中にしっかりとセットしてますた(笑)早速deviseと、ついでにページネーション5実装時に使用するkaminariもこのグループから取り出して、ファイルの最下部に書き直した。 Gemfile (省略) group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'pry-rails' gem 'compass-rails', '3.1.0' gem 'sprockets', '3.7.2' - gem 'devise' - gem 'kaminari' gem 'rspec-rails' gem 'rails-controller-testing' gem 'factory_bot_rails' gem 'faker', '~> 2.8' end (省略) # Use jquery as the JavaScript library gem 'jquery-rails' # 最下部に追加 + gem 'devise' + gem 'kaminari' 【別解】若しくは下のように、開発・テスト・本番の3つの環境によるグループを作成してその中に入れ込む。 Gemfile (省略) group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'pry-rails' gem 'compass-rails', '3.1.0' gem 'sprockets', '3.7.2' - gem 'devise' - gem 'kaminari' gem 'rspec-rails' gem 'rails-controller-testing' gem 'factory_bot_rails' gem 'faker', '~> 2.8' end (省略) group :production do gem 'rails_12factor' end # 全運用環境によるグループを作成 + group :development, :test, :production do + gem 'devise' + gem 'kaminari' + end (省略) そりゃプリコンパイルできない訳だ...まぁ当時は本番環境のことを何も考慮してなかったから仕方ない...orz 因みに「NameError: uninitialized constant Devise」というエラーを見て、「Devise」が最初Gemのdeviseのことだと思わず、電子デバイスかなんかと勘違いしちゃって「ハードの問題か!?」とちょっと焦っちまったのは内緒...❤️ 6. テンプレートリテラル記法を認識させる さぁ、今回も楽しいたのしいデプロイチャレンジの時間がやって参りました!(白目) terminal.app ログインしてなかったらログインする % heroku login --interactive コードの全修正をインデックスに追加 % git add . Let's commit! % git commit -m “コミットメッセージ” デプロイリベンジ % git push heroku master 結果は...? terminal.app remote: ! remote: ! Precompiling assets failed. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed ぐぬぬ...同じエラーか...しかし、メッセージを遡って読んでみると terminal.app remote: Bundle completed (3.49s) remote: Cleaning up the bundler cache. remote: -----> Installing node-v12.16.2-linux-x64 remote: -----> Detecting rake tasks remote: -----> Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: Yarn executable was not detected in the system. remote: Download Yarn at https://yarnpkg.com/en/docs/install remote: rake aborted! remote: Uglifier::Error: Unexpected character '`' (以下略) 良かった...少しは進展があったようだ。「予期してない文字『`』」というUglifier6によるエラー。 この解決策もこちらの方の記事に一緒に記述されてあった。このバッククオート`は無論テンプレートリテラルによるものである。この記法を認識させるには、 config/environments/production.rb (省略) # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + # config.assets.js_compressor = :uglifier ←この行をコメントアウト # config.assets.css_compressor = :sass (省略) これだけでOK。 7. Gemのcompass-railsを無効にするつもりだった(意味のない変更) このstepは完全に誤りです。恥ずかしいので無視してもらってかまいませんが、変更部分は一応記述しておきます。 terminal.app remote: ! remote: ! Precompiling assets failed. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed はい、またまたプリコンパイル失敗...。めげずにstep.5やstep.6と同様に出力メッセージを辿ってみる。 terminal.app remote: Bundle completed (4.34s) remote: Cleaning up the bundler cache. remote: -----> Installing node-v12.16.2-linux-x64 remote: -----> Detecting rake tasks remote: -----> Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: Yarn executable was not detected in the system. remote: Download Yarn at https://yarnpkg.com/en/docs/install remote: rake aborted! remote: Sass::SyntaxError: File to import not found or unreadable: compass. (以下略) お次は「インポートするcompassファイルが見つからないか読み取れない」というSyntaxError(構文エラー)。なぜか「見つからないなら無効にして無かった事にすれば良いじゃん!」と安直に考えてしまい、 app/assets/stylesheets/style.scss - @import "compass"; + // @import "compass"; ←先頭の第1行をコメントアウト /* * "ほのか明朝" licensed under the IPA Font License Agreement v1.0 * http://fontfree.me/966(配布元のURL) * http://ipafont.ipa.go.jp/(IPAフォントのURL) * http://ipafont.ipa.go.jp/ipa_font_license_v1.html(IPAフォントライセンスv1.0のURL) */ (以下略) てやっちゃった!(・ω < )使用するためにインストールした筈なのに... 8. step.7による変更を元に戻して、Gemのcompass-railsを全環境で使えるようにする terminal.app remote: Bundle completed (4.20s) remote: Cleaning up the bundler cache. remote: -----> Installing node-v12.16.2-linux-x64 remote: -----> Detecting rake tasks remote: -----> Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: Yarn executable was not detected in the system. remote: Download Yarn at https://yarnpkg.com/en/docs/install remote: rake aborted! remote: Sass::SyntaxError: Undefined mixin 'transition-property'. (以下略) 内容が変わってるから少しずつゴールには向かっているのだろうけど、いつになったらこの「Precompiling assets failed.」のエラーから抜け出せるのか...先程と同じくSassによるSyntaxErrorだが、今度は「定義されてないmixin『transition-property』」とのこと。 フロントエンド学習時に少し出てきたけど、ちょっとだけ復習...。変数は値を定義するものだが、このmixinとはスタイルを定義するものである。これを利用すれば、何度も同じスタイルを使い回す事ができる。例えばセレクタの.clearfixはよく使われる可能性のあるスタイルなので、 sample_style.scss @mixin clearfix() { &:after { clear: both; display: block; content: ""; } } みたいに事前に定義しておき、必要になったら@includeを用いて sample_style.scss @include clearfix(); とすれば毎回呼び出す事ができる。さてさて、そんじゃ今回のスタイルシートで、mixinのtransition-propertyを呼び出している箇所を見てみるか... app/assets/stylesheets/style.scss (省略) .transition { @include transition-property(all); @include transition-duration(0.2s); @include transition-timing-function(linear); } (省略) あったあった。どうやら他にもtransition-durationやtransition-timing-functionというmixinも併せて定義して、ここで一緒に呼び出しているようだ。ではこれらのmixinを定義している箇所も見てみるか...あれ?無いぞ!?command + pで検索しても見つからない。別ファイルに定義してある訳でもなさそうだ...どうやらこれはGemのcompass-railsが関係しているらしい。そもそもこの『compass』って何だろう? ちょっとググってみると、CSSを効率的に作成できるように設計されたSass用のフレームワークとのこと。多数のmixinや関数がライブラリ化してあり、色んな機能を提供してくれる。ここで、VSCodeから検索のサイドバーを開いて、上の検索ボックスにtransition-propertyと入力し、これと同じ記述がしてあるコードを含むファイルがあるかどうかを調べてみると、 app/assets/stylesheets/setting.css (省略) .animate { -moz-transition-property: all; -o-transition-property: all; -webkit-transition-property: all; transition-property: all; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; -webkit-transition-duration: 0.2s; transition-duration: 0.2s; -moz-transition-timing-function: ease-in; -o-transition-timing-function: ease-in; -webkit-transition-timing-function: ease-in; transition-timing-function: ease-in; } (省略) 発見した!ベンダープレフィックス7が付いてるのもあれば、そのまま使えるのもあるっぽい。これらはstep.7でコメントアウトした@import "compass";により、モジュールとして読み込む事ができる。と言うことは、やっぱりこれを無効化するのは間違いだったか... このGemについて理解しようとすると、俗に言うHTML/CSSの沼にまたハマってしまうこと間違い無しなので(笑)、とにかくエラーの解決に集中。step.7で付け加えたコメントを外して、「ローカル環境では普通に使えてたんだから、リモートでも使えるようにすれば良いだけじゃないのか」という正しい解釈に至った。 app/assets/stylesheets/style.scss - // @import "compass"; + @import "compass"; ←先頭の第1行のコメントを外す /* * "ほのか明朝" licensed under the IPA Font License Agreement v1.0 * http://fontfree.me/966(配布元のURL) * http://ipafont.ipa.go.jp/(IPAフォントのURL) * http://ipafont.ipa.go.jp/ipa_font_license_v1.html(IPAフォントライセンスv1.0のURL) */ (以下略) step.5と同様に、開発・テストの両環境のグループからcompass-railsを取り出して、ファイルの最下部に書き直す。ついでにアセットパイプライン8というものを実装するためのsprocketsというGemも、一緒に外に出しとくか。多分、問題無いだろう... Gemfile (省略) group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'pry-rails' - gem 'compass-rails', '3.1.0' - gem 'sprockets', '3.7.2' gem 'rspec-rails' gem 'rails-controller-testing' gem 'factory_bot_rails' gem 'faker', '~> 2.8' end (省略) # Use jquery as the JavaScript library gem 'jquery-rails' # 最下部に追加 + gem 'compass-rails', '3.1.0' + gem 'sprockets', '3.7.2' gem 'devise' gem 'kaminari' 【別解】若しくは下のように、開発・テスト・本番の3つの環境によるグループの中に入れ込む。 Gemfile (省略) group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'pry-rails' - gem 'compass-rails', '3.1.0' - gem 'sprockets', '3.7.2' gem 'rspec-rails' gem 'rails-controller-testing' gem 'factory_bot_rails' gem 'faker', '~> 2.8' end (省略) group :production do gem 'rails_12factor' end # 全運用環境によるグループを作成 group :development, :test, :production do + gem 'compass-rails', '3.1.0' + gem 'sprockets', '3.7.2' gem 'devise' gem 'kaminari' end (省略) 9. 感動のフィナーレ それは、一番下の方に記されてあった... terminal.app remote: Verifying deploy... done. で...できたー! 正直この流れだとまだ続きそうな感じだったが、ようやくこの闘いに終止符が打たれた。厳密に言うと、少しこの上の方に terminal.app remote: ###### WARNING: remote: remote: No Procfile detected, using the default web server. remote: We recommend explicitly declaring how to boot your server process via a Procfile. remote: https://devcenter.heroku.com/articles/ruby-default-web-server と表示されてあるが、多分現段階だと無視しても問題無い警告メッセージだろう。 今回学んだこと デプロイに対して牧歌的すぎた。ちょっとナメてた。 環境構築時以外でも、バージョンや依存関係は常に意識しておく。 新しいGemをインストールする際は、どの環境で運用させるべきかを予め把握しておく必要がある。 エラーログに目立つ色で強調表示してある部分は重要なメッセージが記されている。そうじゃない部分にも重要なメッセージが記されている。 やっぱり英語は大事。 今回はこの程度の変更修正で済んだけど、下手したらもっと多くのエラーが出て、もっと時間を費やす羽目になっていたかもしれない。こんな形で備忘録として残しておいて、同じようなエラーが発生したら直ぐに対処できるようにしておきたい。 これをもって、初めてのHerokuデプロイが完了したことにする。 参考記事 Herokuにデプロイをする時に詰まった箇所について - Qiita compassエラーからのー - 思ったこと Grunt プリプロセッサとは - IT用語辞典 Compassの使い方 - 過去記事 - [SMART] Uglifier::Error: Unexpected character '`' 【Railsデプロイエラー】 - 箱のプログラミング日記。 CSSとは - IT用語辞典 アセットパイプライン - Railsガイド 関連投稿記事 Herokuデプロイ奮闘記(前編) - Qiita ブログ執筆時のバージョン ↩ step.4後段階のバージョン ↩ 逆に、メインの処理に対する後処理を行うものをポストプロセッサというらしい。これは、いわゆるインタプリタとかを指すのかな? ↩ プログラミング言語で書かれたソースコードを解釈して、そのソースコードを今度はコンピュータが解釈、且つ実行できるネイティブコード(若しくはバイトコード)に変換する翻訳作業のこと。 ↩ 長い文章を複数のページに分割して、各ページへのリンクを並べアクセスしやすくする機能。Googleなんかの検索結果のページの下に表示されてるアレ。 ↩ Railsに標準で搭載されているJavaScript軽量化のライブラリのことらしい。これは英語の『醜くする』という意味の『uglify』が由来っぽい? ↩ CSSでプロパティや値を指定する際に、各ブラウザのメーカーさんが独自に拡張した機能を呼び出すために付けられる接頭辞のこと。 ↩ JavaScriptやCSSなどのアセットを最小化または圧縮化して連結するためのフレームワークのことらしい。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bin/rails app:update で routes.rb をキレイに上書きしてしまった話

概要 Rails を 6.0.0 から 6.1.3 にバージョンアップしようとしていた bin/rails app:update を実行し、出てくるプロンプトを脳死で全て Y で許可した 結果、config/routes.rb をまっさらなものに上書きしてしまった 参考記事 How to Upgrade Rails app "Warning, there can be breaking changes to your application if you accept all changes suggested. (i.e. getting a new clean routes.rb file.) Be careful with your choices!" この記事はこの 2 文に尽きます。 詳細 Rails をバージョンアップする際に config 周辺のファイルの更新をしようとbin/rails app:updateを実行しました。 実行して、もしファイルに conflict が発生した場合には conflict config/routes.rb Overwrite /sample/config/routes.rb? (enter "h" for help) [Ynaqdhm] と聞かれます。脳死で Y を押すと聞かれている通り Overwrite されてしまいますので、 差分を見て上書きしても問題ないか確認する必要があります。 記載の通り h で help を見ると Y - yes, overwrite n - no, do not overwrite a - all, overwrite this and all others q - quit, abort d - diff, show the differences between the old and the new h - help, show this help m - merge, run merge tool と説明がでるので、d で --- /sample/config/routes.rb 2021-05-31 15:57:03.000000000 +0900 +++ /sample/config/routes.rb20210604-77510-10uw86v 2021-06-04 09:46:51.000000000 +0900 @@ -1,3 +1,3 @@ Rails.application.routes.draw do - root 'application#hello' + # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end Retrying... Overwrite /sample/config/routes.rb? (enter "h" for help) [Ynaqdhm] と差分が確認できます。 更新したくない場合は n でスキップすればよいのですね。 まとめ 何か聞かれたらちゃんと内容を聞きましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyの例外とは? 例外処理の流れ(未経験エンジニア・駆け出しエンジニアの方におすすめ)

そもそも例外とは プログラミングにおいていわゆるエラーと呼ばれるものには2種類あります。 1つは、シンプルにエラー。もう一つはバグ。 この二つの違いは以下です。 □エラー → 開発者が想定していなかったエラー □バグ → 開発者が認識しているエラー ※エラーには構文(Syntax)エラーと論理エラーがありますが、ここでは詳細を割愛します。 もちろん、開発者はバグを治す必要がありますが、その中でも「ここでエラーが起こるかもしれないなぁ」と思いながらも、その可能性の低さや、エラーを完全に消去することの難しさから、違う対処法を取ることもあります。 それこそが、例外処理であり、そのエラーが発生した時の情報をログで残しつつ、救済処置を用意しておくことで、エラーが発生した時は例外処理で対処しつつ、今後そのエラーを減らしたり、対策したりするためにログで情報を残すことができます。 ここからはRubyの例外処理です。 Rubyの例外とは まず、Rubyの例外とはどのようなものなのか。 Rubyでは例外もオブジェクトである。 オブジェクトなので例外オブジェクトはメソッドを持っています。このメソッドを使うことで発生した例外の情報を抜き出すことができます。 具体的な情報の抜き出し方は後ほど説明します。 まずは、例外発生の具体例を見てみましょう。 example.rb def method_1 puts 'メソッド処理スタート!' 1 / 0 #ゼロ除算で例外を発生させる end 出力 メソッド処理スタート! ZeroDivisionError: divided by 0 # エラー発生!プログラムも止まる。 このZeroDivisionErrorが例外であり、例外オブジェクトと呼ばれるものです。 また、1 / 0 より前にあるputs はきちんと出力されていますね! また、例外はオブジェクトなので、以下のようなメソッドを持っています。 example.rb # error は例外が発生した時にできる例外オブジェクトというものです。 error.class # エラーオブジェクトのクラス error.message # エラーオブジェクトのクラス error.backtrace # バックトレース これらのメソッドで例外が発生した際の情報が得られます。情報は、エラーオブジェクトのクラスや、エラーオブジェクトのクラス、バックトレース(エラーが発生するまでにたどったコードたち)。 しかし、一つ目の例で見たコードだと普通にエラーを吐いてコードが止まりますね。(1 / 0の部分) このままではerrorオブジェクトはキャッチできないですし、まず本番コードだと普通にバグでアプリケーションが死んでしまいます笑 ってことで、ここからは例外処理というものを学びます! 例外処理は以下の記法で書けます。 例外処理 begin # 例外が起こりうる処理 rescue => error # 例外が発生した場合の処理 end これでbeginで発生したエラーのオブジェクトがerrorとしてキャッチでき、プログラムも止まりません。 さっきのゼロ除算を例に見ると以下になります。 example.rb def method_1 puts 'メソッド処理スタート!' begin 1 / 0 #ゼロ除算で例外を発生させる rescue => e # エラーキャッチ! 例外オブジェクトは、errorじゃなくてもどんな文字列でもおけです。eという記法が良く使われます。 e.class e.message e.backtrace end end 出力 メソッド処理スタート! ZeroDivisionError # エラーオブジェクトのクラス divided by 0 # エラーメッセージ (irb):2:in '/' # バックトレース (irb):2:in 'irb_binding' # バックトレース (略) これでプログラムを止めずにエラーの原因を特定する情報も手に入れることができますね! まとめ ここまで例外と例外処理について説明してきました。 しかし、例外処理は積極的に使うべきではありません。 フレームワークのRailsであればエラーが発生した時に、ユーザーをエラー画面に遷移させる例外処理が組み込まれています。 変にいじってしまうと元々の「レール」から外れてしまいかねません。RailsのベストプラクティスはRails側が用意したレールの上を走り続ける事です。 特に初心者の方は、「例外が発生したら即座に異常終了させよう」「フレームワークの共通処理に全部丸投げしよう」と考えましょう! 参考文献 書籍: プロを目指す人のためのRuby入門(伊藤 淳一)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyの例外とは?例外処理の流れ(未経験エンジニアにおすすめ)

はじめに 例外処理について学ぶタイミングってなかなかないですよね〜。 とはいえ、実務になると普通によく遭遇します。 ということで、これからエンジニアになりたい人、エンジニアになりたての人に向けて、例外とは何かというところから丁寧に解説した記事です。 ぜひご覧ください。 そもそも例外とは プログラミングにおいていわゆるエラーと呼ばれるものには2種類あります。 1つは、シンプルにエラー。もう一つはバグ。 この二つの違いは以下です。 □エラー → 開発者が想定していなかったエラー □バグ → 開発者が認識しているエラー ※エラーには構文(Syntax)エラーと論理エラーがありますが、ここでは詳細を割愛します。 前提として、開発者はバグを治す必要があります。 そこで、「ここでエラーが起こるかもしれないなぁ」と思う箇所に、エラーが発生した場合の対処法をコードに落とし込んでおいたり、エラーメッセージをログに残すことで、開発者がエラーの改修をすばやく行うことができるようになります。 それこそが、例外処理であり、そのエラーが発生した時の情報をログで残しつつ、救済処置を用意しておくことで、エラーが発生した時は例外処理で対処しつつ、今後そのエラーを減らしたり、対策したりするためにログで情報を残すことができます。 ここからはRubyの例外処理です。 Rubyの例外とは まず、Rubyの例外とはどのようなものなのか。 Rubyでは例外もオブジェクトである。 オブジェクトなので例外オブジェクトはメソッドを持っています。このメソッドを使うことで発生した例外の情報を抜き出すことができます。 具体的な情報の抜き出し方は後ほど説明します。 まずは、例外発生の具体例を見てみましょう。 example.rb def method_1 puts 'メソッド処理スタート!' 1 / 0 #ゼロ除算で例外を発生させる end 出力 メソッド処理スタート! ZeroDivisionError: divided by 0 # エラー発生!プログラムも止まる。 このZeroDivisionErrorが例外であり、例外オブジェクトと呼ばれるものです。 また、1 / 0 より前にあるputs はきちんと出力されていますね! また、例外はオブジェクトなので、以下のようなメソッドを持っています。 example.rb # error は例外が発生した時にできる例外オブジェクトというものです。 error.class # エラーオブジェクトのクラス error.message # エラーオブジェクトのクラス error.backtrace # バックトレース これらのメソッドで例外が発生した際の情報が得られます。情報は、エラーオブジェクトのクラスや、エラーオブジェクトのクラス、バックトレース(エラーが発生するまでにたどったコードたち)。 しかし、一つ目の例で見たコードだと普通にエラーを吐いてコードが止まりますね。(1 / 0の部分) このままではerrorオブジェクトはキャッチできないですし、まず本番コードだと普通にバグでアプリケーションが死んでしまいます笑 例外処理 ってことで、ここからは例外処理というものを学びます! 例外処理は以下の記法で書けます。 例外処理 begin # 例外が起こりうる処理 rescue => error # 例外が発生した場合の処理 end これでbeginで発生したエラーのオブジェクトがerrorとしてキャッチでき、プログラムも止まりません。 さっきのゼロ除算を例に見ると以下になります。 example.rb def method_1 puts 'メソッド処理スタート!' begin 1 / 0 #ゼロ除算で例外を発生させる rescue => e # エラーキャッチ! 例外オブジェクトは、errorじゃなくてもどんな文字列でもおけです。eという記法が良く使われます。 e.class e.message e.backtrace end end 出力 メソッド処理スタート! ZeroDivisionError # エラーオブジェクトのクラス divided by 0 # エラーメッセージ (irb):2:in '/' # バックトレース (irb):2:in 'irb_binding' # バックトレース (略) これでプログラムを止めずにエラーの原因を特定する情報も手に入れることができますね! まとめ ここまで例外と例外処理について説明してきました。 しかし、例外処理は積極的に使うべきではありません。 フレームワークのRailsであればエラーが発生した時に、ユーザーをエラー画面に遷移させる例外処理が組み込まれています。 変にいじってしまうと元々の「レール」から外れてしまいかねません。RailsのベストプラクティスはRails側が用意したレールの上を走り続ける事です。 特に初心者の方は、「例外が発生したら即座に異常終了させよう」「フレームワークの共通処理に全部丸投げしよう」と考えましょう! 参考文献 書籍: プロを目指す人のためのRuby入門(伊藤 淳一)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む