20210414のRailsに関する記事は21件です。

【Rails】コードレビューで脆弱性を発見しよう!セキュリティコードレビューのルール10選

コードレビューは誰かが書いたソースコードを確認することで、ソフトウェアの問題を発見する作業です。 コードレビューで発見できる問題には、コーディング規約違反や機能的なバグだけでなく、セキュリティの欠陥、つまり脆弱性も含まれます。 本記事では筆者が実際に脆弱性を発見したことがあるコードレビューのルールを10個紹介します。 1. JSON からの情報漏洩 SPAなどAjaxによる画面更新を実装するために JSON を返すアクションを作ることがあります。その際、オブジェクトを不用意にシリアライズしてしまうと意図しない情報漏洩につながります。 レビュールール コントローラを正規表現 render.+?json で検索し、必要最小限の属性のみシリアライズされていることを確認します。 悪い例 オブジェクトをそのまま render render :json => @user user に氏名住所など非公開情報や、パスワードのような秘密情報が含まれていたら大変ですね。 良い例 画面描写に必要な属性だけシリアライズ render json: @user.slice(:id, :first_name) 補足 jb や jbuilder を使っている場合は、テンプレートに不要な属性が含まれていないことを確認してください。 2. Strong Parameters が適切に設定されていること Strong Parameters で更新可能なパラメータを不用意に permit してしまうと、開発者が意図しないデータを変更される危険性があります。 レビュールール すべてのコントローラを文字列 .permit で検索し、不要な属性が含まれていないことを確認します。 悪い例 全ての属性を permit params.require(:user).permit! 不要な属性を permit params .require(:user) .permit(:email, :admin, :first_name, :last_name) admin (管理者フラグ) を変更できてしまうのは危険です。 良い例 必要最小限の属性だけ permit params .require(:user) .permit(:email, :first_name, :last_name) 3. クエリに認可が含まれていること クエリに認可が含まれていない場合、URL等に含まれるIDが改ざんされて他のユーザのリソースが不正に操作されてしまうことがあります。 例)自分のプロフィール画面のURL http://example.com/customers/123 の 123 を 444 に変えてみたら、他人のプロフィールが見えてしまった。 レビュールール コントローラを正規表現 params\[:.*id\] で検索し、リソースにアクセス制御が必要な場合、アクションまたはクエリにアクセス制御が含まれているかを確認します。 悪い例 全てのリソースを更新できてしまう def update @article = Articles.find(params[:article_id]) @article.update!(article_params) redirect_to @article end article_id パラメータを変更することで、他人の article も更新できてしまいます。 ただし、下記の場合は安全です。 コントローラ、アクションでアクセス制御を実装している(例えば管理者のみ使用できる、など) cancancan や pundit でアクセス制御を実装している ※アクセス制御ロジックの妥当性は別途確認しましょう 良い例 クエリにアクセス制御が含まれている def update @article = @current_user.articles.find(params[:article_id]) @article.update!(article_params) redirect_to @article end article_id パラメータを変更しても、ユーザ自身の article しか参照・更新できないので安全です。 4. CORS が適切に構成されていること CORSの設定ミスは Same Origin Policy によるセキュリティを緩めてしまい、意図しない情報漏洩につながる可能性があります。 レビュールール すべてのコードを Access-Control-Allow-Origin で検索し、信頼できない Origin を設定していないことを確認します。 悪い例 ワイルドカード headers['Access-Control-Allow-Origin'] = '*' リクエストヘッダをコピー headers['Access-Control-Allow-Origin'] = request.headers["Origin"] null headers['Access-Control-Allow-Origin'] = 'null' ※非公開情報を含まない、オープンなAPIの場合のみ * を設定しても安全です。 良い例 信頼できる Origin headers['Access-Control-Allow-Origin'] = `our-website.example.com' 許可リストに基づいて検証された request.headers["Origin"] if %w(example.com example.jp).include?(request.headers["Origin"]) headers['Access-Control-Allow-Origin'] = request.headers["Origin"] end 補足 凝ったCORS設定が必要な場合はrack-corsを使う場合もありますが、その場合も Access-Control-Allow-Origin の妥当性は確認しましょう。 CORSについての詳細はMDN: オリジン間リソース共有 (CORS)を参照してください。 5. レースコンディション:ファイルパスが重複しないこと 一時ファイルなどを固定のファイルパスに書き込みしている場合、同時に複数のリクエストが実行されるとデータが壊れてしまいます。これによって整合性がとれなくなったり、意図しない情報漏洩につながったしります。 レビュールール すべてのコードを正規表現 File.open\(.+?"w で検索し、ファイルパスが重複する可能性がないことを確認します。 悪い例 固定値 f = File.open("tmp/upload-file", "w") ユーザがアップロードしたファイルの名前 file = fileupload_param[:file] output_path = Rails.root.join('public', file.original_filename) f = File.open(output_path, "w") ※該当するロジックがアクションから呼び出されない場合は安全です。 良い例 重複する可能性がないか超低い output_path = Rails.root.join('tmp', SecureRandom.uuid) File.open(output_path, "w") { |f| 補足 そもそも一時ファイルを作るだけなら tempfile を使いましょう。 6. レースコンディション:時刻をIDにしていないこと ファイルだけでなく、IDやキーも同様に重複するとレースコンディションが発生する可能性があります。 過去に見受けられたケースとして、時刻をIDとして使ってしまうケースがありました。利用者が少数の時は発現する可能性は低いですが、利用者が増えてくるとリクエストが同時に処理される可能性が高くなり、複数のリクエスト間で同一のリソースを操作してしまいます。これによってデータの整合性がとれなくなったり、意図しない情報漏洩につながったしります。 レビュールール すべてのコードを正規表現 Time\.(now|current|zone\.now) で検索し、IDやファイル名などを現在時刻だけで作成していないかを確認します。 悪い例 IDが時刻のみで構成されている File.open("tmp/#{Time.zone.now.to_i}", "w") ※概要するロジックがアクションから呼び出されない場合は安全です 良い例 IDが時刻+ユニークな値で構成されている File.open("tmp/#{Time.zone.now.to_i}_#{SecureRandom.uuid}", "w") 補足 IDとなりうるものとして、ファイルやデータベースのレコードだけでなく、S3のオブジェクトキーなども含まれます。 7. CSRF対策:GETメソッドでリソースを変更していないこと GETメソッドのアクションでリソースの状態を変更すると、CSRF攻撃に対して脆弱になります。 レビュールール GETメソッドでリソースを変更していないことを確認します。どのアクションがGETメソッドなのかは rails routes の出力を見ると良いでしょう。 悪い例 GETメソッドのアクションでデータを作成、更新、削除をしている場合、CSRF攻撃に脆弱です。 # GET /articles/:id/change_to_private def change_to_private @article = @current_user.articles.find(params[:id]) @article.private = true ... 良い例 GETメソッドのアクション内がデータの参照のみの場合、安全です。 def show @article = @current_user.articles.find(params[:id]) end 8. CSRF対策:トークン検証を省略していないこと CSRFトークンの検証を安易に無効化すべきではありません。 過去の事例として、Ajax通信をすると Can't verify CSRF token authenticity エラーが出るからと言って検証を無効化しているケースがありました。 レビュールール コントローラを次の文字列で検索し、トークン検証を無効化していないことを確認します。 verify_authenticity_token skip_forgery_protection protect_from_forgery 悪い例 変更が伴うアクションでCSRFトークン検証を省略すべきではありません。 class HogeController < ApplicationController protect_from_forgery :except => [:index, :show, :update] class HogeController < ApplicationController skip_forgery_protection class HogeController < ApplicationController skip_before_action :verify_authenticity_token 良い例 参照系のアクションだけど、クエリ文字列が長くなってしまうためにHTTPメソッドをPOSTに変更しているような場合、CSRFトークン検証を省略しても安全です。 class HogeController < ApplicationController protect_from_forgery :except => [:search] セッションに基づかないリクエスト(API等)の場合、CSRFトークン検証を省略しても安全です。 class HogeApiController < ApplicationController skip_forgery_protection 補足 Rails の CSRF 対策については、Rails セキュリティガイド:CSRFへの対応策 を参照してください。 9. 標準のセキュリティ機能を無効化していないこと Rails 標準のセキュリティ機能を無効化すべきではありません。 レビュールール config/application.rb の各種設定を確認し、設定値の妥当性を確認します。 注意が必要な例 CSRFトークン検証を無効化 config.action_controller.allow_forgery_protection = false デフォルトのセキュリティヘッダーを削除 config.action_dispatch.default_headers.clear デフォルト値やヘッダの意味はRailsガイドの9 デフォルトのヘッダーを参照してください。 10. Ransack を ransackable なしで使ってないこと 便利すぎる機能は、時として脆弱性となってしまうこともあります。ここでは ransack を使った検索機能を例に選びました。 ransack は高機能な検索機能を実装するのに便利ですが、非公開の属性で検索出来てしまうと、検索結果の差異から非公開の値を特定できてしまいます。これは暗号化されたパスワードやトークンなど、データベースに保存された秘密情報の漏洩につながります。 レビュールール Gemfileに ransack が含まれている場合、ソースコードを ransack で検索します。下記3つの条件を満たす場合、脆弱です。 {モデル}.ransack のパラメータがユーザ入力 モデルにパスワードやトークン等、非公開の属性が含まれている ransackable_attribute, ransackable_associations, ransortable_attributes がモデルに定義されていない、または非公開の属性が含まれている ちょっと複雑ですが、要は非公開の属性やアソシエーションで検索できたらNGということです。 悪い例 1. ransack のパラメータがユーザ入力 # app/controllers/users_controller.rb def search @q = User.ransack(params[:q]) 2. モデルに非公開の属性が含まれている # db/schema.db create_table "users", force: :cascade do |t| t.string "email" t.string "password" t.string "api_key" t.boolean "admin" t.string "first_name" t.string "last_name" t.datetime "created_at" t.datetime "updated_at" end どの属性が非公開かはアプリケーションの仕様によりますが、少なくとも password や api_key は公開してはいけない属性でしょう。 3. ransackable_attribute, ransackable_associations, ransortable_attributes がモデルに定義されていない、または非公開の属性が含まれている # app/models/user.rb class User < ApplicationRecord # def ransackable_* がない end 良い例 次の 1, 2, 3 いずれかに当てはまる場合、安全です。 1. ransack のパラメータがユーザ入力ではない Article.ransack(id_eq: 1) 2. モデルに非公開の属性やアソシエーションが含まれていない # db/schema.db create_table "users", force: :cascade do |t| t.string "email" t.string "first_name" t.string "last_name" t.datetime "created_at" t.datetime "updated_at" end 3. ransackable_attribute, ransackable_associations, ransortable_attributes がモデルに定義されており、必要な属性・アソシエーションに絞られている # app/models/user.rb class User < ApplicationRecord # ... def self.ransackable_attributes(auth_object = nil) super & %w(email first_name last_name) end def self.ransortable_attributes(auth_object = nil) ransackable_attributes(auth_object) end def self.ransackable_associations(auth_object = nil) [] end # ... end 補足 ransack の認可 ransackable の詳細は Ransack: Authorization (whitelisting/blacklisting) を参照してください。 ransack で検索してもマッチしない場合、search を使っている可能性があります。詳細は Ransack #search method を参照してください。 Next step 脆弱性を発見するコードレビュールールを10個紹介しました。こんな感じでコードレビューをすると脆弱性を発見できるかもしれません。 次に、よりセキュアにアプリケーション開発をするためにできることを書いておきます。 Brakeman 今回はソースコードを目視で確認しましたが、自動で解析してくれるツールもあります。 Brakeman は、SQLインジェクションやクロスサイトスクリプティングなどインジェクション系の脆弱性やセキュリティ設定の不備をそれなりの精度で検出してくれる便利なツールです。 ただし Brakeman は全ての脆弱性を見つけてくれるわけではありません。本記事で紹介した脆弱性のほとんどは Brakeman では検出できません。 そのため、実際のコードレビューでは Brakeman と目視によるレビューの両方で相互補完するのが効果的です。 Ruby on Rails セキュリティガイド 脆弱性がコードレビューで見つかるのはとても良いことですが、そもそも作りこまないことも重要です。脆弱性を作りこみにくい、安全にコーディングすることをセキュアコーディングといいます。 Rails セキュリティガイド にはWebアプリケーションの脆弱性とRailsにおける対策方法が満載です。セキュアコーディングの実践に大いに役に立つでしょう。 セキュリティコードレビュールールの改善 本記事で紹介したルールはあくまで例なので、アプリケーションによっては検索にマッチしすぎたり、逆に見つけられなかったりすることもあります。 Rails セキュリティガイドを参考にしたり、過去に発覚した脆弱性をもとにルールを追加・変更していきましょう。 本記事のルールを改善する良いアイデアがあったらぜひ教えてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails herokuのエラー:Failed to install gems via Bundler. の対応

ポートフォリオ作成の際に、詰まったのでメモ bundle installしても解決しない場合です。 環境 ruby 2.6.6 Rails 6.0.3 経緯 $ heroku create 後に $ git push heroku main そこでエラー発生 . . remote: ! remote: ! Failed to install gems via Bundler. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed . . 解決方法 エラー文を読むと、bundlerに問題あるとのこと heroku公式サイトからbundlerの記事を検索してみる。 Applications specifying Bundler 2.x in their Gemfile.lock will receive bundler: 2.2.15 Applications specifying Bundler 1.x in their Gemfile.lock will receive bundler: 1.17.3 Applications with no BUNDLED WITH in their Gemfile.lock will default to bundler: 1.17.3 https://devcenter.heroku.com/articles/ruby-support#supported-runtimes 2021年 4月現在 今のbundlerのversionはいくつかな? $ bundler -v Bundler version 2.x.xx ふむ、この場合 Applications specifying Bundler 2.x in their Gemfile.lock will receive bundler: 2.2.15 とあるので、2.2.15にしないといけないらしい。 bundlerのversionを設定し直す まずアンインストール $ gem uninstall bundler version指定して再インストール $ gem install bundler --version '2.2.15' gemfile.lockを消去 (あとでbundle installして最新のものを作成) $ rm gemfile.lock version 2.2.15で、bundle install $ bundle _2.2.15_ install git add, commit して再デプロイ $ git push heroku 通りました。 これでも、解消しない場合、エラー文をよく読むと . . remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. . . とあり、 $ bundle lock --add-platform x86_64-linux で解決することもあるそうです。 ついで $ heroku create NAME NAME 部分に好き文字を付けることができますが、付けない場合はランダムになります。 もし後からつけたい場合、 $ heroku app:rename NAME で簡単に書き換えられます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails 環境構築メモ

背景 久しぶりにRailsの環境構築したので自分用のメモです。 基本の流れ ・下記記事を参考に進めさせて頂きました。 【完全版】MacでRails環境構築する手順の全て - Qiita ※Bundlerは2.010以前をインストールする(bundler 2.0.1以降をインストールしているとYou must use Bundler 2 or greater with this lockfile.という厄介なエラーに遭遇することが多々あるため) ・Bundlerのバージョンを変更する場合 Ruby | bundler を特定のバージョンに切り替えて実行する - Qiita ・rbenvでインストールするrubyのバージョンを変更する場合 rbenvでrubyのバージョンを管理する - Qiita エラー解決 Gemをインストール時 undefined method `invoke_with_build_args' for nil:NilClass というエラーが出る場合 ・下記記事を参考に解決。 Yosemiteに変えたらgem installできなくなった件 - Qiita ・opensslをうまく参照できていないため上記のようなエラーが発生する。 上記記事内でrbenvを再インストールするよう指示があるが、opensslを参照できていない場合opensslのインストールで止まってしまう。その場合は下記記事を参考にパスを貼り直す必要がある。 rbenv installがopensslで失敗する - Qiita ・rbenvのインストールは遅いため最大で10分以上かかる可能性がある。 全然進まなくてもエラーではないため注意 rbenv install 遅くて痺れを切らした話 - Qiita DBにMySQLを選択し、bundle install時 ld: library not found for -lssl , Make sure that gem install mysql2 -v 〇〇 というエラーが出る場合 ・下記記事を参照 bundle installでmysql2がエラーになる件 - Qiita サーバー起動時に、Please run rails webpacker:install Error: No such file or directory @ rb_sysopenというエラーが出る場合 ・webpackerがインストールできていないのが原因 yarnにpathを通し、bundle exec rails webpacker:installすることで解決 サーバー起動時に、Unknown database 'pet_reserve_development'というエラーが出る場合 ・そもそもDBが存在しないのが原因 MySQLクライアントで作成するよりrails db:createで作成するほうがラク 以上になります。記事を書いてくださった皆さんありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MVCの基本がまだ頭に入っていませんでした:uninitialized constantのエラー

何度ファイルの修正、ルートの確認をしても以下のエラーが出ていました。 基本に立ち返り items/new.html.erbを見直し、items_contoroller.rbを見直しやっていましたが、何度やっても items/new.html.erbが表示されません。 uninitialized constantで調べ、 @imotan さんの記事で uninitialized constantのエラーをどう解決すべきか の記事に辿り着きました。@imotanありがとうござます。気づかせていただき感謝!! Error文 uninitialized constantは「定義したクラスを読むことができない」といっている。 でももっと重要なのは rails sした直後は実行時点でクラスを読み込んでいないので、app/models/user.rbファイルを探しにいく。app/models/user.rbというファイルがあれば、ファイルをオープンしてクラスを読み込む.しかし、app/models/user.rbというファイルがない、ファイルの中にUserというクラスが定義されていない場合、uninitializedエラーが起きる。 気づき modelを読み込む!!!→itemのテーブルもmodelもないじゃないですか!!!! 解決方法1 uninitiali#zed constantのエラーが出たら やるべきこと ①関連テーブルを確認 ②関連モデルを確認 ③関連マイグレーションファイルを確認 MVCに常に返るって大事。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails初学者によるRailsチュートリアル学習記録② 第0章

目次 1. はじめに 2. これまでの学習内容 3. Railsチュートリアルを読了して 1. はじめに この記事は、Rails初学者の工業大学三年生がRailsチュートリアルの学習記録をつけるための記事です。 筆者自体がRailsやWebについて知識が少ないので、内容の解釈などに間違いがある可能性があります。(その時はコメントで指摘してくださると助かります!) 2. これまでの学習内容 前回の記事に引き続き、Railsチュートリアルの第8章から第14章の内容を、 キーワードレベルで見返せるように記録しておきます。 ▶第8章 -ログイン機能を作成する- cookiesを使ってセッション機能を作成する ログインフォームを作成する フォームへの入力値からユーザを検索する ログインの成功、失敗に応じてメッセージを表示する ログアウト機能を作成する ▶第9章 -ユーザのログイン情報を記憶する- 記憶トークンを生成し暗号化する 記憶しているユーザ情報を削除する ▶第10章 -ユーザの更新・表示・削除- ユーザ情報の更新 情報更新時のバリデーション 更新の成功、失敗に応じた処理 ユーザが可能な操作の管理 ログインしてないユーザが実行できる操作の設定 ログインしているユーザが実行できる操作の設定 ユーザを表示する ユーザ一覧のページネーション ユーザを削除する ▶第11章 -新規登録時にアカウントの有効化を要求する- ユーザの初期状態を有効化されていない設定をする ヘルパー(組み込み・カスタム) ユーザ登録時にメールを送信する メイラーの使い方 ▶第12章 -パスワードを再設定できるようにする- パスワード再設定のメールを送信できるようにする データベース内のパスワードを更新する アセットパイプラインとは 名前付きルートの使い方 統合テストの作り方 ▶第13章 -メッセージの投稿機能の作成- 投稿内容のバリデーション 投稿とユーザの関連付け 投稿の表示 画像の投稿 ▶第14章 -ユーザーのフォロー機能の作成- フォローしている、フォローされているという関係をデータモデルで表現する 関係性をユーザと関連付ける フォローしているユーザの投稿を表示させる 3. Railsチュートリアルを読了して 学習の流れとして、まず一度読んでから実際に手を動かすという流れを考えていたが、 一通り読んだだけでもこの教材のボリュームを実感できました。 特にこの記事でメモした8章以降の内容は実装する機能がより複雑な物となっており、 データモデルの考え方やテストといったprogateなどの教材では学ぶことのなかった内容は、 読んでいてほとんど理解することができませんでした。 これから実際に手を動かして学習を進めていきますが、学習スピードを保ちながら進められるか非常に不安です:(
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

carrierwave+Minimagickで利用する、画像リサイズメソッド

はじめに ポートフォリオの一覧ページを作成する際に、サイズが違うときれいに並ばないので画像のサイズを投稿時に変更するようにしました。 その際、使うメソッドを迷ったので書いておきます。 種類は4つです。 ・ resize_to_fit ・ resize_to_limit ・ resize_to_fill ・ resize_and_pad minimagickを導入 まず最初にminimagickを利用するには、imagemagickというパッケージをインストールする必要があります。 dockerのalpineを使っているので、apkでインストールします。 Mac使っている人はbrewでインストールしてください。 コンソール apk add imagemagick 続いて、Gemfileにminimagickを記載し、bundle installを実行します Gemfile gem 'minimagick', '~> 4.0' 最後にuploader.rbのinclude CarrierWave::MiniMagickのコメントを外す app/uploaders/image_uploader.rb class ImageurlUploader < CarrierWave::Uploader::Base # Include RMagick or MiniMagick support: # include CarrierWave::RMagick include CarrierWave::MiniMagick # ←コメントを外す  (省略) end これで画像をリサイズすることができるようになります。 resize_to_fit これが基本的なメソッドです。 縦横比を保持しながら、引数で指定したサイズを変更します app/uploaders/image_uploader.rb process resize_to_fit: [width,height] resize_to_limit 縦横比を保持しながら、引数で指定したサイズに収まるようにサイズを変更します。 ただし、引数よりサイズが小さい場合はサイズを変更しない。 app/uploaders/image_uploader.rb process resize_to_limit: [width,height] resize_to_fill 元画像から第3引数を中心として第1・2引数ののサイズに切り抜きを行う。 縦横比は変更される。 app/uploaders/image_uploader.rb process resize_to: [width,height,gravity] #(gravity: デフォルトは 'Center'; オプション: 'NorthWest'、 'North'、 'NorthEast'、 'West'、 'C​​enter'、 'East'、 'SouthWest'、 'South '、'SouthEast') resize_and_pad 縦横比を保持しながら、引数で指定したサイズに収まるように画像のサイズを変更する。 余白部分の第三引数の色で塗りつぶす app/uploaders/image_uploader.rb process resize_to: [width,height,background,gravity] #(background: 'ffffff'のように16進コードで指定) #(デフォルトはgif・pngの場合は透明、jpegは白) 参考 Github Carrirwave Github Minimagick 紹介したメソッドのソースコード
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】左上の邪魔な時間( ms )の表示を消す or デフォルト非表示 or 移動する方法と手順まとめ

ローカルでrailsを起動した時に左上に表示される数値msが邪魔でデザインが見えない場合がある。 この表示を非表示にする方法について。 目次 URLに?pp=disableをつける デフォルト非表示にする 表示位置を変更する rack-mini-profilerをアンインストールする 1. URLに?pp=disableをつける 最も簡単な方法としては、URLの末尾に?pp=disableをつけることで非表示にすることができる。 セッションが切れるまで有効となる。 例 http://localhost:3000/?pp=disable 復活させたい時は?pp=enableをつけてリロードする。 2. デフォルト非表示にする configを設定することでデフォルト非表示にすることができる。 config > initializers 配下に rack_profile.rb を作成する。(ファイル名はなんでもいい) rack_profile.rb Rack::MiniProfiler.config.start_hidden = true プロジェクトを停止してrails sで再起動すれば反映される。 3. 表示位置を変更する configを設定することで表示位置を変更することができる。 ▼例 右下表示に変更 実施手順 config > initializers 配下に rack_profile.rb を作成する。(ファイル名はなんでもいい) rack_profile.rb Rack::MiniProfiler.config.position = 'bottom-right' ▼positionの指定 値 位置 備考 top-left 左上 デフォルト top-right 右上 bottom-left 左下 bottom-right 右下 プロジェクトを停止してrails sで再起動すれば反映される。 4. rack-mini-profilerをアンインストールする この秒数表示はrack-mini-profilerというgemの機能。なので、アンインストールすれば表示がなくなる。 GemfileでGemをコメントアウトする Gemfile group :development do gem 'web-console', '>= 4.1.0' # gem 'rack-mini-profiler', '~> 2.0' gem 'listen', '~> 3.3' end bundle installを実行する。 rack-mini-profilerはSQLクエリのデバッグなど使う場面も多いため、アンインストールする方法はおすすめしない。 参考リンク https://github.com/MiniProfiler/rack-mini-profiler#configuration-options https://stackoverflow.com/questions/65589200/how-to-remove-the-top-left-timer-counter-in-react-web-app
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】i18nとは?使い方まとめ。ビュー(View),コントローラー(Controller), コンソールでの呼び出し方とlocaleの使い方

Railsのi18nを使うと、日本語のテキストを一つのファイルにまとめて、呼び出したい文言を共通化して呼び出すことができる。 また、railsの言語設定を変更することでエラーメッセージの変更もできる。 目次 i18nとは? i18nの使い方 config/application.rbに以下を追記する 言語ファイルの作成 呼び出し コントローラで表示する方法 ビューで表示する方法 railsコンソールで表示する方法 日付と時刻を呼び出す方法 変数を渡す方法 htmlとして呼び出す方法 ファイルを分割する方法 localeファイルのディレクトリパスを変更する方法 i18nとは? APIの一つで、Internationalizationの略。iとnの間に18文字あるので、i18nとなっている。 意味は国際化・多言語化。例えば英語用の言語ファイルと日本語用の言語ファイルを用意しておいて、i18nを使って言語を切り替えれば、ビューに表示される言語を切り替えることができる。 日付や時刻のフォーマット変更ができる。言語ファイルの中で数式を使って記述することも可能。 i18nの使い方 i18nを使得るようにするのはとても簡単。 1. config/application.rbに以下を追記する config/application.rb module RailsScreamer class Application < Rails::Application #追記 config.i18n.default_locale = :ja end end ・config.i18n.default_locale = :ファイル名 ファイル名はconfig/locale配下で参照したいファイルの名前を入力する。(拡張子 .yml は省略) 2. 言語ファイルの作成 config/locales/ja.yml を作成する。ja:で書き出して、キー名: 文字列の形で記述していく。 ja ja: test: テスト deep: test: 深いテスト 3. 呼び出し 作成したテキストを呼び出すには、ビューとコントローラの中ではt('キー名')を使う。 railsコンソールではI18n.t 'キー名'を使う。 カッコをつけても機能する: t('キー名') 3-1. コントローラで表示する方法 デバッグ用のppを使うと便利。localeの指定は`t('キー名.キー名...')で指定。 controller class TopController < ApplicationController def index pp t 'test' pp t 'deep.test' end end ▼注意点 ja.ymlに値が存在しない場合は、translation missing: 入力した値が表示される。 controller pp t 'dummy' pp t 'xxx.存在しない' 3-2. ビューで表示する方法 <%= t 'キー名' %>を使う。 .html.erb <%= t 'test' %> <%= t 'deep.test' %> ▼注意点 ja.ymlに値が存在しない場合は、入力した値が表示される。 <%= t 'dummy' %> <%= t 'yyy.存在しない' %> 3-3. railsコンソールで表示する方法 I18n.t 'キー名'で呼び出すことができる。 # rails c Running via Spring preloader in process 197 Loading development environment (Rails 6.1.3.1) irb(main):002:0> I18n.t 'test' => "テスト" irb(main):001:0> I18n.t 'deep.test' => "深いテスト" ▼注意点 ja.ymlに値が存在しない場合は、translation missing: 入力した値が表示される。 変更内容はリアルタイムで反映されないため、後からlocaleのja.ymlを変更した場合は、一旦railsコンソールをexitで抜けて、rails cで再度対話モードに入る必要がある。 4. 日付と時刻を呼び出す方法 ja.ymlの中でdateとtimeを定義すると現在の日付や時刻を呼び出せるようになる。(定義せずに呼び出そうとするとエラーになる) 4-1. ja.ymlの編集 formatsの中に、defaultと任意の名前のフォーマットを追加する。 ja.yml ja: date: formats: default: "%Y/%m/%d" long: "%Y年%m月%d日(%a)" short: "%m/%d" day_names: [日曜日, 月曜日, 火曜日, 水曜日, 木曜日, 金曜日, 土曜日] abbr_day_names: [日, 月, 火, 水, 木, 金, 土] month_names: [~, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] abbr_month_names: [~, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] time: formats: default: "%Y/%m/%d %H:%M:%S" long: "%Y年%m月%d日(%a) %H時%M分%S秒 %z" short: "%y/%m/%d %H:%M" am: "午前" pm: "午後" day_namesやabbr_day_namesは曜日を表す%aなど日本語の曜日や月を表示するときに必須。 これらがないとエラーになるので注意。 エラー例 #"%Y年%m月%d日(%a)"を呼び出そうとした "translation missing: ja.date.abbr_day_names" 4-2. 呼び出し コントローラやビューの中で呼び出す場合は以下となる。 ・l Date.today, format: :フォーマット名 ・l Time.now, format: :フォーマット名 ▼defaultを呼び出す場合 defaultを呼び出す場合は, format以下を省略できる。 ・l Date.today ・l Time.now コントローラでの呼び出し例 controller class TopController < ApplicationController def index pp l Date.today pp l Date.today, format: :long pp l Date.today, format: :short pp l Time.now pp l Time.now, format: :long pp l Time.now, format: :short end end railsサーバーを起動しているコンソールにlogとして出力される。 ビューでの呼び出し例 .html.erb <%= l Date.today %><br> <%= l Date.today, format: :long %><br> <%= l Date.today, format: :short %><br> <%= l Time.now %><br> <%= l Time.now, format: :long %><br> <%= l Time.now, format: :short %><br> railsコンソールでの呼び出し ・I18n.l Date.メソッド ・I18n.l Time.メソッド $ rails c Running via Spring preloader in process 93 Loading development environment (Rails 6.1.3.1) irb(main):001:0> I18n.l Date.today => "2021/04/14" irb(main):002:0> I18n.l Date.today, format: :long => "2021年04月14日(水)" irb(main):003:0> I18n.l Date.today, format: :short => "04/14" 5. 変数を渡す方法 呼び出し時に変数名と値を指定することで、代入した文字列を抽出できる。 ▼ビューやコントローラの記述 ・t 'キー名', 変数名: 数値 ・t 'キー名', 変数名: :文字列 ※文字列を渡す場合は冒頭に:をつける。 ▼ja.ymlの記述 ・キー名: "文字列%{変数名}文字列" ja.yml ja: product: "%{price}円" product2: price: "%{price}円" .html.erb <%= t 'product', price: 543 %> <br> <%= t 'product2.price', price: 543 %> 6. htmlとして呼び出す方法 キー名がhtmlまたは、_htmlで終わる場合はhtmlとして呼び出せる。 ja.yml ja: test: html: "<p>pタグで囲まれている</p>" div_html: "<div>divタグで囲まれている</div>" tag_html: "<%{tag}>%{tag}タグで囲まれている</%{tag}>" .html.erb <%= t 'test.html' %> <%= t 'div_html' %> <%= t 'tag_html', tag: :b %> 7. ファイルを分割する方法 locale配下で.ja.ymlを末尾に付ければ好きなファイルを作成することができる。 ▼注意点 ファイル毎にキー名は区別できない キー名が被った場合は後から読み込まれたファイルの値で上書きする 実例 例えば以下の3つのファイルを作成したとする。並び順はアルファベット順で自動整列。 ┝ locales  ┝ ja.yml  ┝ product.ja.yml  ┝ test.ja.yml ja.yml ja: test: "ja.ymlのtestです" test1: "ja.yaml専用のtest1" test.ja.yml ja: test: "test.ja.yamlのtestです" test2: "test.ja.yaml専用のtest2" product.ja.yml ja: test: "product.ja.ymlのtestです" test3: "product.ja.yaml専用のtest3" ▼ビューの設定 <%= t 'test' %> <br> <%= t 'test1' %> <br> <%= t 'test2' %> <br> <%= t 'test3' %> <br> 8. localeファイルのディレクトリパスを変更する方法 localeディレクトリ以外の.ymlファイルも読み込みたい場合は、config/application.rbに設定を追記する。 ・config.i18n.load_path += Dir[Rails.root.join('パス', 'locales', '*.{rb,yml}')] 上記設定であれば、パス > localse 配下のrbまたはymlファイルをlocaleファイルとして読み込む。 config.i18n.default_localeの前に記述すること。 application.rb #追記 config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] config.i18n.default_locale = :ja 関連リンク
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseを使用したuserモデルバリデーションチェックのRSpec初コーディング(後編)

バリデーションもかかってやっと後半に向かえました。 目的 ・氏名(漢字)の半角登録禁止バリデーションとテスト ・氏名カナの半角カタカナ禁止バリデーションとテスト ・メールアドレスの重複チェック ・@のないアドレスチェックテスト *ユーザー登録の終わりは見えてきました。 これまでの経過 ・RSpecインストール ・FactoryBotインストール ・Fakerインストール ・Gimeiインストール ・異常系テスト実装「〜can't be blank」 ・6文字混合パスワード設定(Faker) ・カタカナ・漢字ダミーデータ設定(Gimei) ・パスワード・カタカナ漢字全角設定のための正規表現の学習 ・バリデーションのまとめかた with_options ・追加でvalidatableについて Ruby on rails モデル単体テストでのメールアドレス二重登録テストについて https://qiita.com/takuo_maeda/items/ae15efd9adcd38d7d323 目的1:半角カタカナ文字変換 Gimeiを使って全角カタカナを作った後で Rubyでの半角全角の変換はNKFで行います。 NKFとは Linuxの基本的なコマンド、「nkf」は「Network Kanji Filter」の略です。 オプション 意味 -w UTF-8コードを出力する(BOMなし) -Z4 半角カナ文字を渡した場合に全角カナ文字/半角カタカナを渡した場合その逆に さらに-xオプションを追加すると半角カナしか出力されないようにできます。 terminal NFK.nfk("-w -x -Z4", "アナスタシア") => "アナスタシア" 参考:なえ様 https://310nae.com/text-conversion/ メールアドレスの一意性 すでに登録のあるメールアドレスは新規登録用には使えない仕様にすること。 私がQiitaに掲載した初記事です。よかったら https://qiita.com/takuo_maeda/items/ae15efd9adcd38d7d323 spec/models/user_spec.rb it '同じメールアドレスを登録できないこと' do user1 = FactoryBot.create(:user) @user.email = user1.email @user.valid? expect(@user.errors.full_messages).to include("Email has already been taken") end FactoryBot.createでいったん作っただみデータを保存。 その後、user1.emailで先ほど作っただみデータを呼び出すだけです。 *基本のエラーメッセージは"Email has already been taken"です。 @のないメールははじく spec/models/user_spec.rb it '@のないメールアドレスを登録できないこと' do @user.email = Faker::Lorem.characters(number: 10, min_alpha: 10) @user.valid? expect(@user.errors.full_messages).to include("Email is invalid") end Fakerで文字列を作るだけでOKです。 *基本のエラーメッセージは"Email is invalid"です。 前半後半と長いことお付き合いいただきまして誠にありがとうございました。 書いたバリデーションとテストです。 app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable with_options presence: true do validates :nickname, :birthday, :password_confirmation validates :first_name, :last_name, format: { with: /\A[ぁ-んァ-ン一-龥々]/, message: "は全角ひらがな、全角カタカナ、漢字で入力して下さい" } validates :last_name_prono, :first_name_prono, format: { with: /\A[ァ-ヶー-]+\z/, message: "は全角カタカナで入力して下さい" } end validates :password, :password_confirmation, format: { with: /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i, message: "は半角英数で入力して下さい" } end spec/factories/users.rb FactoryBot.define do factory :user do nickname {Faker::Name.last_name} email {Faker::Internet.free_email} password {Faker::Lorem.characters(number: 6, min_alpha: 1, min_numeric: 1) } password_confirmation {password} first_name { Gimei.first.kanji } last_name { Gimei.last.kanji } first_name_prono { Gimei.first.katakana } last_name_prono { Gimei.last.katakana } birthday { Faker::Date.backward } # password_short {Faker::Lorem.characters(number: 5, min_alpha: 1, min_numeric: 1) } # password_alpha {Faker::Lorem.Alpanameric.alpa(number: 6) } # password_number {Faker::Lorem.Number(6) } end end spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do describe '#create' do before do @user = FactoryBot.build(:user) end it 'nickame,email,password,password_comfirmation,first_name,last_name,first_name_prono,last_name_prono,birthdayの値が存在すれば登録できること' do expect(@user).to be_valid end it 'nicknameが空では登録できないこと' do @user.nickname = '' @user.valid? expect(@user.errors.full_messages).to include("Nickname can't be blank") end it 'emailが空では登録できないこと' do @user.email = '' @user.valid? expect(@user.errors.full_messages).to include("Email can't be blank") end it '同じメールアドレスを登録できないこと' do user1 = FactoryBot.create(:user) @user.email = user1.email @user.valid? expect(@user.errors.full_messages).to include("Email has already been taken") end it '@のないメールアドレスを登録できないこと' do @user.email = Faker::Lorem.characters(number: 10, min_alpha: 10) @user.valid? expect(@user.errors.full_messages).to include("Email is invalid") end it 'paswordが空では登録できないこと' do @user.password = '' @user.valid? expect(@user.errors.full_messages).to include("Password can't be blank", "Password confirmation doesn't match Password", "Password は半角英数で入力して下さい") end it 'paswordが文字数5文字では登録できないこと' do @user.password = Faker::Lorem.characters(number: 5, min_alpha: 1, min_numeric: 1) @user.valid? expect(@user.errors.full_messages).to include("Password is too short (minimum is 6 characters)") end it 'paswordが半角アルファベットでは登録できないこと' do @user.password = Faker::Lorem.characters(number: 6, min_alpha: 6) @user.valid? expect(@user.errors.full_messages).to include("Password は半角英数で入力して下さい") end it 'paswordが数字のみでは登録できないこと' do @user.password = Faker::Lorem.characters(number: 6, min_numeric:6) @user.valid? expect(@user.errors.full_messages).to include("Password は半角英数で入力して下さい") end it 'paswordが全角英数のみでは登録できないこと' do password_zen = Faker::Lorem.characters(number: 1, min_numeric:1) require 'nkf' password_zen.tr("A-Z0-9","A-Z0-9") @user.password = password_zen @user.valid? expect(@user.errors.full_messages).to include("Password は半角英数で入力して下さい") end it 'pasword_confirmationが空では登録できないこと' do @user.password_confirmation = '' @user.valid? expect(@user.errors.full_messages).to include("Password confirmation は半角英数で入力して下さい") end it 'passworとpasword_confirmationが一致しないと登録できないこと' do @user.password = Faker::Lorem.characters(number: 7, min_alpha: 3, min_numeric: 1) @user.password_confirmation = Faker::Lorem.characters(number: 6, min_alpha: 3, min_numeric: 2) @user.valid? expect(@user.errors.full_messages).to include("Password confirmation doesn't match Password") end it 'pasword_confirmationが文字数5文字では登録できないこと' do @user.password_confirmation = Faker::Lorem.characters(number: 5, min_alpha: 1, min_numeric: 1) @user.valid? expect(@user.errors.full_messages).to include("Password confirmation doesn't match Password") end it 'first_nameが空では登録できないこと' do @user.first_name = '' @user.valid? expect(@user.errors.full_messages).to include("First name can't be blank") end it 'last_nameが空では登録できないこと' do @user.last_name = '' @user.valid? expect(@user.errors.full_messages).to include("Last name can't be blank") end it 'first_nameが半角では登録できないこと' do @user.first_name = Faker::Alphanumeric.alphanumeric(number: 4) @user.valid? expect(@user.errors.full_messages).to include("First name は全角ひらがな、全角カタカナ、漢字で入力して下さい") end it 'last_nameが半角では登録できないこと' do @user.last_name = Faker::Alphanumeric.alphanumeric(number: 4) @user.valid? expect(@user.errors.full_messages).to include("Last name は全角ひらがな、全角カタカナ、漢字で入力して下さい") end it 'first_name_pronoが空では登録できないこと' do @user.first_name_prono = '' @user.valid? expect(@user.errors.full_messages).to include("First name prono can't be blank") end it 'last_name_pronoが空では登録できないこと' do @user.last_name_prono = '' @user.valid? expect(@user.errors.full_messages).to include("Last name prono can't be blank") end it 'first_name_pronoが半角では登録できないこと' do require 'nkf' first_half_kana = Gimei.first.katakana @user.first_name_prono = NKF.nkf('-w -Z4 -x', first_half_kana) @user.valid? expect(@user.errors.full_messages).to include("First name prono は全角カタカナで入力して下さい") end it 'last_name_pronoが半角では登録できないこと' do require 'nkf' last_half_kana = Gimei.last.katakana @user.first_name_prono = NKF.nkf('-w -Z4 -x', last_half_kana) @user.last_name_prono = '' @user.valid? expect(@user.errors.full_messages).to include("Last name prono は全角カタカナで入力して下さい") end it 'birthdayが空では登録できないこと' do @user.birthday = '' @user.valid? expect(@user.errors.full_messages).to include("Birthday can't be blank") end end end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails モデル作成時に関連付け(1対多の場合)

※ 関連付け(アソシエーション)とは テーブル間の関係を、モデル上の関係として操作できるようにする事。 【モデル作成時に関連付けを設定する方法】 例として、1人のユーザー(Userモデル)が多くのつぶやき(Tweetモデル)を作成する場合。 ※Userモデルはすでに存在しているものとします。 1. Modelの作成 $ rails g model Tweet content:string user:references user:referencesの指定で、Tweetモデルにuser_idカラムが作成されます。 ※references=参照 2. 作成されたマイグレーションファイルの確認 20210414_create_tweets.rb class CreateTweets < ActiveRecord::Migration[5.2] def change create_table :tweets do |t| t.string :content t.references :user, foreign_key: true # foreign_key: true ? 外部キー制約 t.timestamps end end end ※外部キー制約:  1. Usersテーブルに存在しない値を外部キーとして登録不可  2. Tweetsテーブルの外部キーに値が登録されているUsersテーブルのレコードは削除不可 3. マイグレーション実行 rails db:migrate 4. shema.rbで確認 app/db/schema.rb create_table "tweets", tweets: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.string "content" t.bigint "user_id" # reference型でuser_idカラム生成 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["user_id"], name: "index_opinions_on_user_id" end 5. Tweetモデルを確認・Userモデルを編集 app/models/tweet.rb class Tweet < ApplicationRecord belongs_to :user end 作成されたtweet.rbにはuserに属する(belongs_to)が記述されます。 これに対して、Userモデルにtweetを所有する(has_many)を追記します。 app/models/user.rb class User < ApplicationRecord has_many :tweets, dependent: :destroy # 多数のつぶやきを持つ為、tweetsと複数形にする end ※dependent: :destroyは、先に書いた外部キー制約でエラーが出ない為の設定。  これにより、Userが削除された場合、属するtweetsも削除されます。 以上の設定で、User has many Tweets、Tweet belongs to userの関連付けができます。 関連付けしていない・している場合の例 ユーザーのつぶやきを取得 ・関連付け無し場合 @user = User.find(params[:id]) Tweets = Tweet.where(user_id: @user.id) ・関連付けした場合 @user = User.find(params[:id]) Tweets = @user.tweets 省略できますね!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】PDFをダウンロードできる状態で投稿する方法

はじめに 様々なファイルを投稿するとき、どうしたらいいのかわからなかったので、画像投稿を参照してPDFを投稿してみました。 はじめる前に:Cloudinaryに登録 このリンクから登録をはじめましょう。 https://cloudinary.com/ 右上のsign up for freeを押すと、登録画面に飛びます。 手順 Step1 ファイル投稿機能のためにMVCを準備 適当なファイルを使って、作っていきます。 今回は、Topicモデルを使っていきます。 Topicsテーブルにカラムを追加 今回はファイルだけを投稿するためのアプリケーションなので、pdfカラムをString型で追加します。 (ファイルを保存するときにString型で指定するのはそういうものだと思っちゃいましょう) 今回はTopicsテーブルにstring型のpdfカラムを追加するので、 ターミナル rails generate migration AddPdfToTopics pdf:string と打ち込んで実行します。 すると、以下のようなマイグレーションファイルがタイムスタンプ付きで自動生成されます。 (「××××××××××」の中には各人がモデルを作成した日付が記述されています。) db/migrate/20××××××××××_create_topics.rb class AddPdfToTopics < ActiveRecord::Migration[6.0] def change add_column :topics, :pdf, :string end end 書き込んだら、ターミナルでmigrateします。ターミナルで以下の操作を行いましょう。 $ rails db:migrate これで、Topicモデルに、pdfという名前の、string型のカラムが追加できました。 Viewの作成 まず、index.html.erbに以下のように追加で記述してください。 app/views/topics/index.html.erb <% @topics.each do |t| %> <!--ここから--> <a href="<%= image_url t.pdf_url %>" target="_blank"> <object data="<%= image_url t.pdf_url %>" type="application/pdf"></object> </a> <!--ここまで--> <%= t.body %> <%= t.created_at %> </div> <% end %> </div> 実はデータベースにはファイルは保存されておらず、代わりにファイルが置かれる住所が保存されます。なのでt.image_urlのような書き方になります。 次にtopics controllerの一番下に以下のような記述があると思うので、それに「:pdf」の記述を付け加えましょう。 topics_controller.rb # 割愛 private def topic_params params.require(:topic).permit(:body, :pdf) end これをしておかないと、pdfのパラメータが取得できず、投稿ができません。 またnew.html.erbに以下を追加で記述してください。 app/views/topics/new.html.erb <%= form_for @topic do |f| %> <div class="field"> <%= f.label :body %> <%= f.text_field :body, :size => 140 %> </div> <!--ここから--> <div class="field"> <%= f.label :pdf %> <%= f.file_field :pdf %> </div> <!--ここまで--> <%= f.submit "投稿" %> <% end %> これはファイルを投稿するためのボタンを実装してくれるためのコードです。 file_fieldの場合は、以下の図のようなformが作られます。 ここにユーザーが投稿したいファイルを投稿することになります。 これで下準備は終わりました。次のStepからは実際のファイル投稿機能を実装していこうと思います。 Step2. ファイル投稿機能 早速ここからファイル投稿機能を導入していこうと思います。 gemの追加 今回はcloudinary、carrierwave、dotenv-railsというgemを使っていきます。Gemfileの一番下に以下を追加しましょう。 Gemfile gem 'carrierwave' , '~> 1.3' , '>= 1.3.1' gem 'cloudinary' gem 'dotenv-rails' そしたらターミナルで ターミナル $ bundle install と打ちこんでインストールします。 アップローダーの作成 CarrierWaveのジェネレーターでアップローダーを作成します。以下のコマンドをターミナルに打ち込みましょう。 ターミナル $ rails g uploader Pdf uplpder の後ろは、先程追加したカラム名を入れてください。 モデルの修正 Topicモデルを以下のように修正します。 app/models/topic.rb class Topic < ApplicationRecord # 追記ここから mount_uploader :pdf, PdfUploader # 追記ここまで end mount_uploader :pdf, PdfUploaderはファイルを指定の場所に保存することを表します。 次に保存場所を指定するアップローダの設定です。 app/uploaders/pdf_uploader.rbの6~8行目を変更しましょう。 app/uploaders/pdf_uploader.rb # Choose what kind of storage to use for this uploader: storage :file # storage :fog 上記を下図のように変えます。 app/uploader/pdf_uploader.rb # Choose what kind of storage to use for this uploader: if Rails.env.production? include Cloudinary::CarrierWave CarrierWave.configure do |config| config.cache_storage = :file end else storage :file end # storage :fog cloudinaryは外部のストレージサービスです。本番環境(リリース後=production)ではcloudinaryにファイルが保存され、それ以外(開発環境=ローカル)では自分のpcに保存されます。(publicフォルダー内に保存されます) APIキーの非公開 Cloudinaryの各アカウントには「Cloud name」、「API Key」、「API Secret」というアカウント固有の秘密のIDのようなものが付与されています。 この情報は非常に機密性が高く決して外部に漏らしてはいけない情報なので、これらの値が間違って公開されないような処理を施す必要があります。 のでこの節ではWebサービスを公開した時にそれらの値を公開しないようなコードを書いていくことにします。 三つの値は以下から確認することができます。 Cloudinaryマイページ .envというファイルをアプリケーションディレクトリ(appやdbやGemfileがあるディレクトリ)に自分で作成します。 以下の図のようになっていればオッケーです。 ※vendorフォルダーの中ではないので注意してください。 次に作成した.envファイルに以下を入力します。 .env CLOUD_NAME=q0w9e8r7t6yu5 #←この値は人によって違います!! CLOUDINARY_API_KEY=123456789012345 #←この値は人によって違います!! CLOUDINARY_API_SECRET=1a2s3d4f5g6h7j8k9l0a1s2d4f5g6h1q #←この値は人によって違います!! ここで「=」の後のそれぞれの値は先ほどのCloudinaryのマイページで取得したキーに書き換えておいてください(自分が取得したキーは絶対他言しないように!!また数字は個々人によって変わります)。 また、書き換える際は、コメントアウト部分を削除してください!  最後に隠しておきたいデータを定義した.envファイルをみんなに公開してしまっては意味が無いのでこのファイルは公開しないようにします。アプリケーションディレクトリにある.gitignoreに下記を追加します。 ※もし.gitignoreファイルがない場合はアプリケーションディレクトリーにて作りましょう! .gitignore # 省略 /.env これでOKです! APIキーの利用 最後の手順です! まずはconfigフォルダにcloudinary.ymlファイルを作成してください。 config/cloudinary.ymlに以下のようにそのままコピペしてください。 config/cloudinary.yml development: cloud_name: <%= ENV['CLOUD_NAME'] %> api_key: <%= ENV['CLOUDINARY_API_KEY'] %> api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %> enhance_image_tag: true static_file_support: false production: cloud_name: <%= ENV['CLOUD_NAME'] %> api_key: <%= ENV['CLOUDINARY_API_KEY'] %> api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %> enhance_image_tag: true static_file_support: false test: cloud_name: <%= ENV['CLOUD_NAME'] %> api_key: <%= ENV['CLOUDINARY_API_KEY'] %> api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %> enhance_image_tag: true static_file_support: false これでAPIキーを非公開にしたまま利用することができました。 サーバー立ち上げ 以上でファイル投稿機能が終わりました! サーバーを立ち上げて(rails s) localhost:3000/topics/newでファイルが投稿できることを確かめてみてください。 PDFのダウンロード PDFをダウンロードするときは、表示されているファイルを右クリックして、保存することができます! 公式ドキュメント carrierwave cloudinary dotenv-rails
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

translation missing: ja~ 系のエラー。日本語、英語どちらも対応させる。

前提知識 なぜこのエラーが出たのか ja.yml内に、該当する値が存在しないから 以前Fakerを日本語化したため、Railsアプリのデフォルトが日本語になっていた。 application.rb config.i18n.available_locales = %i[ja en] config.i18n.default_locale = :ja ↓ Rspecを行った。 spec/models./post_spec.rb it 'is invalid without a title' do post = Post.new() post.valid? expect(post.errors.messages[:title]).to include('can`t be blank') end ↓ エラー発生 Failure/Error: expect(post.errors.messages[:title]).to include('Can not be blank') expected #<ActiveModel::DeprecationHandlingMessageArray(["translation missing: ja.activerecord.errors.models.post.attributes.title.blank"])> to include "Can not be blank" 大事なところ translation missing: ja.activerecord.errors.models.post.attributes.title.blank" ↓ ja.ymlのja.activerecord.errors.models.post.attributes.title.blankに対する値がないというエラー。 解決方法 ja.ymlにエラーに書かれている内容通りに設定する "ja.activerecord.errors.models.post.attributes.title.blank" ja.yml ja: activerecord: errors: models: post: attributes: title: blank: "タイトルが空白です" rails cで確認してみる pry(main)> I18n.t("activerecord.errors.models.post.attributes.title.blank") => "タイトルが空白です。" rspecを直す spec/models./post_spec.rb it 'is invalid without a title' do post = Post.new() post.valid? - expect(post.errors.messages[:title]).to include('can`t be blank') + expect(post.errors.messages[:title]).to include(I18n.t("activerecord.errors.models.post.attributes.title.blank")) end Rspec無事通りました。 参考: https://railsguides.jp/i18n.html#%E8%A8%B3%E6%96%87%E3%81%AE%E5%8F%82%E7%85%A7 https://qiita.com/punkshiraishi/items/bdb2d48425782e25eadc https://qiita.com/shimadama/items/7e5c3d75c9a9f51abdd5 https://pikawaka.com/rails/i18n
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】変数の中身を確認する方法のまとめ。View(ビュー)とController(コントローラ)の中のデバッグ実例

railsで変数の値を確認するためにデバッグする方法のまとめ。 目次 コントローラ(controller)の中でデバッグする方法 pp puts logger.debug ビュー(View)の中でデバッグする方法 <%= debug @変数名 %> <%= @変数名 %> モデルの中身を対話モードで確認する方法 localeファイルの中身を確認する コントローラで表示する ビューで表示する railsコンソールで表示する 1. コントローラ(controller)の中でデバッグする方法 1-1. pp (☆おすすめ) ・pp @変数名 Pritty Printの略。Pritty Printとはインデントをつけるなど人間が見やすいように可視化する機能。 railsではppで指定した値をconsoleに出力する。 Controller class TopController < ApplicationController def index @footerTexts = [ ['kumasanについて', '入れ子'], 'ヘルプセンター', '利用規約', 'プライバシーポリシー'] pp @footerTexts end end 保存してページをリロードすると、アプリケーションを起動しているコンソールに変数の中身が表示される。 1-2. puts ・puts @変数名 putsメソッドを使うとコンソールに値が出力される。ただし、配列の場合は値毎に改行される。 Controller class TopController < ApplicationController def index @footerTexts = [ ['kumasanについて', '入れ子'], 'ヘルプセンター', '利用規約', 'プライバシーポリシー'] puts @footerTexts end end 変数の中の構造が見えないので、ppの方がわかりやすい。 1-3. logger.debug ・logger.debug(@変数名) コンソールに内容が表示される。 Controller class TopController < ApplicationController def index @footerTexts = [ ['kumasanについて', '入れ子'], 'ヘルプセンター', '利用規約', 'プライバシーポリシー'] logger.debug(@footerTexts) end end 出力内容はppと同じ。文字数の少なさからppに軍配があがる。 2. ビュー(View)の中でデバッグする方法 2-1. <%= debug @変数名 %> <%= debug @変数名 %>を使うとブラウザ上に変数の中身を表示することができる。 .html.erb <%= debug @footerTexts %> yaml形式で表示される。 2-2. <%= @変数名 %> (☆おすすめ) <%= @変数名 %>で、画面上に変数の中身をそのまま表示できる。 .html.erb <%= @footerTexts %> 3. モデルの中身を対話モードで確認する方法 モデル(テーブル)の中身を確認したい時は、対話モードを使うのが便利。 ・rails c で対話モードに入れる。cはconsoleの略。 # rails c Running via Spring preloader in process 55 Loading development environment (Rails 6.1.3.1) irb(main):001:0> ・モデル名.メソッドでデータを呼び出すことができる。 モデルを使ってデータを呼び出す irb(main):008:0> @var = User.all User Load (0.6ms) SELECT "users".* FROM "users" /* loading for inspect */ LIMIT $1 [["LIMIT", 11]] => #<ActiveRecord::Relation []> 4. localeファイルの中身を確認する i18nを使ってlocaleの指定した文字列を表示する方法について。次のようなja.ymlを例として使用する。 ja.yml ja: test: テスト xxx: test: 深いテスト 4-1. コントローラで表示する 先ほどのppを使う。localeの指定は`t('プロパティ名.プロパティ名...')で指定。 controller class TopController < ApplicationController def index pp t('test') pp t('xxx.test') end end ▼注意点 ja.ymlに値が存在しない場合は、translation missing: 入力した値が表示される。 controller pp t('dummy') pp t('xxx.存在しない') 4-2. ビューで表示する <%= t('プロパティ名') %>を使う。 .html.erb <%= t('test') %> <%= t('xxx.test') %> ▼注意点 ja.ymlに値が存在しない場合は、入力した値が表示される。 <%= t('dummy') %> <%= t('yyy.存在しない') %> 4-3. railsコンソールで表示する I18n.t('プロパティ名')で呼び出すことができる。 # rails c Running via Spring preloader in process 197 Loading development environment (Rails 6.1.3.1) irb(main):002:0> I18n.t('test') => "テスト" irb(main):001:0> I18n.t('xxx.test') => "深いテスト" ▼注意点 ja.ymlに値が存在しない場合は、translation missing: 入力した値が表示される。 変更内容はリアルタイムで反映されないため、後からlocaleのja.ymlを変更した場合は、一旦railsコンソールをexitで抜けて、rails cで再度対話モードに入る必要がある。 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでCSSを遅延読み込みさせる

stylesheet_link_tag に preload を付与する。 PageSpeedInsight のスコアアップのために rails で CSS をプリロードさせてファーストビューのレンダリング速度をあげたい。 stylesheet_link_tag に media onload rel preload 属性を付与してあげる。 変更前.erb <%= stylesheet_link_tag 'app_basic', media: 'all' %> 変更後.erb <%= stylesheet_link_tag 'app_basic', media: 'all', onload: "this.onload=null;this.rel='stylesheet'", rel: 'preload', as: 'style' %> <noscript><%= stylesheet_link_tag 'app_basic' %></noscript> preload に対応していないブラウザがあるため、noscriptタグで通常のCSS読み込み処理を書いてあげれば完成。 Chromeの場合は設定から、javascriptを許可しないモードにすればデバッグできます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでCSSをpreloadさせる

stylesheet_link_tag に preload を付与する。 PageSpeedInsight のスコアアップのために rails で CSS をプリロードさせてファーストビューのレンダリング速度をあげたい。 stylesheet_link_tag に media onload rel preload 属性を付与してあげる。 変更前.erb <%= stylesheet_link_tag 'app_basic', media: 'all' %> 変更後.erb <%= stylesheet_link_tag 'app_basic', media: 'all', onload: "this.onload=null;this.rel='stylesheet'", rel: 'preload', as: 'style' %> <noscript><%= stylesheet_link_tag 'app_basic' %></noscript> preload に対応していないブラウザがあるため、noscriptタグで通常のCSS読み込み処理を書いてあげれば完成。 Chromeの場合は設定から、javascriptを許可しないモードにすればデバッグできます。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails1 MVCとは(勉強メモ)

※勉強メモです。 Rails(Webアプリケーションフレームワーク ) ユーザがHTTPリクエストの4つのCRUD(POST、GET、PUT、DELETE)を使って、Web上のリソースを操作するアプリケーション Web上のリソースとは、Webページに表示されるすべて ただし、ユーザに変更権限があるものとないものがある ・MVC役割構造 URLとHTTPメソッドを受取り、Controllerに渡すのが、Router リソース(データ)として振舞うのが、Model Modelを表示・整形するひな形が、View ModelやViewを制御するのが、Controller ・Rails開発の流れ 1.Model:リソースであり操作対象であり扱うデータを決める 2.Router:Modelを置くURLを決定 3.ルーティングごとに (1)Controller (2)View ・Railsプロジェクト作成コマンド rails new プロジェクト名 ・Bundler RubyやRailsのプロジェクトで使用するライブラリの管理ツール Gemfileというファイルにインストールしたいライブラリ名と必要なバージョンの一覧を全て書き出し、bundle installで一気にそれらのライブラリをインストールできる 『gem 'ライブラリ名', 'バージョン指定'』 # バージョン 5.2.x の内、最新をインストールする gem 'rails', '~> 5.2.2' # バージョン 0.4.4 以上で 0.6.0 未満の中で、最新のライブラリをインストールする gem 'mysql2', '>= 0.4.4', '< 0.6.0' # 最新のライブラリをインストールする(バージョン指定無し) gem 'spring' ・Gitでバージョン管理 プロジェクト作成後はまずコミットしておく (リポジトリの初期化) ターミナル、cdで作成フォルダに移動した時(master)と書かれていれば初期化不要。入っていない場合はgit intでバージョン管理を開始 $ git add . #フォルダ内全て選択 $ git commit -m "init Rails Project" #-mを付けてcommit $ git branch -M main #ブランチ名をmainに変更 (REPLとは) Read-eval-printloopの略で「読込み-処理-表示」の繰り返しの意味 ターミナルでirbを実行することで、RubyのREPLが起動 終了はexit ・Rails console ターミナルで実行すると、irb(main):001:0>というirbによるRubyコマンド待受状態になる コードを変更したらreload!する。reload!コマンドを実行しないと再変更後のRubyコードは反映されない ・サーバの起動rails s ・エラーの発生 Mysql2::Error::ConnectionError mysql2 の Gem からエラーは発せられていてり、データベースとの連携でエラーが出ていることを予想する エラーを解消するために、新規でターミナルのタブを開き、sudo service mysql start で MySQL サーバを起動 Activerecord::NoDatabaseError エラーメッセージから、'message-board_development' のデータベース名で作成すれば良さそうだと予想 ・MySQL連携の設定ファイルの確認と変更 Rails のデータベースの接続設定を管理しているファイルは config/database.yml この設定をRailsがサーバ起動時に読み込むことで、RailsとMySQLの接続が確立(連携)される ・MySQL接続時のユーザを変更 初期ではdefaultでrootに設定されているので、MySQLレッスンで作成したdbuserに変更 config/database.yml の16・17行目あたりにある username と password の設定を以下のとおりに変更し、ファイルを保存 username: dbuser password: dbpass ・データベースの作成 rails db:create ? Created database 'message-board_development' Created database 'message-board_test' 上記のデータベースが作成され、エラーが解決 ・サーバを起動してデータベースの接続を確認 rails s ・Model リソースであり操作対象。永久保存されるデータのためデータベースと密接に関係している。 はじめに作成するアプリケーションは、どんなデータを扱うのかを決める。 ・Modelwo作成する rails g model Message content:string 「メッセージのテーブル設計」 ・マイグレーションファイル Railsではマイグレーションファイルという、テーブル管理ファイルによってデータベースのテーブルを管理・実行する 主に、テーブル作成・削除、カラム追加・削除に関することがファイル内に記述される Railsでテーブルの設定を変更したいときは、マイグレーションファイル作成、Railsからマイグレーション実行をする def change ... end内にテーブル操作が書かれる ・マイグレーションの実行 マイグレーションファイルの作成だけでは実行されない rails db:migrateで実行 ※マイグレーション実行後は、このファイルを修正してもテーブルの変更はできない! 変更を加える時は、rails g migrationによって追加のマイグレーションファイルをさらに生成する ・マイグレーションの確認 MySQL ログイン $ sudo mysql -u root データベースの選択 mysql> use message-board_development テーブル一覧確認(messagesテーブルが作成されているか確認) mysql> show tables; テーブル設計の確認 mysql> describe messages; ・モデルクラスの作成 マイグレーションでmodels/messages.rbも作成される ActiveRecord::Base → ApplicationRecord という順番でクラスを継承した Message モデルには、それらの機能が全て継承されており、Message クラスにコードが無くてもモデル操作できる ActiveRecord: Railsという大きく統合されたライブラリ群の中の1つで、Model全般の操作を担うライブラリ Railsアプリとのデータベース連携を担う ActiveRecordを介する事で、ほとんどSQLを書かず、Rubyの文法でレコードを操作可能になる ・Modelで使用するCRUDメソッド一覧 all:全レコード取得 new:新規レコードの為のモデルインスタンス作成 find:idを指定して検索 find_by:id以外でも指定して検索 where:検索条件を文字列、配列、ハッシュのいずれかの方法で与えられる first:最初のレコードを1件だけ取得する save:レコードの作成 update,save:データの更新 destroy:データ削除 モデル名::メソッド名やモデルのインスタンス.メソッド名で使用 これらのRubyメソッドを使うことで、Rubyコードが自動的にSQLに変換されて実行される ORM(Object-relational mapping):オブジェクト指向プログラミング言語コードでデータベースを操作できる技法 ・Rails consoleでモデルのCRUD操作 rails console前に必ずSQLサーバ起動確認sudo service mysql status ・モデルの一覧を確認 Messages.all ・モデルのインスタンスを作成 message = Message.new(content: "test") ・モデルのインスタンスに値を代入 message.content = "hello" ・モデルのインスタンスをデータベースへ保存(Create)  message.save ・モデルのインスタンスの取得(Read) all:複数のレコードを取得するメソッドで、レコード全体を取得 First:最初のレコードのインスタンスを返す find:idから単一のレコードを検索し返す find_by:id以外でも単純な条件で検索可能 Where:条件指定してレコードを検索 ・レコードの更新(Update) findなどでレコードを取得、インスタンスに対して値を再代入、Saveするのが通常 ・レコードの削除(Delete) destroy:削除 ・RouterとControllerとViewの開発概要 Router:URLのルーティングを一元管理している config/routes.rbの1ファイルで、ルーティングを全て把握できる Controller:ユーザから送信されてきたHTTPリクエストの処理を担当 HTTPリクエストはRouterからControllerの1つのメソッドに割り当てられる Routerと対応するControllerのメソッドをアクションと呼ぶ View:最終的にHTTPレスポンスとして返すWebページ ・RouterとControllerとViewの開発の流れ どんな機能を作るか考え、実装すべき機能が挙がると、 1.ルーティングを決める 2.Controllerのメソッドを決める 3.Viewの名前を決める 「Router」 ・リソースに対するCRUDの為の4つのルーティング Rails.application.routes.draw do get 'messages/:id', to: 'messages#show' post 'messages', to: 'messages#create' put 'messages/:id', to: 'messages#update' delete 'messages/:id', to: 'messages#destroy' end :idはURLにidを入れる POSTの場合だけidは不要。新規作成のためidはまだ用意されていない ・CRUDのための残り3つの補助ページ 詳細ページ(show)にアクセスするには、一覧ページ(index)が必要 get 'messages/:id', to: 'messages#show' この詳細ページにアクセスするには、まずリソースの一覧ページがないと、個々のリソースページである詳細ページへ辿り着けない ?一覧ページ(index)を付け加える get 'messages', to: 'messages#index' ・保存アクション(create)にデータを送るには、新規作成用フォームページ(new)が必要 get 'messages/new', to: 'messages#new' ・更新アクション(update)にデータを送るには、更新用のフォームページ(edit)が必要 get 'messages/:id/edit', to: 'messages#edit' ・削除アクション(delete)はボタンがあればOK どこかのページに削除ボタンが設置してあれば、削除ページは不要 ・7つの基本ルーティングの省略形 Rails.application.routes.draw do # CRUD get 'messages/:id', to: 'messages#show' post 'messages', to: 'messages#create' put 'messages/:id', to: 'messages#update' delete 'messages/:id', to: 'messages#destroy' # index: show の補助ページ get 'messages', to: 'messages#index' # new: 新規作成用のフォームページ get 'messages/new', to: 'messages#new' # edit: 更新用のフォームページ get 'messages/:id/edit', to: 'messages#edit' end ? Rails.application.routes.draw do resources :messages end これでさっきのルーティングと全く同じ意味になる。 indexやshowという命名をした理由は、resourcesで生成されるルーティングがそのように決まっているから ・Routerの完成 トップページにアクセスした時のルーティングをMessagesControllerのindexアクションに設定する。 indexアクションは、トップページ「/」と「/messages」にアクセスした両方で同じルーティングが設定されたことになる Rails.application.routes.draw do root to: 'messages#index' resources :messages end ・Routerの確認 routes.rbで少し込み入ったコーティングをした場合、rails routesで現状のルーティングを確認できる resources :messagesで生成される7つのルーティングを『RESETfulなルーティングと呼ぶ』 RESETfulと言われたら、7つの基本アクション(index,show,new,create,edit,update,destroy)を思い出すように ●ControllerとView ・Controllerの作成 rails g controller コントローラ名(モデルの複数形) Modelと同様に、継承によってControllerとしての基本機能が提供される ・ControllerをRESETfulなルーティングに対応させる routes.rbで設定したルーティングに対応したアクションをmessages_controller.rbに追加 Controller内のルーティングと同じ名前のメソッド名として定義 ・7つのアクションに対応したViewファイルの作成 GETメソッドで指定されたルーティングのみ GET以外はリソースの具体的な操作の為 index.html.erb のように .erb だけでなく、.html と入れているのは、 ERB ファイルが HTML ファイルへと変換されることを Rails に対して明示するため ・共通部の書き出し HTMLを書く時に必ず共通部分が出てくる <!doctype html>やhead内要素 application.html.erbにまとめる! body要素内の<%= yield %>に埋め込まれるページが切り替わる ?<%= yield %>にindex.html.erbなどの内容が代入される ・7つのルーティングに対するレスポンスの実装 controllerのindexアクション:Messageモデルのレコード一覧表示 def index @messages = Message.all end viewの作成ほか・・・ link_to 表示文字列, リンク先 <%= link_to message.id,message %> message のように Model のインスタンス(レコード)を渡すと、自動的にそのインスタンスの show アクションへとリンクされる 省略せずに書くと message_path(message) となり、リンク先 にこう記述しても正常に動作する message も message_path(message) もどちらも最終的に /messages/1 など/messages/:id の形の URL を生成しているだけ 【リンク生成の為のメソッド】 Prefixとしてmessages,new_messagesなどが載せられている ルーティングを設定すると、自動的にリンク生成の為のメソッドも定義される (Prefix + _path)となる :idが必要なものはインスタンスを特定する必要があるので、引数として@messageなどのインスタンスが必要 <%= link_to message.id, message_path(@message) %> ・View ERBにフォームを設置 form_withを使用して生成 <h1>メッセージ新規作成ページ</h1> <%= form_with(model: @message, local: true) do |f| %> <%= f.label :content, 'メッセージ' %> <%= f.text_field :content %> <%= f.submit '投稿' %> <% end %> <%= link_to '一覧に戻る', messages_path %> form_withでフォーム開始、endで終了 form_with(model: @message)のように、Controllerのnewアクションで用意した@messageを使用してフォームを作成する事を明示する (local: true)抜かすと画面推移しない通信が発生 今回はサイト内での遷移であること、ならびに画面遷移を伴う同期通信のみで構わないため local: true をつける f.label,f.text_fieldでカラムを指定し、@messageの中のどのカラムに対する入力欄なのかを明示 f.submitで送信ボタンを生成 <%= link_to '新規メッセージの投稿', new_message_path %> を追加して、 index から new へのリンクを作成 def create @message = Message.new(message_params) if @message.save flash[:success] = 'Message が正常に投稿されました' redirect_to @message else flash.now[:danger] = 'Message が投稿されませんでした' render :new end end def edit end def update end def destroy end private # Strong Parameter def message_params params.require(:message).permit(:content) end コード下部 privateは、それ以降に定義されたメソッドがアクションではなく、クラス内でのみ使用する事を明示 def message_params が【Strong Parameter】 ちゃんと必要なパラメータを把握し、送信されてきたデータを精査(フィルタリング)しようということ 今回は:content以外のデータはフィルタにかけて捨てるようにしている params.require(:message)でMessageモデルのフォームから得られるデータに関するものと明示し、.permit(:content)で必要なカラムだけを選択している コード上部 @message = Message.new(message_params)で、Messageインスタンス生成時に、Strong parameterが使用されている ・redirect_toとrenderの違い redirect_to:強制的に移動させる。createアクション実行→showアクション実行後、show.html.erbが呼ばれる render:単にmessage/new.html.erbを表示するだけ(アクションは実行しない) ・flashとflash.nowの違い redirect_toの前ならflash。HTTPリクエストを発生させるため、flash.nowだと内容を保存できず消えてしまう。 renderの前ならflash.now。HTTPリクエストを発生させないため消えない。 ・flash表示ファイルの作成 view/layouts/_flash_messages.html.erb <% flash.each do |message_type, message| %> <div><%= message %></div> <% end %> 全てのviewで表示させたいため、application.html.erbでrenderする (renderと書かれた場所に_flash_messages.html.erb の記述内容が埋め込まれる) 名前が_から始まるファイルを作成し、Viewの一部を抜き出して記述したものを『パーシャル』と呼ぶ flashに代入されたメッセージを1つ1つ取り出し、全表示する。flashはハッシュである為、|key, value|のペアで取り出される。 今回は|message_type, message|という変数名を用いている。 views/layouts/application.html.erb <body> <div class="container"> <%= render 'layouts/flash_messages' %> <%= yield %> </div> </body> ●messages#edit 既存のメッセージレコードを編集するので、idでメッセージレコードを検索する。(params[:id]) destroyアクションのredirect_to messages_url 今までprefix_pathだったが、リダイレクトの場合だけは上記で↑ messages_urlはPrefixがmessagesのためindexへリダイレクトされる リダイレクトの時だけ_urlを使用する show.html.erb <%= link_to 'このメッセージを削除する', @message, method: :delete, data: { confirm: '本当に削除してよろしいですか?' } %> method: :delete =DELETEメソッドを送信するのを明示 data: { confirm: ... } =javaScript 【bootstrapの適用】 ・link_toにクラスを指定する class "..." ・bootstrapのページネーション コマンドでBootstrap用のkaminariのviewを生成 rails g kaminari:views bootstrap4 ページネーションに手を加える場合、viws/kaminariフォルダ内のファイルを編集 ・show showのp要素をテーブルに <table> <thead> table header <tr> table rou(行) <td> table data ... 一覧に戻るリンクを削除。navbarがあるため、タイトルクリックでindexに戻る為 ・new フォームにform-groupとform-controlのclass属性を付与 グリッドシステム 横長のテキスト入力欄をグリッドシステムで横半分に フォームをrowとcol-6のdiv要素で囲む→6/12で半分 new.html一覧に戻るリンク削除 ・edit フォームはパーシャルで更新済 一覧に戻るリンク削除 まとめ ModelはControllerによって必要なものが取得され、Viewに流れて表示される。ユーザのHTTPリクエストをRouterが解析し、リクエストに沿って適切なModelをユーザに表示するのがWebアプリケーション。 Modelはデータベースが無いと保存できない。データベースが用意される事で、Modelのインスタンスをレコードとして保存できるようになる。 【エラーのデバック】 エラー画面には以下の情報が含まれる エラータイトル:例外クラスの名前と、発生場所 エラーが起きたファイル名:ファイルのパスと行番号 エラー詳細:エラーメッセージ エラーが起きた行:強調表示される トレース状況:エラーが起きたメソッドの呼び出し状況を出力 リクエスト情報:Railsサーバに送信したリクエスト パラメータ:必要なパラメータが含まれているか、間違っていないか レスポンス情報:Railsサーバーがどんなレスポンスを返したか ・エラーの修正手順 1.エラーの概要を理解する タイトルと詳細を読んで概要を理解する よくあるエラー? 「NoMethodError in ...」 undefined method (a)for(b) bにaというメソッドが存在しない。定義されていない、bに正しいデータが入っていない場合が多い bがnil:NilClassの場合、エラーが起きた場所より前で正しくデータをセットできているか確認 「ActiveRecord::RecordNotFound in ...」 Coludn't find a with b bの条件を満たすaが見つからない。ユーザIDを指定したけど、DB上にデータが存在しないなど。 「routingError」 No route matches [a] "b" bのURLへのaメソッドのリクエストを受け取ったが、対応するルーティング設定がない rails routesコマンドの結果や、routes.rbの内容を確認する 「Missing Template」 コントローラのアクションに対応するビューが存在しない erbファイルが存在し、ファイルパスが正しい事を確認 「Syntax Error」 プログラムの文法エラー ・エラー発生部分を特定する ・ソースコードを修正する Railsのデバックツール pry-byebug Gemfileの追加 gem 'pry-byebug', group: :deveropment bundle install 使い方 ソースコードの途中で、Railsアプリケーションの実行を一時的に止められる。実行を止めたい部分に「binding.pry」という1行を追加
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpacker::manifest::missingentryerror に遭遇した

エラー文 webpacker::manifest::missingentryerror 〜 みたいなのが出てきました 原因は Your Yarn packages are out of date! Please run yarn install --check-files to update. ?と出てました。 やったこと yarn install --check-files bundle exec rails webpacker:install
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecで単体テストしようと思ったらバリデーションが足らないことが判明しました。

テストコードを調べながらコーディングしていたら結局、バリデーションかけ忘れが発覚してバリデーションを調べていく羽目になりました。とほほ 参考 https://qiita.com/iamu_TECH_CAMP/items/d10ffa4f7fa3afaa61b6 目的 テストコードとして ・パスワードは英数混合かつ半角以外は登録できない ・名前(姓・名で分割入力)は漢字・ひらがな・全角カタカナ以外は登録できない ・読みがな(姓・名分割入力)は全角カタカナ以外登録できない としたい できればある程度DRY(繰り返しを避ける)ためにコードもまとめたい 疑問1 空欄しない「presence :trueは浮かぶが全角カタカナで登録という文字指定・文字サイズ指定はどうするのか? *(参考)バリデーションを定義すると、下記のメソッドが動く前に必ず検証が行われます。 save/create/update length(文字数制限) #最低でも3文字以上 validates :カラム名, length: { minimum: 3 } #3文字以内 validates :カラム名, length: { maximum: 3 } #5文字から10文字以内 validates :カラム名, length: { in: 5..10 } #6文字ピッタリ validates :カラム名, length: { is: 6 } オプション名 内容 minimum 最小値を指定 maximum 最大値を指定 in 長さの範囲を指定 is 値の長さを指定 format 正規表現とフォーマットに入力された値が合致するかの検証 withオプションと併用して使用 下は全角カタカナのみを許可したバリデーション validates :name, format: { with: /\A[ァ-ヶー-]+\z/ } 指定型 指定方法 全て数値(半角) /\A[0-9]+\z/ 半角アルファベット(小文字) /\A[a-z]+\z/ 半角アルファベット(大文字) /\A[A-Z]+\z/ 半角アルファベット(大文字・小文字) /\A[a-zA-Z]+\z/ 半角アルファベット(小文字・数値) /\A[a-z0-9]+\z/ 半角アルファベット(大文字・数値) /\A[A-Z0-9]+\z/ 全角ひらがな /\A[ぁ-んー-]+\z/ 全角カタカナ /\A[ァ-ヶー-]+\z/ 全角ひらがな、カタカナ /\A[ぁ-んァ-ヶー-]+\z/ 半角カナ /\A[ァ-ン゙゚]+\z/ 漢字 /\A[一-龥]+\z/ 全角ひらがな、漢字 /\A[一-龥ぁ-ん]/ 全角ひらがな、全角カタカナ、漢字 /\A[ぁ-んァ-ン一-龥]/ (参考)Rubyのバリデーション用正規表現集 https://gist.github.com/nashirox/38323d5b51063ede1d41 完成したバリデーション models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable with_options presence: true do validates :nickname, :birthday, :password_confirmation validates :first_name, :last_name, format: { with: /\A[ぁ-んァ-ン一-龥々]/, message: "は全角ひらがな、全角カタカナ、漢字で入力して下さい" } validates :last_name_prono, :first_name_prono, format: { with: /\A[ァ-ヶー-]+\z/, message: "は全角カタカナで入力して下さい" } end validates :password, :password_confirmation, format: { with: /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i, message: "は半角英数で入力して下さい" } end with_options〜endで同じバリデーションをまとめているのですが、 上の方のdeviseの:validatableでパスワードの空登録禁止のバリデーションはかかっているので with_options〜endで同じバリデーションをまとまりの中から外しています。バリデーションが2回かかり 同じメッセージ「Email cant't be blank」が2度出ないようにしています。 パスワードのwithの部分ですが models/user.rb format: { with: /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i, message: "は半角英数で入力して下さい" } 詳しく知りたい方は @momotaro98さんのパスワード向け正規表現 /^(?=.?[a-z])(?=.?\d)[a-z\d]{8,100}$/i を解読するを参考にしてみてください。とっても詳しいです。 https://qiita.com/momotaro98/items/460c6cac14473765ec14
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails sで起動できなかった・・・

対処 エラー文 Address already in use - bind(2) for "127.0.0.1" port 3000 (Errno::EADDRINUSE) ?といったエラー文が出てきました 解決 使用したURL:https://madogiwa0124.hatenablog.com/entry/2018/04/07/135714 $ lsof -i:3000 と記述 $ kill -9 (PIDの番号) とやったらrails sと再起動したら起動できました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]継続的にプロダクト開発していくために必要な設定たち

Railsを使えば簡単にWebアプリケーションを立ち上げることができます。 例えば、Railsガイドに書かれている手順通りに実施するだけで1時間もかからずに画面を表示することができると思います。 静的なページだけではなく、scaffoldを使うことで簡単なデータ操作を一通りできる画面もサクッと作ることができます。 以前私が書いた『Rails newからproductionモードで動くようになるまで』では、1歩踏み込んでproductionモードで動くまでの手順を書きました。 ただ、上記はproductionモードで動いているものの、プロダクト開発で使うためには他にも様々な設定が必要です。 プロダクトで使えるようにするためには、同期的な処理だけでは足りず、ほとんどの場合に非同期処理を行う仕組み(Active job)やメール配信の仕組み(Action Mailer)などが必要になります。 また、外部接続情報など環境ごとに設定値を持つようにしたり、継続的に開発できるようにCIを設定したり、誰でも同じ環境で開発できるようにどのマシンでも開発環境を再現できるようにしたり、様々なことをする必要があります。 この記事ではrails newで作ったRailsアプリケーションに上記に書いたプロダクト開発するために必要になるであろう設定を入れる手順をまとめました。 ※私個人が最近はRailsをAPIアプリケーションとしか扱っていないため、この記事ではAPIモードで構築します。 前提 開発環境: Docker ソース管理: GitHub CI: GitHub Actions 各種バージョンは下記の通り Ruby: 3.0 Rails: 6.1.1→6.1.3.1 APIモード 記事を書いている途中でライセンス問題が発生して動かなくなったのでバージョンアップ MySQL: 8.0.23 Redis: 6.0.9 Githubにリポジトリを作る 今の時代、ソース管理は必須ですよね。GitHubに新しいリポジトリを追加します。 READMEや.gitignoreは rails newした時に生成されるので不要です。 リポジトリができたらローカルにCloneしましょう。 % git clone git@github.com:ham0215/rails_api_base.git Cloning into 'rails_api_base'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), done. % cd rails_api_base rails_api_base % ls LICENSE Dockerの準備 開発環境はどのマシンでも同じ環境を再現しやすくするためにDockerで構築します。 Dockerfile 書き方は様々あると思いますが、下記のような感じにしています。 localeやvimは必須ではないのですが、コンテナ上でvimを使ったり日本語入力できるようにするために入れています。 Dockerfile FROM ruby:3.0.0 RUN apt-get update && apt-get install -y \ build-essential \ vim \ locales \ locales-all \ default-mysql-client \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* ENV LANG ja_JP.UTF-8 RUN mkdir /app WORKDIR /app COPY Gemfile Gemfile COPY Gemfile.lock Gemfile.lock RUN bundle install COPY . . CMD ["rails", "server", "-b", "0.0.0.0"] Dockerfile内でGemfileとGemfile.lockは明記しており、ファイルがないとエラーになってしまうのでファイルを作っておきます。 Gemfileはrailsだけ記述しておき、Gemfile.lockは空でOKです。 Gemfile source 'https://rubygems.org' gem 'rails', '6.1.1' ここまでできたら一度ビルドしてみます。 DOCKER_BUILDKITを指定すると少し高速化され、コンソールの表示が見やすくなるのでオススメです。 % COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose build db uses an image, skipping WARNING: Native build is an experimental feature and could change at any time Building api ... Successfully built c20632ba2529ddbf7702f9df80ded5d28955d0272290c57ebbdb01b65f55b5ed docker-compose.yml MySQLもローカルから接続できるようにportsを指定しています。 portはデフォルトのままだと他のアプリケーションと被ることが多いので少しずらすと良いです。 docker-compose.yml version: '3.8' services: db: image: mysql:8.0.23 environment: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" ports: - '3308:3306' volumes: - ./mysqlcnf.d:/etc/mysql/conf.d - ./tmp/mysql:/var/lib/mysql api: tty: true stdin_open: true build: . command: rails s -b 0.0.0.0 volumes: - .:/app environment: DB_HOST: db ports: - "3001:3000" depends_on: - db dbのvolumesに指定している- ./mysqlconf.d:/etc/mysql/conf.dはMySQLの認証方法を変更するために指定しています。 mysqlcnf.d/custom.cnf [mysqld] default_authentication_plugin=mysql_native_password 詳細は下記の記事をご覧ください。 下記の記事はGitHub Actionsの話ですが、同様のことを行っています。 rails new 次にRailsアプリケーションを作成します。 APIモードで作成するので--apiをつけています。その他不要なものはskipしています。 docker-compose run api bundle exec rails new . --database=mysql --skip-action-mailbox --skip-action-text --skip-spring --skip-turbolinks --skip-bootsnap --skip-action-cable --skip-javascript --skip-jbuilder --skip-system-test --api --skip-test --force rails newが正常終了して必要なファイルが生成されたら、必要最低限の設定を行ってサーバーを立ち上げてみます。 まずDB接続情報の修正が必要です。 docker-composeでつけた名前がhostとして使えるので修正します。 config/database.yml - host: localhost + host: db 接続情報を直したらDBを作成して、空のschema.rbを作っておきましょう。 % docker-compose run api rails db:create % docker-compose run api rails db:migrate 一通り設定ができたのでDockerを立ち上げてサーバーにアクセスしてみます。 下記コマンドでコンテナをバックグラウンドで立ち上げます。 "COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose up --build -d" 立ち上がったらブラウザで画面を表示してみましょう。 http://localhost:3001/ お馴染みの下記画面が出たらここまでの手順は成功です! database setting 最初にデータベースのcharacter_setを確認しておきます。 データベースの設定は途中で変える場合、面倒なことになることが多いので必ず最初に確認しましょう。 MySQLに接続します。 db:createとdb:migrateを行っているのでDBとテーブルが作成されています。 % mysql -h 127.0.0.1 -P3308 -uroot mysql> show databases; +--------------------+ | Database | +--------------------+ | app_development | | app_test | | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 6 rows in set (0.01 sec) mysql> use app_development Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed mysql> show tables; +---------------------------+ | Tables_in_app_development | +---------------------------+ | ar_internal_metadata | | schema_migrations | +---------------------------+ 2 rows in set (0.01 sec) character_set character_setを確認します。 mysql> show variables like '%char%'; +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ 8 rows in set (0.01 sec) database.ymlに下記設定があるため、ほとんどの項目はutf8mb4が設定されています。 config/database.yml encoding: utf8mb4 特にこだわりがなければutf8mb4を使えばよいと思います。 character_set_systemだけutf8ですが、こちらはutf8mb4は設定できないので問題ありません。 utf8mb4はutf8を拡張したものです。詳細は下記をご覧ください。 照合順序 続いて照合順序を確認します。 mysql> SELECT @@collation_database; +----------------------+ | @@collation_database | +----------------------+ | utf8mb4_0900_ai_ci | +----------------------+ 1 row in set (0.00 sec) MySQL8系からデフォルトの照合順序がutf8mb4_0900_ai_ciになったようです。 下記のブログがとても分かりやすかったので参考にさせていただきました。 日本語を使う環境であれば、utf8mb4_general_ciかutf8mb4_binを選ぶのが良いと思います。 この表だけで考えると英語の大文字/小文字が区別できるutf8mb4_binが良さそうに見えますが、文字列型でよくユニーク制約を付けるメールアドレスの比較は大文字/小文字を区別しない方が嬉しいのでutf8mb4_general_ciが便利だったりします。 今回はcollation: utf8mb4_general_ciに変更します。 database.ymlにcollation: utf8mb4_general_ciを設定してDBを作り直します。 作り直すときはvolumsで指定しているtmp/mysql/配下を消し忘れないようにご注意ください。 Dockerを起動し直して、改めてdb:createを行い変更されていることを確認します。 mysql> use app_development Database changed mysql> SELECT @@collation_database; +----------------------+ | @@collation_database | +----------------------+ | utf8mb4_general_ci | +----------------------+ 1 row in set (0.00 sec) ログをjsonにする Railsではデフォルトで下記のようなログが出力されます。 log/development.log Started GET "/" for 172.24.0.1 at 2021-02-24 14:49:00 +0000 (0.7ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC Processing by Rails::WelcomeController#index as HTML Rendering /usr/local/bundle/gems/railties-6.1.1/lib/rails/templates/rails/welcome/index.html.erb Rendered /usr/local/bundle/gems/railties-6.1.1/lib/rails/templates/rails/welcome/index.html.erb (Duration: 11.5ms | Allocations: 406) Completed 200 OK in 52ms (Views: 33.8ms | ActiveRecord: 0.0ms | Allocations: 2208) 開発中は上記のログで特に問題ないのですが、本番のログなどは監視ツール等に取り込みたいなどありjson形式にしたいことがあります。 そんなときに役立つgemがlogrageです。 Gemfileに追加して最低限の設定を入れてみました。 元のログも残したいので別ファイルに出力するようにしています。 設定方法の詳細はREADMEをご覧ください。 config/initializers/lograge.rb Rails.application.configure do return if Rails.env.test? config.lograge.enabled = true config.lograge.keep_original_rails_log = true config.lograge.logger = ActiveSupport::Logger.new "#{Rails.root}/log/lograge_#{Rails.env}.log" config.lograge.formatter = Lograge::Formatters::Json.new end 下記のようにjson形式のログも出力されるようになりました。 log/lograge_development.log {"method":"GET","path":"/","format":"html","controller":"Rails::WelcomeController","action":"index","status":200,"duration":44.07,"view":28.32,"db":0.0} RSpec, factory-bot, faker, simplecov 継続的に開発するアプリケーションにはテストコードが必須です。 テストを行うgemは様々あると思いますが、メジャーなRSpecを使うと良いと思います。 RSpec関連のgemを入れます。私がよく使うgemは下記の通り。 rspec-rails RailsでRSpecを使えるようにするgem factory_bot_rails テストデータを生成するためのgem faker ダミーデータ(適当な名前やメールアドレスなど)をいい感じに生成してくれるgem simplecov テストカバレッジを出力してくれるgem simplecov-json simplecovのテストカバレッジをjsonにしてくれるgem では早速インストールします。 各gemのREADMEに記載されている通り、Gemfileに追加してinstallするだけです。 本番環境では必要ないのでdevelopmentやtestのgroupに追加しましょう。 ここからは各Gemについてもう少し詳細に書いていきます。 rspec-rails インストールするためのコマンドがあるので実行します。 % docker-compose exec api rails g rspec:install create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb RSpecで必要なファイルが生成されます。 Ruby2.7.2からdeprecated warningがデフォルトで出力されなくなったのですが、test時には出力されたほうが嬉しいので出力されるように下記の記述を追加しました。 spec/spec_helper.rb Warning[:deprecated] = true factory_bot_rails 設定不要で使えますが、下記の記述を書いておくとFactoryBot.createなどのFactoryBotを省略してcreateだけで使えるようになるので便利です。 spec/rails_helper.rb config.include FactoryBot::Syntax::Methods faker 特に初期設定は不要です。 simplecov, simplecov-json READMEを参考に設定を行います。 設定はconfig/initializers配下に置きました。 出力は人が見やすいHTMLとプログラムで扱いやすいJsonの2種類にしています。 config/initializers/simplecov.rb return unless Rails.env.test? require 'simplecov' require 'simplecov-json' SimpleCov.formatters = [ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::JSONFormatter, ] .simplecovにadd_filterでカバレッジを取得する必要がないフォルダーを指定します。 設定ファイルやマイグレーションファイル、テストファイルを除いています。 .simplecov SimpleCov.start('rails') do add_filter 'config' add_filter 'spec' add_filter 'db' end RSpec実行 設定したのでテストを1つ書いてみます。 と言っても、まだ1つもアクションがないのでテストと一緒に作成してみます。 簡単にscaffoldを使ってUserリソース(カラムはnameのみ)を作ってみます。 RSpecやFactoryBotを導入していた後なので、controller / model / migration だけではなく、RSpecやFactoryBotのファイルも自動生成されました。 % docker-compose exec api rails g scaffold User name:string invoke active_record create db/migrate/20210311150016_create_users.rb create app/models/user.rb invoke rspec create spec/models/user_spec.rb invoke factory_bot create spec/factories/users.rb invoke resource_route route resources :users invoke scaffold_controller create app/controllers/users_controller.rb invoke resource_route invoke rspec create spec/requests/users_spec.rb create spec/routing/users_routing_spec.rb 簡単に動作させるなら何も編集しなくても良いレベルのソースが書かれているのでそのまま利用します。 まずはマイグレーションを実行してテストDBにも反映します。 % docker-compose exec api rails db:migrate == 20210311150016 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0302s == 20210311150016 CreateUsers: migrated (0.0303s) ============================= % docker-compose exec api rails db:test:prepare % docker-compose exec api rspec ***********...... Pending: (Failures listed here are expected and do not affect your suite's status) 1) User add some examples to (or delete) /app/spec/models/user_spec.rb # Not yet implemented # ./spec/models/user_spec.rb:4 2) /users GET /index renders a successful response # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:36 3) /users GET /show renders a successful response # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:44 4) /users POST /create with valid parameters creates a new User # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:53 5) /users POST /create with valid parameters renders a JSON response with the new user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:60 6) /users POST /create with invalid parameters does not create a new User # Add a hash of attributes invalid for your model # ./spec/requests/users_spec.rb:69 7) /users POST /create with invalid parameters renders a JSON response with errors for the new user # Add a hash of attributes invalid for your model # ./spec/requests/users_spec.rb:76 8) /users PATCH /update with valid parameters updates the requested user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:91 9) /users PATCH /update with valid parameters renders a JSON response with the user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:99 10) /users PATCH /update with invalid parameters renders a JSON response with errors for the user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:109 11) /users DELETE /destroy destroys the requested user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:120 Finished in 0.13536 seconds (files took 5.92 seconds to load) 17 examples, 0 failures, 11 pending Coverage report generated for RSpec to /app/coverage. 14 / 29 LOC (48.28%) covered. Coverage report generated for RSpec to /app/coverage/coverage.json. 14 / 29 LOC (48.28%) covered. user_spec.rbのテストはすべてpendingになっているので実行できるように修正します。 テストを直す前にUserモデルのバリデーション失敗をテストするために、nameに必須制約を入れておきます。 app/models/user.rb class User < ApplicationRecord validates :name, presence: true end users_specはskipのところを修正しました。 nameの生成にはFakerを使っています。 spec/requests/users_spec.rb let(:valid_attributes) { - skip("Add a hash of attributes valid for your model") + { name: Faker::Name.name } } let(:invalid_attributes) { - skip("Add a hash of attributes invalid for your model") + { name: '' } } # This should return the minimal set of values that should be in the headers @@ -85,7 +85,7 @@ RSpec.describe "/users", type: :request do describe "PATCH /update" do context "with valid parameters" do let(:new_attributes) { - skip("Add a hash of attributes valid for your model") + { name: Faker::Name.name } } it "updates the requested user" do @@ -93,7 +93,7 @@ RSpec.describe "/users", type: :request do patch user_url(user), params: { user: new_attributes }, headers: valid_headers, as: :json user.reload - skip("Add assertions for updated state") + expect(user.name).to eq new_attributes[:name] end 再実行してみましたが、headerのチェックでこけてしましました。 % docker-compose exec api rspec *.....F..F....... Failures: 1) /users POST /create with invalid parameters renders a JSON response with errors for the new user Failure/Error: expect(response.content_type).to eq("application/json") expected: "application/json" got: "application/json; charset=utf-8" (compared using ==) # ./spec/requests/users_spec.rb:80:in `block (4 levels) in <top (required)>' 2) /users PATCH /update with invalid parameters renders a JSON response with errors for the user Failure/Error: expect(response.content_type).to eq("application/json") expected: "application/json" got: "application/json; charset=utf-8" (compared using ==) # ./spec/requests/users_spec.rb:114:in `block (4 levels) in <top (required)>' Finished in 1.52 seconds (files took 5.43 seconds to load) 17 examples, 2 failures, 1 pending 調べてみるとRials6.1でresponse.content_typeで返却される値が変わったようです。 ということなので、エラーになっているテストのexpected valeueを変更します。 よく見たらcontent_typeの部分一致で比較している箇所もあったので合わせてみました。これはRSpecのテンプレートの修正漏れなのかな? → rspec-railsに該当箇所のプルリクを送ったらマージされたので次のバージョンからはこの事象は発生しなくなると思います。 spec/requests/users_spec.rb - expect(response.content_type).to eq("application/json") + expect(response.content_type).to match(a_string_including("application/json")) 再実行したら全て成功しました。 モデルスペックがpendingのままですが今回は省略します。直す気がないpendingは残しておいても邪魔なので削除しておくと良いです。 % docker-compose exec api rspec *................ Pending: (Failures listed here are expected and do not affect your suite's status) 1) User add some examples to (or delete) /app/spec/models/user_spec.rb # Not yet implemented # ./spec/models/user_spec.rb:4 Finished in 1.44 seconds (files took 6.11 seconds to load) 17 examples, 0 failures, 1 pending Coverage report generated for RSpec to /app/coverage. 28 / 30 LOC (93.33%) covered. Coverage report generated for RSpec to /app/coverage/coverage.json. 28 / 30 LOC (93.33%) covered. 最後にRSpec実行時の最後に記載されているカバレッジをみてみましょう。 coverage/index.htmlをブラウザで開いてみると下記のようにファイルごとのカバレッジが見れます。 今回追加したuser系のファイルは100%になっているのでテストは網羅されてそうです! coverage配下にjson形式のファイルも入っています。 simplecovが出力するカバレッジファイルはコミット不要なので.gitignoreにcoverageディレクトリを追加しておきましょう。 .gitignore + coverage RuboCop RuboCopはコードを静的解析してくれるGemです。 これを入れておくことでコードの記述揺れやインデントやスペースの入れ方などをルールに基づいて機械的にチェックすることができます。 例えば下記のようなチェックを行ってくれます。 # `{`の後のスペースがいらない app/models/user.rb:13:19: C: [Correctable] Layout/SpaceInsideBlockBraces: Space between { and | detected. (1..10).each { |n| p n } ^ # hogeが定義されているけど使われていない app/models/user.rb:25:5: W: Lint/UselessAssignment: Useless assignment to variable - hoga. hoge = 'hoge' ^^^^ # 最終行はreturnを書かなくても良い app/models/user.rb:26:5: C: [Correctable] Style/RedundantReturn: Redundant return detected. return true ^^^^^^ 上記のような機械的にできるチェックをレビューで人がやるのは時間の無駄になるだけでなく、体裁ばかりに目がいってしまい本来に抽出したい複雑な不具合などに目がいかなくなってしまいます。 このような事象を回避するために静的コードチェックツールは必須と言えるでしょう。 READMEに記載されている通り、Gemfileに追加してinstallしましょう。 本番環境では必要ないものなのでdevelopmentやtestのgroupに追加しましょう。 インストールできたら早速実行してみます。 % docker-compose exec api rubocop Inspecting 38 files CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Offenses: .simplecov:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment. SimpleCov.start('rails') do ...(略) 38 files inspected, 196 offenses detected, 183 offenses auto-correctable 196個も指摘されました・・・ 様々な原因がありますが、現時点ではまだほとんど実装していないので指摘されている箇所はRailsが自動生成したファイルが多いです。 私はざっくり下記の方針で直しています。 Railsが自動生成したファイル 今後触るファイル: RuboCopのルールに合わせて修正 今後ほぼ触らないファイル: RuboCopの対象外にする migrationファイルやconfigは対象外 まずはこの方針に合わせて設定ファイル(.rubocop.yml)を設定しました。 NewCops: enableを書いておくことで、バージョンアップで追加されるチェック項目が自動的に有効になります。 .rubocop.yml AllCops: Exclude: - 'bin/**/*' - 'config/**/*' - 'config.ru' - 'db/**/*' - 'Gemfile' - 'spec/**/*' - 'vendor/**/*' NewCops: enable この設定を入れて再実行すると21個まで減りました。 8 files inspected, 21 offenses detected, 19 offenses auto-correctable 残りは[修正する / チェックを無効(またはゆるく)にする]を考えながら見ていきます。 最終的に下記のチェックだけ無効にして他はコードを修正しました。 Style/Documentationを無効 classの先頭にドキュメントがないとNGになる。コメントは必要な時だけ追加すれば良いと思っているので無効化。 まだコード量が少ないのでほとんど引っ掛かりませんでしたが、今後コードを追加していくと新しくNGになることがあると思います。 特にMetrics/AbcSizeやMetrics/MethodLengthは曲者です。 無理やりメソッドを分割することでNGを回避することができるのですが、可読性を考えて分割しない方がよいという判断もありえます。 RuboCopのチェックを全て正とするのではなく[修正する / チェックを無効(またはゆるく)にする]を吟味して開発速度が最大化される方向にチェック仕様をアップデートしていきましょう。 Brakeman Brakemanはコードを静的解析して脆弱性を検知してくれるgemです。 これを入れておくことで典型的な脆弱性となり得るコードを機械的に検知することができます。 あくまで典型的なものを防げるだけですが、入れておいて損はないでしょう。 READMEに記載されている通り、Gemfileに追加してinstallするだけです。 本番環境では必要ないのでdevelopmentやtestのgroupに追加しましょう。 早速実行します。 オプションはこのページに細かく記載されています。 # Rails6系なので -6 を指定 # 全てのチェック項目を実施したいので -A を指定 # 全ての警告を検知したいので -w 1 を指定 % docker-compose exec api brakeman -6 -A -w 1 ... == Overview == Controllers: 2 Models: 2 Templates: 0 Errors: 0 Security Warnings: 1 == Warning Types == Missing Encryption: 1 == Warnings == Confidence: High Category: Missing Encryption Check: ForceSSL Message: The application does not force use of HTTPS: `config.force_ssl` is not enabled File: config/environments/production.rb Line: 1 1つ警告が検知されました。 productionではconfig.force_sslを有効にしろとのことです。 こちら本番環境の構成によって設定すべきかどうか違うと思いますが、今回はとりあえずtrueにしておきます。 対応不要な場合はbrakeman.ignoreというファイルを生成することでチェック対象外にできます。 config/environments/production.rb - # config.force_ssl = true + config.force_ssl = true これで警告はなくなりました。 % docker-compose exec api brakeman -6 -A -w 1 ... == Overview == Controllers: 2 Models: 2 Templates: 0 Errors: 0 Security Warnings: 0 == Warning Types == No warnings found tbls tblsはデータベースのスキーマ情報からテーブル定義のドキュメントを生成してくれるツールです。 テーブル定義に限りませんが、手動でメンテしているドキュメントは本番環境と乖離してしまうので、tblsのようにコードからリバースエンジニアリングできるツールは重要です。 こちらはDockerが提供されているのでそれを使います。 docker-composeに追記しました。 設定値の詳細はREADMEをご覧ください。 下記のように設定することでdocs/tables配下にテーブル定義が出力されます。 docker-compose.yml + tbls: + image: k1low/tbls:latest + volumes: + - .:/work + environment: + TBLS_DSN: mysql://root:@db:3306/app_development + TBLS_DOC_PATH: docs/tables + depends_on: + - db 早速実行してみます。 --forceをつけることでファイルがあっても上書きするようにしています。 % docker-compose run --rm tbls doc --force Creating rails_api_base_tbls_run ... done docs/tables/schema.svg docs/tables/ar_internal_metadata.svg docs/tables/schema_migrations.svg docs/tables/users.svg docs/tables/README.md docs/tables/ar_internal_metadata.md docs/tables/schema_migrations.md docs/tables/users.md 下記のMarkdownが生成されました。 シードデータ Dockerで環境構築していると環境を簡単にリセットすることができますが、そのたびにデータまで初期化されてしまっては面倒です。 そこで開発時にあったら便利なデータはシードデータを作っておき、いつでもロードできるようにしておくと良いです。 また、マスターデータなどを入れる際にもシードデータを作っておくと便利です。 シードデータの作成はseed-fuというgemが便利です。 Gemfileに追加してインストールしましょう。 2018年から更新されていないのでドキュメントなどが古いですが、Rails6でも問題なく使えます。 Gemfile +gem 'seed-fu' development環境用のシードデータを作ってみます。 db/fixtures/development/users.rb User.seed(:id, { id: 1, name: 'hoge' }, { id: 2, name: 'fuga' }, ) 実行します。 % docker-compose exec api rails db:seed_fu == Seed from /app/db/fixtures/development/users.rb - User {:id=>1, :name=>"hoge"} - User {:id=>2, :name=>"fuga"} データベースに登録されました。 mysql> select * from users; +----+------+----------------------------+----------------------------+ | id | name | created_at | updated_at | +----+------+----------------------------+----------------------------+ | 1 | hoge | 2021-03-15 15:03:33.725453 | 2021-03-15 15:03:33.725453 | | 2 | fuga | 2021-03-15 15:03:33.734686 | 2021-03-15 15:03:33.734686 | +----+------+----------------------------+----------------------------+ 2 rows in set (0.00 sec) seed-fuのいいところは指定したキーが重複している場合はUpdateになるところです。 上記の場合はidをキーとしてデータが存在している時はUpdateとして動作します。 そのため重複実行してもエラーになったり、実行ごとにデータが増加していったりすることはありません。 Security Alert / dependabot Gemfileでインストールしているgemのバージョンを定期的にチェックし、脆弱性があるバージョンを使っていたり、新しいバージョンがある場合に通知してくれるサービスがあります。 継続的に開発していく場合、ライブラリのアップデートは必要不可欠なので検知できるように設定しておきましょう。 Security Alert GitHubのリポジトリページのSettingsタブのSecurity & analysisから設定できます。 このページでDependabot alertsとDependabot security updatesを有効にするだけです。 有効にしておくとセキュリティーアラートがある場合にリポジトリページに下記のように表示されます。 See Dependabot alertsをクリックすると詳細ページに飛べます。 詳細ページからは脆弱性を解消するためのプルリクを生成することができ、とても便利です。 (当然ですが、脆弱性を解消するライブラリのバージョンが存在していない場合はプルリクは生成できません) 下記のようなプルリクが作られるので、内容を確認して問題なければマージしてバージョンアップ完了です dependabot dependabotは脆弱性有無にかかわらず、ライブラリのバージョンアップを検知してプルリクを作ってくれるサービスです。 こちらも入れておきましょう。 marketplaceからインストールします。 インストールするとSettings>Applicationsに追加されます。 設定画面から対象のリポジトリを追加しましょう。 これで新しいバージョンのライブラリを検知した場合、プルリクを勝手に作ってくれます。 CI(GitHub Actions) ここまででRSpecやRuboCopなどをインストールして自動テストや静的解析が整ってきましたが、コードを修正するたびに手動で各種チェックツールを実行するのは現実的ではなく、実行し忘れのリスクがあります。 そこで、Pushしたときに自動実行されるようにします。 CIをするためのツールは様々ありますが、GitHubを使っているのでGitHub Actionsを使います。 Github Actionsの構文は公式ドキュメントをご覧ください。 GitHub ActionsでRailsのCIを行う記事は以前に書いたのでこちらもみていただけるとありがたいです。 今回は自動実行したいツールごとにYAMLを作りました。 Brakeman Brakemanを実行します。 Brakemanは静的解析なのでBrakemanのGemだけインストールしています。 また、DBも必要ないのでDBセッティングもなし。 pathsやpaths-ignoreを設定設定してBrakemanに影響があるファイルが更新された時だけ実行するようにしておくと良いです。 .github/workflows/brakeman.yml name: Brakeman on: push: branches: - 'feature/*' - main paths-ignore: - README.md - Dockerfile - docker-compose.yml - 'spec/**' - 'docs/**' jobs: brakeman: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby 3.0.0 uses: ruby/setup-ruby@v1.66.0 with: ruby-version: 3.0.0 bundler-cache: true - name: run brakeman run: | gem install brakeman brakeman -6 -A -w 1 RSpec RSpecを実行します。 GitHub ActionsでRailsからMySQL8系に接続するにも一手間必要なのですが、詳細は別記事に書いています。 .github/workflows/rspec.yml name: RSpec on: push: branches: - 'feature/*' - main paths-ignore: - README.md - Dockerfile - docker-compose.yml jobs: rspec: runs-on: ubuntu-latest timeout-minutes: 10 env: RAILS_ENV: test DB_HOST: 127.0.0.1 DB_PORT: 33060 services: db: image: mysql:8.0.23 volumes: - mysqlconf.d:/etc/mysql/conf.d ports: - 33060:3306 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes BIND-ADDRESS: 0.0.0.0 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby uses: ruby/setup-ruby@v1.64.1 with: ruby-version: 3.0.0 bundler-cache: true - name: bundle install run: | gem install bundler bundle install --jobs 4 --retry 3 --path vendor/bundle - name: migration run: | bundle exec rails db:create bundle exec rails db:test:prepare - name: run rspec run: bundle exec rspec RuboCop RuboCopを実行します。 RuboCopは静的解析なのでRuboCopのGemだけインストールしています。 また、DBも必要ないのでDBセッティングもなし。 paths-ignoreを.rubocopのExcludeに合わせておくと良いと思います。 .github/workflows/rubocop.yml name: RuboCop on: push: branches: - 'feature/*' - main paths-ignore: - README.md - Dockerfile - docker-compose.yml - 'spec/**' - 'docs/**' - 'db/**' jobs: rubocop: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby uses: ruby/setup-ruby@v1.66.0 with: ruby-version: 3.0.0 bundler-cache: true - name: run rubocop run: | gem install rubocop rubocop Seed seedを実行します。 RuboCopやRSpecのようにコード自体をチェックするものではないのですが、コード修正した時にseedを修正し忘れて壊れてしまうことが多々あるので、CIで正常に実行できるか確認するようにしています。 seedに限らず、コード修正時によく修正漏れしてしまうものがある場合はこのように機械的に検知できるようにしておくと良いです。 .github/workflows/seed.yml name: seed on: push: branches: - 'feature/*' - main paths-ignore: - README.md - Dockerfile - docker-compose.yml jobs: seed: runs-on: ubuntu-latest timeout-minutes: 10 env: RAILS_ENV: development DB_HOST: 127.0.0.1 DB_PORT: 33060 services: db: image: mysql:8.0.23 volumes: - mysqlconf.d:/etc/mysql/conf.d ports: - 33060:3306 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes BIND-ADDRESS: 0.0.0.0 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby uses: ruby/setup-ruby@v1.66.0 with: ruby-version: 3.0.0 bundler-cache: true - name: bundle install run: | gem install bundler bundle install --jobs 4 --retry 3 --path vendor/bundle - name: migration run: | bundle exec rails db:create bundle exec rails db:schema:load - name: run seed_fu run: bundle exec rails db:seed_fu Tbls tblsを実行します。 tblsはREAMEに下記のように書かれている通り、CIと相性が良いです。 tbls is a CI-Friendly tool for document a database, written in Go. ドキュメントに記載されている通りdiffというパラメーターを指定するとDBスキーマとドキュメントの差分を検出してくれます。 これを利用してドキュメントの更新漏れを検知できるようにしています。 .github/workflows/tbls.yml name: Tbls on: push: branches: - 'feature/*' paths: - 'docs/tables/*' - 'db/**' jobs: tbls: runs-on: ubuntu-latest env: RAILS_ENV: development DB_HOST: 127.0.0.1 DB_PORT: 33060 services: db: image: mysql:8.0.23 volumes: - mysqlconf.d:/etc/mysql/conf.d ports: - 33060:3306 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes BIND-ADDRESS: 0.0.0.0 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby uses: ruby/setup-ruby@v1.66.0 with: ruby-version: 3.0.0 bundler-cache: true - name: apt-get run: | sudo apt-get update sudo apt-get install libmysqlclient-dev jq - name: bundle install run: | gem install bundler bundle install --jobs 4 --retry 3 --path vendor/bundle - name: migration run: | bundle exec rails db:create bundle exec rails db:migrate - name: tbls diff uses: docker://k1low/tbls:latest env: TBLS_DSN: mysql://root:@db:3306/app_development TBLS_DOC_PATH: docs/tables with: args: diff Github Actionsのdependabot Github Actionsのyamlで指定しているライブラリもガンガンバージョンアップしていくので、dependabotで検知できるようしておくと良いです。 dependabotの設定ファイル(yaml)はGitHubのリポジトリーページから生成することができます。 DependenciesタブのDependabotで"Create config file"をクリックしてください。 (執筆時点でBetaと記載されているので今後変わるかもしれません) yamlが生成され、ブラウザ上のエディターでyamlが生成されるので、修正してコミットします。 package-ecosystemのところだけ"github-actions"に修正しました。 特に変更しませんでしたが、チェックする頻度なども指定できます。 指定できるものはドキュメントをご覧ください。 i18n i18nは多言語化対応するための仕組みです。 Railsガイドにも記載されている通りRailsに最初から入っています。 特に多言語化対応しないとしても、エラーメッセージなどの文言はi18nの仕組みを使っておくと一元管理されて便利です。 今回はすでに作っているuserモデルのエラーメッセージを英語/日本語で出し分けられるようにしてみます。 Nameの必須チェックしか実装されておらず面白みがないのでi18nの機能をわかりやすく試すためにカスタムバリデーションも追加しました。 app/models/user.rb class User < ApplicationRecord validates :name, presence: true validate :ng_name private def ng_name errors.add(:name, 'にNGNAMEは使えません') if name == 'NGNAME' end end このまま実行すると下記のようなエラーメッセージが出力されます。 ブランクのメッセージは英語になっており、カスタムエラーのメッセージはハードコーディングした通りに出ていますが項目名がNameになっています。 irb(main):001:0> User.create!(name: '') Traceback (most recent call last): 1: from (irb):1:in `<main>' ActiveRecord::RecordInvalid (Validation failed: Name can't be blank) irb(main):002:0> User.create!(name: 'NGNAME') Traceback (most recent call last): 2: from (irb):1:in `<main>' 1: from (irb):2:in `rescue in <main>' ActiveRecord::RecordInvalid (Validation failed: Name にNGNAMEは使えません) まずは設定を変更して英語と日本語を使えるようにします。 今回は言語を分岐させたい箇所で明示的に指定しようと思っているので、デフォルトはenのままにしています。 config/initializers/locale.rb I18n.config.available_locales = %i[ja en] I18n.default_locale = :en 続いて言語ファイル(ja.ymlやen.yml)を作りますが、最初から作るのは大変なのでrails-i18nにある言語ファイルのyamlをがconfig/localesにコピーしておきます。 コピーしてきたyamlに今回必要な項目を追加し、モデルのバリデーションを使うようにします。 --- a/config/locales/en.yml +++ b/config/locales/en.yml + ng_name: cannot be NGNAME --- a/config/locales/ja.yml +++ b/config/locales/ja.yml ja: activerecord: + attributes: + user: + name: 名前 errors: messages: record_invalid: 'バリデーションに失敗しました: %{errors}' restrict_dependent_destroy: has_one: "%{record}が存在しているので削除できません" has_many: "%{record}が存在しているので削除できません" + ng_name: に'NGNAME'は使えません --- a/app/models/user.rb +++ b/app/models/user.rb def ng_name - errors.add(:name, 'にNGNAMEは使えません') if name == 'NGNAME' + errors.add(:name, :ng_name) if name == 'NGNAME' end end それでは英語、日本語を出し分けてみます。 I18n.with_localeで囲むことでその中で使う言語を変更することができます。 irb(main):001:1* I18n.with_locale(:en) do irb(main):002:1* User.create!(name: '') irb(main):003:0> end Traceback (most recent call last): 2: from (irb):1:in `<main>' 1: from (irb):2:in `block in <main>' ActiveRecord::RecordInvalid (Validation failed: Name can't be blank) irb(main):004:1* I18n.with_locale(:ja) do irb(main):005:1* User.create!(name: '') irb(main):006:0> end Traceback (most recent call last): 3: from (irb):3:in `<main>' 2: from (irb):4:in `rescue in <main>' 1: from (irb):5:in `block in <main>' ActiveRecord::RecordInvalid (バリデーションに失敗しました: 名前を入力してください) irb(main):007:1* I18n.with_locale(:en) do irb(main):008:1* User.create!(name: 'NGNAME') irb(main):009:0> end Traceback (most recent call last): 3: from (irb):6:in `<main>' 2: from (irb):7:in `rescue in <main>' 1: from (irb):8:in `block in <main>' ActiveRecord::RecordInvalid (Validation failed: Name cannot be NGNAME) irb(main):010:1* I18n.with_locale(:ja) do irb(main):011:1* User.create!(name: 'NGNAME') irb(main):012:0> end Traceback (most recent call last): 3: from (irb):9:in `<main>' 2: from (irb):10:in `rescue in <main>' 1: from (irb):11:in `block in <main>' ActiveRecord::RecordInvalid (バリデーションに失敗しました: 名前に'NGNAME'は使えません) config 開発を行っていると、外部環境への接続情報など環境ごとに値を出し分けたいことが多々あります。 そういうときにはconfigというGemを使うと便利です。 これを使うことで環境ごとに切り替えたい設定値をyamlに定義することができるようになります。 Gemfileに追加してインストールします。 インストールコマンドを実行すると、下記のように環境ごとの設定ファイルが生成されます。 config直下のsettingsは環境共通の設定値で、config/settings配下に環境ごとに設定値を設定します。 % docker-compose exec api rails g config:install create config/initializers/config.rb create config/settings.yml create config/settings.local.yml create config/settings create config/settings/development.yml create config/settings/production.yml create config/settings/test.yml append .gitignore また、gitignoreにlocalとついているyamlが除外されるようになっています。 localがついているファイルはgitに保存しないような機密情報を設定するために用意されています。 ただ、特に暗号化されるわけではなくgitignoreに追加されるだけなので管理は自分たちで行う必要があります。 機密情報は次に紹介するcredentialsが使えるのでそちらもご確認ください。 +config/settings.local.yml +config/settings/*.local.yml +config/environments/*.local.yml 早速動作確認をしてみます。 yamlに下記を追加しました。 # config/settings/development.yml app: env: devleopment # config/settings/production.yml app: env: production 下記のように環境ごとに出し分けることができました。 # rails c Loading development environment (Rails 6.1.1) irb(main):001:0> Settings.app.env => "devleopment" # RAILS_ENV=production rails c Loading production environment (Rails 6.1.1) irb(main):001:0> Settings.app.env => "production" Credentials 1つ前に紹介したconfigと同様に環境ごとの設定値を格納できるのですが、こちらは暗号化して保存されます。 暗号化されており値の確認や編集に一手間かかるので機密情報かどうかでconfigと使い分けると良いと思います。 こちらは最初からRailsに入っている仕組みなのでGemの追加は不要です。 早速環境ごとに作ってみます。 下記のようなエラーが発生した場合はEDITORという環境変数に編集に使うエディターを指定して実行ください。 % docker-compose exec api rails credentials:edit --environment development No $EDITOR to open file in. Assign one like this: EDITOR="mate --wait" bin/rails credentials:edit For editors that fork and exit immediately, it's important to pass a wait flag, otherwise the credentials will be saved immediately with no chance to edit. 私はvimを使うのでdocker-compose.ymlの環境変数のところに下記のように追加しました。 docker-compose.yml environment: DB_HOST: db + EDITOR: vim 改めてeditコマンドを実行するとファイルが生成されるので下記の項目を記載しました。 config/credentials/development.yml secret: key: secret!! --environment developmentを指定したのでdevelopment環境用の設定ファイル(暗号化済み)と復号するためのkeyが生成されます。 config/credentials/development.key config/credentials/development.yml.enc またこのときに.gitignoreに/config/credentials/development.keyが追加されます。 keyが流出してしまうと誰でも復号できるようになってしまうので、GitHubには上げずに厳重に管理しましょう。 設定値は下記のように利用することができます。 こちらもSettingsと同様に環境ごとに出し分けることができます。environmentを指定して必要な環境分用意しておきましょう。 irb(main):005:0> Rails.application.credentials.secret[:key] => "secret!!!" Active Job 開発していると実行に時間がかかる処理やリアルタイムで行う必要がない処理などがでてきます。 そのような処理を非同期で実行する仕組みがActive Jobです。 詳細はRailsガイドを参照してください。 Active Jobを使うにはキューとキューに接続するためのフレームワークを使います。 様々なものがありますが、今回は幅広く使われているSidekiqを使いたいと思います。 またキューを保持するためにRedisを使います。 Redis Redisをdocker-composeに追加します。 下記を追加してビルドし直します。PORTは他とかぶらないように少しずらしました。 docker-compose.yml + redis: + image: redis:6.0.9 + ports: + - '6380:6379' 起動できたらコンテナに入って動作確認しておきましょう。 % docker-compose exec redis bash root@86983cd92065:/data# redis-cli -h redis -p 6379 redis:6379> INFO # Server redis_version:6.0.9 ... Sidekiq Sidekiqをインストールします。 ドキュメントが充実しているので参考にします。 まず、Gemfileに追加してbundle installしてRedisの設定をします。 詳細はドキュメントを参照してください。 今回はinitializerを使います。 内容はドキュメント通りですが、テストのときは使わないのでreturnを入れています。 config/initializers/sidekiq.rb return if Rails.env.test? Sidekiq.configure_server do |config| config.redis = { url: 'redis://redis:6379/0' } end Sidekiq.configure_client do |config| config.redis = { url: 'redis://redis:6379/0' } end 続いてActive Jobで使えるようにします。 詳細はドキュメントを参照してください。 テスト以外の環境ではsidekiqを使うのでapplication.rbに追加して、テスト環境だけ:testを設定しました。 config/application.rb config.active_job.queue_adapter = :sidekiq config/environments/test.rb config.active_job.queue_adapter = :test テストアダプターを利用するとジョブを同期実行してくれるようになります。 テストのときに非同期実行されると結果の確認が困難なのでtestの場合はテストアダプターを設定しておくと便利です。 テストジョブ実行 一通り設定は終わったので動作確認用にテストジョブを作って実行します。 動作確認したいだけなのでログを出力するだけのジョブにします。 app/jobs/example_job.rb class ExampleJob < ActiveJob::Base # Set the Queue as Default queue_as :default def perform(*args) Rails.logger.debug 'start job!!!!' end end 続いてsidekiqを起動します。 % docker-compose exec api sidekiq -q default WARN: Unresolved or ambiguous specs during Gem::Specification.reset: minitest (>= 5.1) Available/installed versions of this gem: - 5.14.4 - 5.14.2 racc (~> 1.4) Available/installed versions of this gem: - 1.5.2 - 1.5.1 WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>' Please report a bug if this causes problems. 2021-04-01T08:43:01.144Z pid=38 tid=6hq INFO: Booting Sidekiq 6.2.0 with redis options {:url=>"redis://redis:6379/0"} m, `$b .ss, $$: .,d$ `$$P,d$P' .,md$P"' ,$$$$$b/md$$$P^' .d$$$$$$/$$$P' $$^' `"/$$$' ____ _ _ _ _ $: ,$$: / ___|(_) __| | ___| | _(_) __ _ `b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` | $$: ___) | | (_| | __/ <| | (_| | $$ |____/|_|\__,_|\___|_|\_\_|\__, | .d$$ |_| 2021-04-01T08:43:02.269Z pid=38 tid=6hq INFO: Booted Rails 6.1.3.1 application in development environment 2021-04-01T08:43:02.269Z pid=38 tid=6hq INFO: Running in ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux] 2021-04-01T08:43:02.270Z pid=38 tid=6hq INFO: See LICENSE and the LGPL-3.0 for licensing details. 2021-04-01T08:43:02.270Z pid=38 tid=6hq INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org 2021-04-01T08:43:02.273Z pid=38 tid=6hq INFO: Starting processing, hit Ctrl-C to stop 起動したらコンソールからジョブを実行してみます。 irb(main):001:0> ExampleJob.perform_later Enqueued ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) to Sidekiq(default) => #<ExampleJob:0x000056020c20d690 @arguments=[], @job_id="c578b673-7b66-43e4-91ac-d48f34537ccb", @queue_name="default", @priority=nil, @executions=0, @exception_executions={}, @timezone="UTC", @provider_job_id="24ee1022e0a460169097b15a"> 結果はログで確認します。 Sidekiq経由でジョブが実行され、きちんとログが出力されています。 [ActiveJob] Enqueued ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) to Sidekiq(default) [ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] Performing ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) from Sidekiq(default) enqueued at 2021-04-01T08:44:50Z [ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] start job!!!! [ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] Performed ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) from Sidekiq(default) in 0.67ms Action Mailer アプリケーションからメールを送信したいことがあると思います。 Railsにはメールを送信する仕組みも入っているので設定しておきます。 詳細はRailsガイドを参照してください。 SendGrid Action Mailerを設定する前にメール送信するためには配信サービスを使う必要があります。 一昔前であれば自分でメールサーバーを立てるなどかなり手間だったのですが、現状はSaasサービスを使ってサクッと配信できるのでそれを利用します。 今回は執筆時点(2021/04)で月に12,000通まで無料でメール配信できるSendGridを利用します。 SendGridは値段がお手頃なだけではなく、Ruby用のGemが用意されているのでRubyとの相性も良いです。 Sidekiq Sidekiqを使ってメール送信を非同期で行うことができます。 ドキュメントに記載されている通りデフォルトではmailersというキューに格納されるのでSidekiqを起動するときにmailersというキューも立ち上げるようにしましょう。 docker-dompose exec api sidekiq -q default -q mailers ちなみにconfig/application.rbなどでconfig.action_mailer.deliver_later_queue_name = 'hoge'のように設定することでキューをmailersから変更することも可能です。 また、nilを設定するとdefaultのキューが使われるようになるようです。 Action Mailerの設定 Action Mailerを設定します。 ActionMailerは配信メソッドをdelivery_methodで指定することができます。 指定できるものはドキュメントを御覧ください。 SendGridをSMTPとして使うだけであれば、delivery_methodにSMTPを設定するだけでできます。 SendGridのドキュメントにもSMTPで設定する方法が記載されています。 ただ、この方法ではSendGridの機能を活かすことができないので、SendGridの機能を活かせるようにdelivery_methodに指定できるカスタムメソッドを作成したいと思います。 Railsではadd_delivery_methodという配信メソッドを追加する仕組みが用意されているため、SendGridを使う配送メソッドを独自に実装してdelivery_methodで使えるようにします。 SendGridの配送メソッド作成 それではSendGridの配送メソッドを作成します。 ゼロから作るのは難しいので、SMTPのソースを参考に作成します。 SMTPのソースをadd_delivery_methodしている箇所 ソースの一部を抜粋しました。 smtpが指定された場合に使うクラスMail::SMTPとパラメーター(address〜enable_starttls_auto)を指定しています。 add_delivery_method :smtp, Mail::SMTP, address: "localhost", port: 25, domain: "localhost.localdomain", user_name: nil, password: nil, authentication: nil, enable_starttls_auto: true SMTPのソース ソースのうち、publicメソッドを抜粋しました。 下記の2つのメソッドが定義されています。 initializeでは、add_delivery_methodで指定した引数を受け取っています。 deliver!では、mailオブジェクトが渡されるので配送処理を実装します。 def initialize(values) self.settings = DEFAULTS.merge(values) end def deliver!(mail) response = start_smtp_session do |smtp| Mail::SMTPConnection.new(:connection => smtp, :return_response => true).deliver!(mail) end settings[:return_response] ? response : self end これらを参考に配送メソッドを作成しました。app/serviseに置きましたが場所はどこでもOKです。 配送処理はsendgrid-rubyのREADMEを参考にしています。 contentを複数指定したかったので、細かいところはmailクラスをのぞいてみて実装しています。 https://github.com/sendgrid/sendgrid-ruby/blob/main/lib/sendgrid/helpers/mail/mail.rb app/services/send_grid_service.rb class SendGridService attr_reader :api_key, :mail def initialize(settings) @api_key = settings[:api_key] end def deliver!(mail) @mail = mail sg = SendGrid::API.new(api_key: api_key) response = sg.client.mail._('send').post(request_body: request_body) raise response.inspect if response.status_code.to_i >= 300 response.body end private def request_body sg_mail = SendGrid::Mail.new sg_mail.from = SendGrid::Email.new(email: mail.from.first) personalization = SendGrid::Personalization.new mail.to.each { personalization.add_to(SendGrid::Email.new(email: _1)) } sg_mail.add_personalization(personalization) sg_mail.subject = mail.subject mail.body.parts.each do content_type = "#{_1.main_type}/#{_1.sub_type}" sg_mail.add_content(SendGrid::Content.new(type: content_type, value: _1.body.raw_source)) end sg_mail.to_json end end 上記の配送クラスをadd_delivery_methodを使って追加します。 api_keyはSendgridで発行したものを使います。環境ごとに違う&機密情報なのでcredentialsを使いました。 ちなみにto_prepareを使うとproductionでは初回のみしか読み込まれませんが、developmentでは都度読み込まれるので開発時に便利です。 config/initializers/sendgrid.rb Rails.configuration.to_prepare { ActionMailer::Base.add_delivery_method(:sendgrid, SendGridService, api_key: Rails.application.credentials.sendgrid_api_key) } 追加できたのでdelivery_methodで指定します。 ついでにconfig.action_mailer.raise_delivery_errors = trueを設定しておくとメール配信時にエラーが発生したときにエラー検知できるようになるので設定しておきましょう。 config/application.rb config.action_mailer.delivery_method = :sendgrid config.action_mailer.raise_delivery_errors = true config/environments/test.rb config.action_mailer.delivery_method = :test テストの場合はメールを送りたくないので:testを指定しておきましょう。 testを指定するとメール配信される代わりにActionMailer::Base.deliveriesに格納されるためテストのときに便利です。 メール配信 設定が終わったのでメールクラスを実装します。 rails g mailerで必要なファイルを生成します。 今回はUserMailerにしました。 % docker-compose exec api rails g mailer UserMailer hello create app/mailers/user_mailer.rb create app/mailers/application_mailer.rb invoke erb create app/views/user_mailer create app/views/layouts/mailer.text.erb create app/views/layouts/mailer.html.erb create app/views/user_mailer/hello.text.erb create app/views/user_mailer/hello.html.erb invoke rspec create spec/mailers/user_mailer_spec.rb create spec/fixtures/user_mailer/hello create spec/mailers/previews/user_mailer_preview.rb テストメールを送りたいだけなのでサクッとmailerクラスとviewを作ります。 viewはRailsガイドに書いてあるとおりhtmlとtext形式を用意しました。 自動生成されたviewファイルに.enをつけていますが、ここを言語の数だけ用意するだけでlocaleの設定によってメールを出し分けてくれます。 今回は動作確認をしたいだけなのでdefault_localeにしているenだけ準備しました。 app/mailers/user_mailer.rb class UserMailer < ApplicationMailer def hello(user_id) @user = User.find user_id # usersテーブルにemailカラム追加 mail(to: user.email, subject: 'Hello!! ham!!' ) end end app/views/user_mailer/hello.en.html.erb hello!! <%= @user.name %>.<br> first mail!!!<br> app/views/user_mailer/hello.en.text.erb hello!! <%= @user.name %>. first mail!!!<br> 準備ができたのでRails consoleを使って動作確認してみます。 irb(main):002:0> UserMailer.hello(1).deliver_now User Load (0.9ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 Rendering layout layouts/mailer.html.erb Rendering user_mailer/hello.en.html.erb within layouts/mailer Rendered user_mailer/hello.en.html.erb within layouts/mailer (Duration: 0.1ms | Allocations: 6) Rendered layout layouts/mailer.html.erb (Duration: 2.6ms | Allocations: 82) Rendering layout layouts/mailer.text.erb Rendering user_mailer/hello.en.text.erb within layouts/mailer Rendered user_mailer/hello.en.text.erb within layouts/mailer (Duration: 0.1ms | Allocations: 4) Rendered layout layouts/mailer.text.erb (Duration: 3.7ms | Allocations: 80) UserMailer#hello: processed outbound mail in 18.5ms Delivered mail 60730ef18f54d_14c21fc2499b@77f71c6e18a0.mail (623.8ms) ...(中略) <60730ef18f54d_14c21fc2499b@77f71c6e18a0.mail>>, <Subject: Hello!! ham!!>, <Mime-Version: 1.0>, <Content-Type: multipart/alternative; boundary="--==_mimepart_60730ef18e808_14c21fc2489e"; charset=UTF-8>, <Content-Transfer-Encoding: 7bit>> メールが届くことを確認しました。 hello!! ham. first mail!!! また、Sidekiqを起動して非同期で遅れることも確認します。 % docker-compose exec api sidekiq -q default -q mailers 非同期送信 irb(main):003:0> UserMailer.hello(1).deliver_later Enqueued ActionMailer::MailDeliveryJob (Job ID: af499814-b119-4a69-b6db-d37935b5e650) to Sidekiq(mailers) with arguments: "UserMailer", "hello", "deliver_now", {:args=>[1]} => #<ActionMailer::MailDeliveryJob:0x00005651c0d9bbc0 @arguments=["UserMailer", "hello", "deliver_now", {:args=>[1]}], @job_id="af499814-b119-4a69-b6db-d37935b5e650", @queue_name="mailers", @priority=nil, @executions=0, @exception_executions={}, @timezone="UTC", @provider_job_id="88ff5493a88556145308df57"> Sidekiqのコンソール 2021-04-11T15:02:46.229Z pid=395 tid=883 class=ActionMailer::MailDeliveryJob jid=88ff5493a88556145308df57 INFO: start 2021-04-11T15:02:47.251Z pid=395 tid=883 class=ActionMailer::MailDeliveryJob jid=88ff5493a88556145308df57 elapsed=1.054 INFO: done Sidekiqを経由してもメール送信できることを確認できました。 Active Storage アプリケーションからファイルをアップロードしたいことがあると思います。 Railsにはファイルアップロードの仕組みも入っているので設定しておきます。 詳細はRailsガイドを参照してください。 セットアップ Railsガイドに記載されている通り、インストールコマンドを実行します。 % docker-compose api rails active_storage:install 実行すると、Active Storageで使うテーブルを作成するマイグレーションファイルが生成されるので実行しておきます。 config/storage.ymlを参照するとAWSやGCPを保存先にするサンプルがコメントされています。 ここの設定値を環境に合わせて変更することで外部サービスのストレージと簡単に連携することができます。 また、アップロードするファイルに対するバリデーションを行うためのGemもあるためインストールしておきます。 今回は開発環境を構築しているので外部ストレージを使わずローカル保存で動作確認します。 ファイルアップロード実装 Userモデルに画像を添付できるようにしてみます。 active_storage_validationsも入れたので、画像以外のファイルやサイズが4MB以上のファイルはアップロードできないようにバリデーションを追加してみました。 app/models/user.rb has_one_attached :avatar validates :avatar, content_type: %i[png jpg jpeg], size: { less_than: 4.megabytes } 次にアップロードするエンドポイントを追加します。 config/routes.rb - resources :users + resources :users do + post :avatar + end routesを確認してみます。 今回追加したファイルアップロード用のエンドポイントuser_avatar POST /users/:user_id/avatar(.:format)が追加されています。 また、/rails/active_storageで始まるActive Storage用のエンドポイントがいくつか追加されています。 % docker-compose exec api rails routes Prefix Verb URI Pattern Controller#Action user_avatar POST /users/:user_id/avatar(.:format) users#avatar users GET /users(.:format) users#index POST /users(.:format) users#create user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy rails_service_blob GET /rails/active_storage/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_service_blob_proxy GET /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_blob_representation GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_blob_representation_proxy GET /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create アップロード処理を実装します。 受け取ったファイルデータをattachするだけです。とても簡単ですね。 app/controllers/users_controller.rb def avatar user = User.find params[:user_id] user.avatar.attach params[:avatar] raise 'バリデーションエラー' if user.invalid? head :ok end 動作確認 動作確認します。 APIにリクエストを送れるツールなら何でも良いのですが、今回はChromeの拡張機能にあるTalend API Testerを使いました。 下記のようにリクエストして200 OKが返却されました。 rails consoleでファイル添付されていることをattached?で確認して参照用のURLを発行します。 irb(main):001:0> user = User.first irb(main):002:0> user.avatar.attached? => true irb(main):003:0> Rails.application.routes.url_helpers.rails_storage_proxy_path(user.avatar, only_path: true) => "/attachments/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--0efc3d198976ed0b619154371865ef5d0ccf5bfe/avatar.jpg" 早速発行したURLにブラウザでアクセスしてみると、下記のようなエラーが発生しました。 NoMethodError (undefined method `flash' for #<ActionDispatch::Request GET "http://localhost:3001/attachments/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--0efc3d198976ed0b619154371865ef5d0ccf5bfe/avatar.jpg" for 172.23.0.1>): APIモードだとflashが使えないのでflashが見つからずエラーになっているようです。 下記を追加して使えるようにします。 config/application.rb config.middleware.use ActionDispatch::Flash 再度アクセスすることでファイルを表示することができました。 おまけ 参照URLの/rails/active_storageを変える Active Storageで自動追加されるURLのprefixは/rails/active_storageになります。 システム名が丸見えでこのまま使いたいと思う人は少ないと思います。 変更するための仕組みが用意されているので変更しておきましょう。 config.active_storage.routes_prefix = '/attachments'のように設定することで/rails/active_storageを変更することができます。 上記のように設定すると下記のように変わります。 % docker-compose exec api rails routes rails_service_blob GET /attachments/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_service_blob_proxy GET /attachments/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show GET /attachments/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_blob_representation GET /attachments/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_blob_representation_proxy GET /attachments/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show GET /attachments/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_disk_service GET /attachments/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /attachments/disk/:encoded_token(.:format) active_storage/disk#update rails_direct_uploads POST /attachments/direct_uploads(.:format) 使わないエンドポイントを消す Active Storageのエンドポイントがいくつか定義されますが、アプリケーションで全てを使うことはないと思います。 使わないエンドポイントは閉じておきたいので、エンドポイントを消す手順を調べてみました。 config.active_storage.draw_routes = falseを設定することまるごと削除することができるのですが、一部だけ消す方法は見当たりませんでした。 そこで、railsのソースから必要な箇所だけ持ってくることにしました。 まずはconfig.active_storage.draw_routes = falseを設定してデフォルトで追加されるルートを削除します。 config/application.rb config.active_storage.draw_routes = false 次にconfig/routes.rbにRailsのソースから必要な箇所を転記します。 参照で使うエンドポイントだけ残まして他は削除しました。 以下差分です。長くなるのでscope以下は省略しています。 config/routes.rb # # 下記から必要な箇所を転記 # https://github.com/rails/rails/blob/v6.1.3.1/activestorage/config/routes.rb # scope ActiveStorage.routes_prefix do get "/blobs/redirect/:signed_id/*filename" => "active_storage/blobs/redirect#show", as: :rails_service_blob get "/blobs/proxy/:signed_id/*filename" => "active_storage/blobs/proxy#show", as: :rails_service_blob_proxy get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service end ...(長いので省略) routesを確認すると減っていることが確認できます。 % docker-compose exec api rails routes ...(前略) rails_service_blob GET /attachments/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_service_blob_proxy GET /attachments/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show rails_disk_service GET /attachments/disk/:encoded_key/*filename(.:format) active_storage/disk#show 今回はRailsのソースコードをコピペして対応しました。 このように対応するとRailsをバージョンアップしたときなどオリジナルのソースが更新されてしまったときにコピペした部分にも反映が必要となります。 コピペ対応はバージョンアップ時のバグの温床になりやすいので可能な限りやらないほうが良いです。 どうしてもやる場合は、下記のようにコピペした箇所にバージョンが変わった場合にエラーになる処理を追加しておきましょう。 config/routes.rb raise 'バージョンアップしたので下記コピペコードも確認' unless ActiveStorage.version.to_s == '6.1.3.1' これを入れておくとバージョンが変わるとエラーが発生するようになり、バージョンアップ時に反映漏れを防げるようになります。 /app/config/routes.rb:10:in `block in <top (required)>': バージョンアップしたので下記コピペコードも確認 (RuntimeError) その他のおすすめ機能 ここまで私がほぼ外せないと思っている機能を設定してきました。 この章では必須ではないが、便利だと思っている機能を紹介します。 devise deviseはRailsに認証を追加するために便利なgemです。 最近では認証・認可はIDaaSを利用することも多いですが、そこまでお金をかけられない場合にサクッと認証を導入するときに便利です。 deviseにはメール送信する処理もあるのですが、同期処理で送信しています。 deviseのメールを非同期にするgem(devise-sync)もあるので合わせて使うと便利です。 さらに私は使ったことはないですが、deviseに2要素認証を入れるgem(devise-two-factorもあるようです。 最近では多要素認証が主流になってきているのでチェックしておきたいです。 JWT JWTは認証用トークンなどで幅広く使われているトークンです。 Amazon CognitoやAzure ADなどでも使われています。 JWT自体の仕組みについてはこの記事には書きませんが、JWTはトークン自体に様々な情報をもたせることができるのでとても便利です。 Rubyで使うためのGem(ruby-jwt)もあるため簡単に導入することができます。 認証を実装するときは候補の1つとしておくと良いと思います。 graphql-ruby RailsでGraphQLを使うためのGemです。 GraphQLはRESTに変わるAPIのインターフェースで、クエリーを指定することで呼び出し側からレスポンスをカスタマイズすることができます。 詳細は公式ドキュメントを御覧ください。 Rails単体で見ると入出力のバリデーションを自動で行ってくれたり、スキーマファイルが自動生成されスキーマファーストの開発がやりやすいなどメリットがありますが、GraphQLの最大のメリットは様々なプログラミング言語に広がっている点だと思います。 マイクロサービス化が主流となっている昨今ではサービス間でプログラミング言語が異なることがよくあります。 そのため、様々な言語でGraphQLのフレームワークが提供されていることはシステム連携する上で大きなメリットだと思います。 初見ではとっつきにくいとこもありますが、新たにAPIを作成する場合は一度検討してみてください。 開発環境完成 かなり長くなりましたが、ここまでの手順でローカルで動作する開発環境がやっと完成しました。 開発時に便利(というか複数人で開発するときは必須)であるCIの設定や、非同期処理やメール配信、環境ごとの設定変更などプロダクトとして利用するアプリケーションを開発するときにはほぼ100%必要となる機能は一通り入れることができたと思います。 この他にも最後に紹介したdeviseやgraphqlなど開発するプロダクトに合わせて必要なものを追加していきましょう。 今回はプロダクション環境での実行については触れませんでしたが、プロダクションで動かすにはインフラやトラフィックに合わせたチューニングなどがさらに必要になります。 ここまでも長かったですが、まだまだここがプロダクト開発のスタート位置です! 作ったプロダクトを継続的にどんどん育てていきましょう!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

お気に入り機能とルーティングのネストについて

前提 Railsにて、単語帳アプリを作成しました。 登録される単語をWordモデルとし、お気に入り機能を実装するため、Wordモデルと紐づくFavoriteモデルがあり、以下のようなアソシエーションが組まれています。 app/model/word.rb class Word < ApplicationRecord has_many :favorites, dependent: :destroy end app/model/favorite.rb class Favorite < ApplicationRecord belongs_to :word end dependent: :destroyで、親モデルが削除されたときは、それに紐付いている子モデルも削除されるように設定。 単語をお気に入りに追加もしくはお気に入りから外す際には、以下のアクションが働きます。 app/controllers/favorites_controller.rb class FavoritesController < ApplicationController before_action :set_word def create #お気に入りに追加する @favorite = Favorite.create(word_id: @word.id) end def destroy #お気に入りから外す @favorite = Favorite.find_by(word_id: @word.id) @favorite.destroy end private def set_word #パラメーターとして送られてくるword情報をインスタンス変数に代入 @word = Word.find(params[:word_id]) end end 予定している動きとしては、以下の通りです。 「お気に入りに追加」ボタンを押すと、その単語の情報がFavoritesControllerにパラメーターとして送られてくる。 (params(送られてきたパラメーターをハッシュのような構造で格納したもの)によって、設定したルーティングのURLに含まれているword_idがFavoritesコントローラーのcreateアクションへ渡される) before_actionにより、コントローラで定義されたアクション(今回の場合、createアクション)が実行される前に、set_wordアクションが実行され、findメソッドを使用し引数で指定したidのレコードを取得し、インスタンス変数@wordに代入。(引数では、送られてきたパラメーターに含まれるwordのid情報をparams[:キー名]で取り出している) createアクションが実行され、word_idには、パラメーターとして送られてきたwordのidを値に持つインスタンス(お気に入り)を生成し、インスタンス変数@favorieに代入。 この動きを実現させるために、リスト1に関わる、「wordのid情報をパラメーターとしてURLに含める」という作業が必要になります。その上で、コントローラーがその情報を受け取り、アクションが実行され、お気に入りインスタンスが生成されるからです。この場合、ルーティングのネストを意識しなければなりません。 ルーティングのネスト ネストとは入子構造のこと。 まず、ネストを意識せずにルーティングを記述すると以下の通り。 config/routes.rb Rails.application.routes.draw do resources :word resources :favorites, only: [:create, :destroy] end この状態で、ターミナルにて、rails routesを実行します。 ターミナル Prefix Verb URI Pattern Controller#Action favorites POST /favorites(.:format) favorites#create favorite DELETE /favorites/:id(.:format) favorites#destroy これだとパス(URL)の中に、どのwordに対してお気に入り機能が働いているかを示す情報がありません。 お気に入り機能が働く際には、どのワードに対してのものなのかをパスから判断し、かつコントローラーにその情報を渡したいので、ルーティングのネストを用います。 以下のように記述。 config/routes.rb Rails.application.routes.draw do resources :word do resources :favorites, only: [:create, :destroy] end end ターミナルにて、rails routesを実行します。 ターミナル Prefix Verb URI Pattern Controller#Action word_favorites POST /words/:word_id/favorites(.:format) favorites#create word_favorite DELETE /words/:word_id/favorites/:id(.:format) favorites#destroy パスの:word_idという部分に記述された値は、パラメーターとして送られます。 このように、ネストを利用すればwordのid情報を含めることができます。 ルーティングをネストさせる一番の理由は、アソシエーション先のレコードのidをparamsに追加してコントローラーに送るところにあると言えます。 この:word_idの箇所へ、お気に入り機能が働くと結びつくワードのidを記述すると、paramsのなかにword_idというキーでパラメーターが追加され、コントローラーで扱うことができます。 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む