20210504のRailsに関する記事は18件です。

[日記]RailsアプリをEC2を使ってAWSにデプロイするまで

はじめに 自作のRailsアプリをAWSにデプロイしその経過を備忘録としてQiitaに投稿してきました。 その投稿をチャプター形式で下記にまとめておきます。 自分用ですので画像でわかりやすく解説するなどはしていません。 環境 Ruby 2.6.6 Rails 6.0.3 nginx 1.12.2 PostgreSQL 11.5 node 12.22.1 yarn 1.22.5 rbenv 1.1.2 日記 [Chapter-1]VPCの設定 [Chapter-2]EC2の設定 [Chapter-3]EC2のサーバー環境構築 [Chapter-4]EC2にRailsアプリの配置 [Chapter-5]ロードバランサー(ELB)の作成 [Chapter-6]ACM(AWS Certificate Manager)でSSL証明書を取得 [Chapter-7]Route53の設定 [Chapter-8]https化に向けたロードバランサーの設定 [Chapter-9]https化によるRoute53の設定 [Chapter-10]https化によるNginxの設定 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rails】validateをかけてみた。

バリデーションを設定 class User < ApplicationRecord #ハイフンあり、7桁の整数 VALID_ZIPCODE_REGEX = /\A\d{3}[-]\d{4}\z/ #メールアドレス VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i #電話番号 ハイフンあり、整数 VALID_PHONE_REGEX = /\A[0-9-]+\z/ validates :first_name, presence: true, length: { maximum: 10 } # 存在する、かつ、10文字以下。 validates :last_name, presence: true, length: { maximum: 10 } validates :zipcode, presence: true, format: {with: VALID_ZIPCODE_REGEX } validates :prefecture, presence: true, length: { maximum: 5 } validates :municipality, presence: true, length: { maximum: 10 } validates :address, presence: true, length: { maximum: 10 } validates :email, presence: true, uniqueness: true, format: { with: VALID_EMAIL_REGEX } validates :phone_number, presence: true, length: { maximum: 15 }, format: { with: VALID_PHONE_REGEX } validates :password, presence: true, length: { minimum: 6, maximum: 15 } belongs_to :user_classification has_secure_password end 苦戦したところ 電話番号の/\A[0-9-]+\z/ について、"9"の後の"-"によって、 0~9の数字と、"-"はOKになる。 ハイフンをどこに入れても許可する設定が中々分からなくて苦労した。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpec導入してCircleCIに組み込んで見る

今までプロダクト優先で自動テストを書くのが億劫だったことを反省し、ついに意を決してRSpecを導入しました。 目標は 重要なモデルのバリデーションテスト コントローラー(request)の200確認・DB更新確認 ボタンやリンク経由で画面遷移のシナリオテスト(フィーチャーテスト) CircleCIでRSpecのテストをデプロイ前に入れ込み、OKとならなければデプロイしないようにする をすることです。もちろん究極は先にSpecを書いて、コードを実装するテストドリブン開発が理想ですが、今回後付になってしまったので現状の挙動を担保するという思想でテストを作成してます。 ただ、テストコードを書くと、意外なことにModelを端折ってしまって書いてないので、テストコード自体が動作しないというケースもあったりしてソース自体にも手を加える必要は出てきます。そうすると動いているソースに手を入れない的な昔からあるようなセオリーで押しきれないことがわかります。これもソースのメンテナンス性を上げるためのRSpecの効能と考えていいでしょう。 Gemfile group :development, :test do gem "rspec-rails" # RSpec本体 gem "factory_bot_rails" # FactoryBot gem 'spring-commands-rspec' # RSpecのパフォーマンスを上げるGem gem 'capybara', '~> 2.15.2' # フィーチャーテストのためのCapibara gem 'rspec_junit_formatter' # CircleCI、JenkinsなどCIでSpec結果を読みやすくするためのGem end ちなみにRSpecは実行すると自動的に環境はRAILS_ENV=testになります。なので専用のデータベースをdevelopment用と別に作成する必要があります。またRails 5.1以降はテストで発生したデータを消すためのdatabase_cleanerは不要だそうです。これは5.1以降はテスト後にDBが自動的にRollbackしてデータを消すようになっているためだとか。データベースの作成に合わせてDBの設定にも以下を追加します。ちゃんとテスト用のDB名にしてあげるのがミソです。 config/database.yml # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: test_db また、config/environments/test.rbも作成します。環境別固有設定を施すためです。 ほとんどdevelopment環境と一緒なのでconfig/environments/development.rbをコピーするだけですが、自分はweb_consoleを使っていてRSpecが動かないことがあったので以下の設定を追加しています。 config/environments/test.rb # web_console config.web_console.development_only = false ここまでお膳立てしてようやく bundle とジェネレーターで環境を整えます。 bash # RSpecインストール $ bundle install # 定型的な設定ファイルの作成 $ rails generate rspec:install Running via Spring preloader in process 266 create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb rails_helper.rbとspec_helper.rbの役割の違いはよくわからないですが、ほとんどのSpecファイルにrails_helper.rbへの読み込みが入るので、こちらにもっぱらヘルパーの設定を加えます。 今回はdevise使うのでそれ用のヘルパーとダミーデータ作成のためのFactoryBotに設定のヘルパーを加えています。コードの最後の方に追加しました。 spec/rails_helper.rb config.include Devise::Test::IntegrationHelpers, type: :request config.include FactoryBot::Syntax::Methods Deviseを使用しているアプリケーションの場合、ヘルパー本体はこちらに書きます。 spec/support/request_spec_helper.rb module RequestSpecHelper include Warden::Test::Helpers def self.included(base) base.before(:each) { Warden.test_mode! } base.after(:each) { Warden.test_reset! } end def sign_in(resource) login_as(resource, scope: warden_scope(resource)) end def sign_out(resource) logout(warden_scope(resource)) end private def warden_scope(resource) resource.class.name.underscore.to_sym end end ベースとなる準備は以上です。 Seed-fuでマスターデータを用意 アプリケーションにもよると思いますが、ある程度マスターデータが用意されてないと凝ったテストが出来ないという人はFactoryBotではなくてSeedでDBに値を事前に入れておくのがいいかと思います。Railsに元からあるseedよりもseed-fuの方が使い勝手良さそうなのでこちらをインストールします。データの自動投入に汎用的に使えるGemです。 Gemfile # Seed -fu gem "seed-fu" bundleで入れたら以下のファイルを作成します。 db/seed.rb Spree::Core::Engine.load_seed if defined?(Spree::Core) Spree::Auth::Engine.load_seed if defined?(Spree::Auth) あとは投入データを作成します。投入するためのスクリプトはこちらです。これがそのままDBの定義とseedの実行母体になります。 db/fixtures/test/001_areas.rb require 'csv' csv = CSV.read('db/fixtures/test/001_areas.csv') csv.each do |csvdata| Area.seed do |s| s.id = csvdata[0] s.prefstate_id = csvdata[1] s.photo = csvdata[2] s.lat = csvdata[3] s.lng = csvdata[4] s.created_at = csvdata[5] s.updated_at = csvdata[6] end end 以下データ。 db/fixtures/test/001_areas.csv 1,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00" 2,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00" 3,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00" ちなみにdb/fixtures/の下のtestは環境名です。上記を設定したら RAILS_ENV=test rails db:seed_fu でデータ投入できますが、この際に環境ごとに投入データを変えられます。そのためにディレクトリで分けているのです。ちなみに実行はファイルの接頭辞の番号順に行われますので、外部キー制約など順番を考慮したい場合はうまく活用してください。 その他DB操作で便利なコマンド RAILS_ENV=test rails db:reset #テーブル作り直し RAILS_ENV=test bundle exec rake db:schema:load #テーブル作り直し RAILS_ENV=test rails db:seed_fu FILTER=029_styles FIXTURE_PATH=./db/fixtures/test #フィクスチャーごとにデータ投入 FactoryBotの作成 テストのたびにbeforeとかにDBのレコードを作成して用意するのが嫌(というかソースも見づらくて気持ち悪い)なので、データの生成をスマートにするためにFactoryBotというツールがあります。元々FactoryGirlという名称でしたが、ジェンダーコンシャスの流れを受けて今の名前になりました。既にGemfileに追加してインストールしているので後はファイルをspec/factories配下に書いていくだけです。必要となる各モデルごとに作成していきます。 FactoryBot.define do factory :user do sequence(:name) { |n| "TEST_NAME#{n}"} # 名前のバリデーションにかからないもの sequence(:email) { |n| "TEST#{n}@example.com"} # メールのバリデーションにかからないもの password { 'password' } # パスワードのバリデーションにかからないもの end end 番号などシーケンシャルなものを挿入したい場合はsequenceというのを使います。 FactoryBot.define do factory :review, class: Review do restaurant_id {1620} user_id {1} rating {5} title {'What a hell?'} image {Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/image.jpg'))} comment {'It was yummy, indeed'} end end 画像データをカラムに入れている場合は上記のように画像のダミーデータをspec/fixtures配下に置いてRack::Test::UploadedFile.newとします。 リレーションがあるモデル(has_one/has_many/belongs_to)の場合、 app/models/menu.rb class Menu < ActiveRecord::Base has_many :menu_translations, dependent: :destroy app/models/menu_translation.rb class MenuTranslation < ActiveRecord::Base belongs_to :menu 以下のようにFactoryBotを作成します。 spec/factories/menu.rb FactoryBot.define do factory :menu, class: Menu do restaurant_id {1} menu_type {5} image {Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/image.jpg'))} last_update_user_id {1000} add_attribute(:public) {'1'} association :restaurant end end ちなみにカラム名がRSpecの予約語とかぶってしまうものを使っている場合(aliasとかpublicなど)、add_attribute(:public)という書き方をすることが出来ます。 spec/factories/menu_translation.rb FactoryBot.define do factory :menu_translation_ja, class: MenuTranslation do menu_id {} menuname {'パルミジャーノとケールのイタリアン菜園サラダ'} description {'SサイズとMサイズがあります。'} price {1000} currency {'円'} lang {'ja'} association :menu end end associationでリレーションを定義します。 Modelのスペック deviseをログイン認証機構に用いた場合のモデルのテストは以下の通りです。 deviseで注意すべきはモデル(今回だとUser)に書いてないバリデーションもあり、そちらもテストするのか、それ以外のカスタムバリデーションだけテストするのかを考えてコードを記載します。デフォルトではメールアドレスとパスワードの存在チェックはdeviseに組み込まれているようでした。 それ以外は普通にバリデーションやメソッドについてテストを記載します。 spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do let(:user) { build(:user)} describe 'Check Validations' do it 'default OK' do expect(user.valid?).to eq(true) end it 'if no password, then NG' do user.password = '' expect(user.valid?).to eq(false) expect(user.errors[:password]).to include 'パスワードを入力してください' end it 'if no name, then NG' do user.name = '' expect(user.valid?).to eq(false) end it 'if name exceed 120 letters, then NG' do user.name = '1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567890000000000000123456789012345678901234567890' user.completed_at = Time.current expect(user.valid?).to eq(false) end end end こちらがModelの本体です。これをテストするためにSpecを記載しました。 app/models/user.rb class User < ApplicationRecord mount_uploader :pic, ImageUploader attr_accessor :current_password validates :name, length: { minimum: 1, maximum: 120 }, if: :is_registration? validates :name, presence: true end FactoryBotには正常系を作成しておいて、バリデーションのテストの中で異常データを挿入してvalid?を試すイメージです。 Controllerのスペック EverydayRails ‐ RSpecによるRailsテスト入門というRSpecの有名書籍によればコントローラーのテストはやがてなくなり、requestsのテストとして集約されるとのことです。いろいろな現場でテストを書かせてもらいましたが、contorollerにテストを書いているところは多いです。コントローラーにアクセスして、所定のキーワードがあるかどうか、関連するモデルの数が増えたり減ったりするかといったテストです。スマホアプリ構築の流れでWebAPIの開発というパターンも多いことからこれをテストするためにrequestsにテストを書くということのようですが、ここにControllerも集約されるぽいです。認識違っていたらすみません。 以下は弊社サービスVegewelでレストランガイドをやっておりまして、ページを表示、レビューの投稿をテストするコードを書いたものです。 spec/requests/restaurant_spec.rb require 'rails_helper' RSpec.describe RestaurantController, type: :request do describe "GET index" do before do @restaurants = FactoryBot.create(:restaurant) end it "responds successfully" do get "/restaurant" expect(response).to be_success end end describe "GET show on restaurant detail" do it "responds successfully" do get "/restaurant/#{@restaurants.id}" expect(response).to be_success end end end コントローラーのテストの基本の作りはシンプルです。 ルーティングに合わせてget, post, patch, put, delete...を発効して結果をexpectのtoに期待値を書いていくというものです。 require 'rails_helper' RSpec.describe ReviewsController, type: :request do describe "POST review" do let(:review) { FactoryBot.create :review } before do @user = FactoryBot.create(:user) @restaurant = FactoryBot.create(:restaurant) @user.confirm # deviseのメール認証が必要なケースでは必須 sign_in @user end it "responds successfully" do expect{ post '/reviews', params: { user_id: @user.id, restaurant_id: @restaurant.id, review: review } }.to change(Review, :count).by(1) end end end post など更新がかかるアクションにはパラメータを設定しておきます。 Factoryの使い方としては事前評価と遅延評価という考え方があり、もともとAだったものがBになるということをテストするならベースとなるデータを事前評価ということでlet!でデータをcreateなどしておいて、更新の結果で増減があるか確かめるという流れです。 単純にレストランデータを作って該当URL(restaurant/1234みたいな)もので表示するだけなら遅延評価でletでデータを作成するので十分かと思います。 ちなみにフォームは確認画面とかはなく、登録・更新ボタン一発で変更が加わるようにしています。 追加したら当該モデルの数が+1、更新したら変更後の該当カラムが想定通りに変更されていることを確認するようなコードにしています。 リレーションのあるモデルを同時に更新する場合のコントローラーのSpecの書き方 コントローラーに以下のようなparamsの設定があるとします。 app/controllers/menu_controller.rb private def menu_params params.require(:menu).permit(:id, :restaurant_id , :image1, :image2, :image3, :last_update_user_id, :public, menu_type_list: [], menu_translations_attributes:[:id, :menu_id, :menuname, :description, :price, :currency, :lang]) end パラメータに親子関係を設定したものとFactoryの設定にassciationを仕込ませておいた上で以下のようなテストコードを書きます。 spec/requests/menu_spec.rb # 新規作成 describe "POST /menu with authentication" do before do @admin_user = FactoryBot.create(:admin_user) sign_in @admin_user end it "Create new menu for Japanese" do menu_translation_params_ja = { menu_translations_attributes: { "0": FactoryBot.attributes_for(:menu_translation_ja) } } menu_params = FactoryBot.attributes_for(:menu, restaurant_id: restaurant.id).merge(menu_translation_params_ja) expect{ post '/menu', params: { menu: menu_params } }.to change(Menu, :count).by(1) end # 更新 describe "PATCH /menu with authentication" do let(:restaurant) { FactoryBot.create(:restaurant) } let!(:menu) { FactoryBot.create(:menu, restaurant_id: restaurant.id) } before do @admin_user = FactoryBot.create(:admin_user) sign_in @admin_user end it "Update menu for Japanese" do menu_translation_params_ja = { menu_translations_attributes: { "0": FactoryBot.attributes_for(:menu_translation_ja, menuname: "ベジプレート(大豆ミートの鶏から風)", price: 500, menu_id: menu.id) } } menu_params = FactoryBot.attributes_for(:menu, id: menu.id, restaurant_id: restaurant.id).merge(menu_translation_params_ja) patch "/menu/#{menu.id}", params: {menu: menu_params} expect(menu.menu_translations.first.menuname).to eq("ベジプレート(大豆ミートの鶏から風)") expect(menu.menu_translations.first.price).to eq(500) end end 上記同様にフォームは確認画面とかはなく、登録・更新ボタン一発で変更が加わるようにしています。 追加したら当該モデルの数が+1、更新したら変更後の該当カラムが想定通りに変更されていることを確認するようなコードにしています。 以上でコントローラーのテストについて記載しました。 サイトの表側のふるまいをテストするフィーチャーテスト Capybaraを用いてサイト上の動作をシミュレートしたテスト(フィーチャーテスト)をすることが出来ます。 RSpecをやるまではテストのコードを書くというとこっちのイメージでした。Seleniumはこちらのイメージと言えるでしょう。 試しにサイトのトップページからリンクをたどって別ページに飛んで、そこから検索するというスクリプトを書いてみます。 spec/features/projects_spec.rb require 'rails_helper' RSpec.feature "Projects", type: :feature do scenario "Vegewel success scenario in English" do # Vegewel TOP visit '/en' expect(page).to have_content "Vegewel restaurant guide" expect(page).to have_content "Vegewel Style" # Restaurant Page click_link ("Restaurant") expect(page).to have_content "Tasty & Healthy Restaurants" expect(page).to have_content "VESPERA" # Search Restaurant fill_in('q[g][1][restaurant_search]', with: 'Cafe') check('q[g][0][veganmenu_eq]') click_button ("Search") expect(page).to have_content "Cafe*teria HANIWA" end end ちなみに 特定のページを開く:visit リンクを踏む:click_link ボタンを押す:click_button チェックボックスを押す:check テキストボックスに入力:fill_in で制御できます。ボタン押したり、リンククリックしたりの場所の特定はIDなどのセレクタや、ラベルなどが表示されていればその文言で指定することが出来ます。可読性を上げるためにボタンのラベルに指定するとかもアリでしょう。 その他の動作に関してはこちらを参照して、色々シミュレートしてみることが出来るかと思います。 https://qiita.com/morrr/items/0e24251c049180218db4 ご確認ください。 RSpecのテスト $ bundle exec rspec で全ての(モデル、コントローラー、フィーチャー)のテストは実行されます。個別に実行したければファイルを指定すればOKです。テスト通ればSuccessと表示され、NGだとエラー内容が表示されます。 CircleCIにRSpecのテストを組み込み もともとBitbucketの特定ブランチ(Staging、Master)にソースをコミットしたらデプロイする仕組みを構築していました。そこにRSpecのテストを追加してみました。以下.circleci/config.ymlです。 本当は全体はかなり長いのですが、RSpec組み込んだところだけフォーカスしています。 .circleci/config.yml version: 2.1 orbs: ruby: circleci/ruby@0.1.2 jobs: build: working_directory: ~/xxxx parallelism: 1 docker: - image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37 steps: - checkout - run: (諸々Deployコマンド) rspec: parallelism: 3 docker: - image: circleci/ruby:2.5.0-node-browsers environment: - BUNDLER_VERSION: 1.16.1 - RAILS_ENV: 'test' - image: circleci/mysql:5.7 environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'true' - MYSQL_ROOT_HOST: '127.0.0.1' steps: - checkout - restore_cache: key: v1-bundle-{{ checksum "Gemfile.lock" }} - run: name: install dependencies command: | gem install bundler -v 2.0.2 bundle install --jobs=4 --retry=3 --path vendor/bundle - save_cache: key: v1-bundle-{{ checksum "Gemfile.lock" }} paths: - ~/circleci-demo-workflows/vendor/bundle # Database setup - run: mv ./config/database.yml.ci ./config/database.yml - run: name: Databasesetup command: | bundle exec rake db:create bundle exec rake db:schema:load bundle exec rake db:seed_fu # run tests! - run: name: Run rspec command: | mkdir /tmp/test-results TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ circleci tests split --split-by=timings)" bundle exec rspec \ --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ --format progress \ $TEST_FILES # collect reports - store_test_results: path: /tmp/test-results - store_artifacts: path: /tmp/test-results destination: test-results workflows: version: 2 build-n-deploy: jobs: - rspec: filters: branches: #Spec走らせるブランチを限定(これないとフィーチャーブランチのコミット&Pushで勝手にCircleCIが走ってしまう) only: - staging - master - build: requires: - rspec # rspecしてからDeployするように filters: branches: only: - staging - master こんな感じでJobを分割してRspecとしたところに - テスト環境構築 - Seedデータを投入 - RSpecテスト実行 を実行しています。 workflowsのところに全体的な流れとして、先にRSpecを流してからOKならDeployを実行する流れにしています。 あと、これが最も重要なポイントですが、これを実行するためにCircleCIに課金しました。 1ヶ月$30 です。 複数Job実行にするのは無料プランでは出来なかったからです。この金額で25000ポイントもらえるので、各リソースの利用状況ごとにポイントが減らされる仕組みになっています。 弊社のサービスは1回のデプロイで300〜400ポイントくらい消費するので実行は慎重にやることにしています。ポイントが無くなってきたら、自動的に+$30してポイント追加になります。 課金するとJobの並行実行も可能です。複数の同時デプロイなどに使えるかと思うので使い方はご検討ください。 その他トラブル対応 特定のURLで200が返ってこなくてエラー 例えば以下のようなテストを書いたとして、エラーになって返ってこないケースです。 it "Login returns 200" do get index_path expect(response).to have_http_status "200" end エラーはこんな感じです。 Failure/Error: expect(response.status).to eq 200 expected: 200 got: 302 つまりリダイレクトしているってことですよね?調べたところapplication_controller.rbでdeviseログイン時やhttp->httpsリダイレクトで302をやらかしていました。また、Staging環境ではBasic認証かけたりとか色々やっていて、条件分岐に環境変数を使っていたことからtest環境へのケアが出来ていなかったのも要因でした。 上記エラーになったらapplication_controller.rbのbefore_actionを片っ端からコメントアウトして1個づつつけたり外したりして試してみるのが吉です。 アプリケーションFQDNがwww.example.comとなってしまう?bad URIも発生 テストに以下のようなコードを書いたところbad URIとなり、しかもアプリケーションはwww.example.comというFQDNになってました。 require 'rails_helper' RSpec.describe "Restaurants", type: :request do describe "GET /index" do it "index responds successfully" do get :index expect(response).to be_success end end end 以下エラー URI::InvalidURIError: bad URI(is not URI?): http://www.example.com:80index これは get :index を get '/' とすることでテストが通りました。:indexとすることで文字列にindexを足してしまうようです。また、www.example.comはRailsでActionPackの中にTestのSessionを司る箇所があり、そこで DEFAULT_HOST='www.example.com' と定義されていて、これがRSpec内でURLに指定がなければ自動的に設定されてしまうのが原因でした。ただ、通常のテストではwww.example.comが問題になることがなく「そういうもの」と考えて特に気にしなくて良い模様です。自分の場合はpryで何度止めてもこのFQDNがサーバ名として定義されているので焦りましたが、RSpecのメンターしてくれた先生に聞いたら気にしなくてOKと言われて安堵しました。 CircleCIでMySQLのDockerImage構築中にDBセットアップを始めようとしてエラーになる この図で言うところの Container circleci/mysql5.7 のところが終わっていないのに、DBのセットアップが始まってしまうということがありました。そもそも当初CircleCIは課金していないので基本的に複数Jobを回すようにしておらず、以下のように環境構築、RSpec実行、AWSCLIの設定、ElasticBeanstalkのデプロイまでまとめてやろうとしていました。 その中でいうとimage: circleci/mysql:5.7のところが長い時間かかっているのですが、これと同期をとる方法がわかりませんでした。 version: 2 jobs: build: working_directory: ~/xxxx parallelism: 1 shell: /bin/bash --login environment: CIRCLE_ARTIFACTS: /tmp/circleci-artifacts CIRCLE_TEST_REPORTS: /tmp/circleci-test-results docker: - image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37 - image: circleci/ruby:2.5.0-node-browsers environment: - BUNDLER_VERSION: 1.16.1 - RAILS_ENV: 'test' - image: circleci/mysql:5.7 environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'true' - MYSQL_ROOT_HOST: '127.0.0.1' steps: - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS - run: working_directory: ~/xxxx command: pip install pip==20.3.4 - run: working_directory: ~/xxxx command: pip install urllib3==1.26 - run: working_directory: ~/xxxx command: pip install awsebcli --upgrade --user - checkout - run: name: install dependencies command: | gem install bundler -v 2.0.2 bundle install --jobs=4 --retry=3 --path vendor/bundle # Database setup - run: mv ./config/database.yml.ci ./config/database.yml - run: name: Databasesetup command: | bundle exec rake db:create bundle exec rake db:schema:load # run tests! - run: name: Run rspec command: | mkdir /tmp/test-results TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ circleci tests split --split-by=timings)" bundle exec rspec \ --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ --format progress \ $TEST_FILES # collect reports - store_test_results: path: /tmp/test-results - store_artifacts: path: /tmp/test-results destination: test-results - run: name: Deploy command: | if [ "${CIRCLE_BRANCH}" == "master" ]; then echo "Deploy production" eb deploy Vegewel-production else echo "Deploy staging" eb deploy vegewel-staging fi workflows: version: 2 build-n-deploy: jobs: - build: filters: branches: only: - staging - master 最終的には課金して複数JobでRSpecとデプロイという形にしましたが、CircleCIのサポートによればDockerizeを使って待機させることもできるそうです。こちらのドキュメントを参照ください。 https://circleci.com/docs/ja/2.0/databases/#dockerize-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E4%BE%9D%E5%AD%98%E9%96%A2%E4%BF%82%E3%81%AE%E5%BE%85%E6%A9%9F acts_as_taggable_onを使った箇所のテスト 動的なタグ生成のためにacts_as_taggable_onを使って開発しているところもあると思います。非常に便利で私もよく利用しています。 これのテストですが、自分は以下のようにしました。前提として料理の種類をCuisineとしていて、コードのID:1に対してCuisineTranslationの日本語(lang:ja)に”和食”、英語(lang:en)に”Japanese”のように2レコード設定しているようなデータ構造とお考えください。また、タグ用のテーブルはtags, taggingsの2つです。 app/models/cuisine.rb class Cuisine < ActiveRecord::Base ActsAsTaggableOn::Tagging.table_name = 'taggings' ActsAsTaggableOn::Tag.table_name = 'tags' has_many :cuisine_translations end app/models/cuisine_translation.rb class CuisineTranslation < ActiveRecord::Base belongs_to :cuisine belongs_to :restaurant scope :with_lang , -> { where(lang: I18n.locale )} end これを扱うRestaurantモデルは app/models/restaurant.rb class Restaurant < ActiveRecord::Base ActsAsTaggableOn::Tagging.table_name = 'taggings' ActsAsTaggableOn::Tag.table_name = 'tags' acts_as_taggable_on :stations, :lines, :cuisines こんな感じです。Factoryの設定はTagに対して行います。 spec/factories/tag.rb FactoryBot.define do factory :cuisine_list, class: ActsAsTaggableOn::Tag do name {Cuisine.first.id} end として、RestaurantのFactoryに以下のようにかけば動くはずです。 spec/factories/restaurant.rb FactoryBot.define do factory :restaurant, class: Restaurant do station_list {[FactoryBot.build(:station_list)]} line_list {[FactoryBot.build(:line_list)]} cuisine_list {[FactoryBot.build(:cuisine_list)]} 最後に かなり長い文章になってしまいました。こういうときは記事を分けるのがいいかなと思いつつ、大半は他のQiitaにも書いてそうなことなので付加価値出そうとしてボリューミーになってしまいました。多少なりともお役に立つ内容があれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ransackで、全件一覧表示を消してみた(備忘録)

はじめに 今回、ransackというgemとJavaScriptを用いて、検索機能を実装した際に(勘違いして)いろいろ試したので、早速アウトプットしていきたいと思います。 この記事を読むとどうなるのか? ・ransackについて少し詳しくなる ・キーワード検索する前に、一覧表示されているビューを表示しないようにできる (必要ない実装でしたが、勘違いして、かなりの時間を費やしました) 環境 Rails 6.1.2 前提条件 ransackは導入済み 手順 ①完成しているアプリ usersコントローラのnameカラムとageカラムについて検索結果が表示されています。 ・現在のビュー ・実装コード users_controller.rb class UsersController < ApplicationController def index @q = User.ransack(params[:q]) @users = @q.result(distinct: true) end end index.html.erb <div> <%= search_form_for @q do |f| %> <%= f.label :name_cont, "氏名" %> <%= f.search_field :name_cont %> <%= f.label :age_eq, "年齢" %> <%= f.search_field :age_eq %> <%= f.submit "検索" %> <% end %> <table> <tbody> <thead> <tr> <th>名前</th> <th>年齢</th> </tr> </thead> <% @users.each do |user| %> <tr> <td><%= user.name %></td> <td><%= user.age %></td> </tr> <% end %> </tbody> </table> </div> このデフォルトの一覧表示を「消さないといけない!」と勘違いをして、いろいろ試行錯誤しました。(正しくは、表示しないように設定しました。) ・修正後のビュー さっきまで、表示されていた一覧が消えました!! ・修正したコード index.html.erb <div> <%= search_form_for @q do |f| %> <%= f.label :name_cont, "氏名" %> <%= f.search_field :name_cont %> <%= f.label :age_eq, "年齢" %> <%= f.search_field :age_eq %> <%= f.submit "検索" %> <% end %> <table> <tbody> <thead> <tr> <th>名前</th> <th>年齢</th> </tr> </thead> ##ココにif文を追加 <% if @q.conditions.present? %> <% @users.each do |user| %> <tr> <td><%= user.name %></td> <td><%= user.age %></td> </tr> <% end %> <% end %> </tbody> </table> </div> 調べたこと&考えたこと ①デフォルトで、どのように処理されているのか? Ransackは、シンプルモードとアドバンストモードの2つのモードを使用できます。 シンプルモード このモードはメタサーチのような動きをし、とても少ない労力でセットアップすることができます。 シンプルモードを使用する場合の注意点としては以下です。 ①デフォルトで送られるparamsのキーは「:search」ではなく「:q」です。これは主にクエリの文字列を短くするためですが、高度なクエリ(下記)はほとんどのブラウザでURLの長さの制限に違反し、HTTP POSTリクエストへの切り替えが必要です。このキーは設定可能です。 users_controller.rb class UsersController < ApplicationController def index ③@q = User.②ransack(①params[:q]) ⑤@users = ④@q.result(distinct: true) end end ①デフォルトで「paramsキー」として送信されている  (ビューファイルから送られてくるパラメーター) ②ransackメソッド。送られてきたパラメーターを元にテーブルからデータを検索するメソッド。 ③ユーザー情報(今回は、名前と年齢)を「@q」へ代入(@qが値を保持している) ④「@q」に対して、「.result」することで検索結果を取得 ⑤「@users」へ代入される ⑥「index.html.erb」の「@users」へ渡されて表示される このような流れで処理されているとわかったので! ビューで条件分岐すれば良さそう!と思い、色々調べてみました! ②present?メソッド 変数.present? ・resent?メソッドは、変数に値が入っていればtrueを。変数そのものが存在しない時は、falseを返す index.html.erb(一部抜粋) <% if @q.conditions.present? %> <% @users.each do |user| %> <tr> <td><%= user.name %></td> <td><%= user.age %></td> </tr> <% end %> <% end % これによって、検索していない時は、一覧表示できないように設定ができました! まとめ ・今回、まったく関係のない実装に時間を費やしてしまいましたが、そのおかげでransackについて少し理解ができました。他に良い方法など、ございましたらコメント頂けると幸いです 参考文献 【Ransack翻訳】Railsで検索機能を簡単に実装できるgem「Ransack」のREADMEを翻訳してみた activerecord-hackery/ransack: Object-based searching. - GitHub Railsのpresentの使い方を現役エンジニアが解説【初心者向け】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オリジナルアプリの要件定義(仮)

今日から、オリジナルアプリ(名前は未定)を作成しようと思う。 そのため要件定義をしようと思うので、未経験の人は参考程度にどうぞ。 ちなみに、私も未経験だ! 要件定義の項目 アプリの名前 アプリの概要 制作背景 ペルソナ 必要機能 使用ツールと使用言語 目標制作期間と実際の制作期間 参考アプリケーション(あれば) 私は未経験で、実務での要件定義をしたことはないので、「とりあえずこれくらいは載せた方がいい」と思ったものを選びました。 また、「目標制作期間と実際の制作期間」については、Read.Meに記載する アプリの名前 未定 つのるん(募るん)みたいにしようと考えています。 アプリ概要 参加者を募るためのアプリケーション タイトル・内容・時間などを入力し参加者を募ることができる 制作背景 ポートフォリオを充実させるため。 ペルソナ 場面 社内ツールを想定。主にPCで使用する。 勉強会でも遊びでも。 人物 年齢:20〜40代、幅広く 性別:どちらも 職業:PCをよく使う職業 何の課題を解決できるか LINEなどのチャットツールでも募ることはできるものの、人数制限など細かいことは自分で確認する必要がある。 主催者側の負担が大きいため、その負担を減らすべく作成。 社内のコミュニケーションにも一役買ってもらう。例えば、「野球しませんか」で募る→Aさんが参加する→Aさんは野球に興味があるということがわかる→話が広がる。 必要機能 ユーザー管理機能 ユーザー名・メールアドレス・パスワードを使って新規登録 メールアドレス・パスワードを使ってログイン ユーザー情報編集機能 ユーザーの写真・名前を変更できるようにする グループ作成・削除機能 グループタイトル・人数制限・日時・詳細・タグ・準備するものを入力してグループを作成できる 削除ボタンを押せばグループは削除される グループ参加・辞退機能 グループ詳細画面から参加辞退ができる タグ機能 グループ作成時にタグをつける。「途中参加OK」「リモートで」など。 チェックボックスを使う。 コメント機能 グループに対して質問などをする メール機能→ユーザー間チャット機能に変更 1対1で話し合える場を設ける。 変更の理由 個人的な意見だが「メール」は少しばかり堅い印象がある。 社内ツールを想定しているので、少しばかりは堅苦しくても良いのだが、連絡の取りやすさなどからもチャットの方がよいと思いました。 使用ツールと使用言語 使用ツール Git/GitHub Trello VSCode Ruby on Rails heroku 使用言語 HTML CSS JavaScript MySQL Ruby 目標制作期間 全体の目標:1ヶ月間 --各機能の目標実装期間 ユーザー管理機能:1日 ユーザー情報編集機能:1日 グループ作成・削除機能:3日 グループ参加・辞退機能:3日 タグ機能:3日 --各テストの目標実装期間 ユーザー単体:1日 グループ単体:1日 グループ参加単体:1日 新規登録結合:1日 ログイン結合:1日 ログアウト結合:1日 グループ作成結合:1日 グループ削除結合:1日 グループ参加結合:1日 グループ辞退結合:1日  おしまい 本当はもっと、細かいところまで決めていくんだと思います。が、未経験がそんな細かいところまで気にしていたら、アプリ作り出す前にモチベーションが消えます。 つまりは、目指すところを重要視しましょう。要件定義とか詳細設計とかの仕事につきたいなら、その辺を全力で。 もしくは、勉強したいところを全力で。 ※この要件定義はいずれ編集されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】フォロー機能実装(コントローラー、view編)

完成GIF フォローする、フォローを外すを表示 フォロー中と、フォロワーの数を表示 非同期通信で実装 前回の記事はこちら 1.メソッド定義 followとunfollowのメソッドを、userモデルに定義する。 user.rb class User < ApplicationRecord def follow(user_id) relationships.create(followed_id: user_id) //フォローするボタンのメソッド end def unfollow(user_id) relationships.find_by(followed_id: user_id).destroy //フォローを外すボタンのメソッド end def following?(user) followings.include?(user) //フォローしているかどうかのメソッド end end find_byでは、与えられた条件にマッチするレコードのうち最初のレコードだけを返します。 include?は対象の配列に引数が含まれていればtrueを返し、含まれていなければfalseを返します。 2.controller編集 app/controllers/rerationships_controller.rb class RelationshipsController < ApplicationController before_action :authenticate_user! def create //フォローする current_user.follow(params[:user_id]) end def destroy //フォローを外す current_user.unfollow(params[:user_id]) end def followings //フォローした人一覧表示 user = User.find(params[:user_id]) @users = user.followings end def followers //フォロワー一覧表示 user = User.find(params[:user_id]) @users = user.followers end end 非同期通信で行うので、redirect_toは記述しません。 3.Viewの記述 viewは以下のようにして、countもつけている。 後述する非同期通信にてcountを変化させる。 app/views/relationships/_relation_count.html.erb <div class="d-inline-block mr-1"> <%= link_to "#{ user.followings.count }フォロー中", user_followings_path(user), class:"text-dark" %> </div> <div class="d-inline-block ml-1"> <%= link_to "#{ user.followers.count}フォロワー", user_followers_path(user), class:"text-dark" %> </div> クリックするとフォロワー一覧、フォロー一覧へ画面遷移する。 relationships/followers.html.erb <h5><%= full_name(@user)%>さんのフォロワー</h5> <div class="row"> <% if @users.exists? %> <%= render 'users/index', users: @users %> <% else %> <p>フォロワーはいません</p> <% end %> </div> @users.exists?では、フォロー、フォロワーがいるかいないかを判断します。レコードがあればtrueを返し、レコードがなければfalseを返します。 フォローボタンの記述です。 app/views/relationships/_relationships/btn.html.erb <% unless current_user == user %> //current_userがuserでなければtrue <% if current_user.following?(user) %>//current_userがfollowしていれば <%= link_to "フォローを外す", user_relationships_path(user.id), method: :delete, remote: true, class: "btn btn-primary" %> <% else %>//followしていなかったら <%= link_to "フォローする", user_relationships_path(user.id), method: :post, remote: true, class: "btn btn-success" %> <% end %> <% end %> 今回はUserの詳細画面にフォローボタンとフォローカウントの表示をさせます。 views/users/show.html.erb <div class="row"> <div class="col-sm-12 col-lg-5 text-center"> <div> <div><%= attachment_image_tag(@user, :profile_image,size:"300x350",fallback: "no_image.png")%></div> <h5 class="mt-3"><%= full_name(@user)%></h5> <div id="follow-count"> <%= render "relationships/relation_count", user: @user%> </div> <div id="follow-btn" class="mt-2"> <%= render "relationships/relation_btn", user: @user%> </div> </div> </div> 4.js.erbファイルの作成・記述 create.js.erbとdestroy.js.erbを作成します。 views/relationships/create.js.erb $("#follow-count").html("<%= j(render 'relationships/relation_count', user: @user ) %>"); $("#follow-btn").html("<%= j(render 'relationships/relation_btn', user: @user ) %>"); 1行目 follow-countに対してのjs内容 2行目 follow-btnに対してのjs内容
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] Rspecでテストコードの実装④

はじめに テストコードの実装③をみていない方はこちらからご覧ください。 結合テスト 結合テストとは、ユーザの一連の操作を再現して行うテストのことです。今まで行ってきた単体テストは、機能ごとでしたが、結合テストでは一連の流れを一気に行います(例:投稿機能であれば、ログイン→入力→投稿→表示の確認)。結合テストコードを実行するためには、System Specという技術を使用します。 System Spec(参考) System Specを記述するためには、Capybara(カピバラ)というGemを用います。これはすでにデフォルトでRuby on Railsに搭載されています。 group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of web drivers to run system tests with browsers gem 'webdrivers' end テストコードの具体的な実装 まずはいいつものようにファイルを生成していきましょう rails g rspec:system users 生成されたファイルに以下を記述していきます system/users_spec.rb it '正しい情報を入力すればユーザー新規登録ができてトップページに移動する' do visit root_path expect(page).to have_content('新規登録') visit new_user_registration_path fill_in 'Nickname', with: @user.nickname fill_in 'Email', with: @user.email fill_in 'Password', with: @user.password fill_in 'Password confirmation', with: @user.password_confirmation expect{ find('input[name="commit"]').click }.to change { User.count }.by(1) expect(current_path).to eq(root_path) expect( find('.user_nav').find('span').hover ).to have_content('ログアウト') expect(page).to have_no_content('新規登録') expect(page).to have_no_content('ログイン') end 一個づつ見ていきましょう。 visit 〇〇_path 〇〇のページへ遷移することを実現します。前回にやったgetと似ていますが、getは単にリクエストを送るのみです page visitで訪れた先のページの見える分だけの情報が格納されています。現在見ているページと考えて問題ないです have_content visitで訪れたpageの中に、文字列があるかどうかを判断するマッチャです。 expect(page).to have_content('X')とすれば、現在いるページにXがあるかどうか判断します have_no_content have_contentの真逆です。 fill_in fill_in 'フォームの名前', with: '入力する文字列'とすることで、任意の文字列をフォームに入力できます。フォーム名は検証ツールで調べてみましょう find().click find('クリックしたい要素').clickと記述することで、指定の要素をクリックができます。 ちなみに、click_on "文字列"としても同じ効果を得られますが、同じ文字列が2つ以上あると使えません change expect{ 何かしらの動作 }.to change { モデル名.count }.by(1)と記述することによって、モデルのレコードの数がいくつ変動するのかを確認できます。このとき、カッコが{}となることに注意です current_path 現在いるパスを示します hover find('ブラウザ上の要素').hoverとすると、その要素にカーソルを合わせることが可能です。hoverしないと表示されないものもありますので、こうした表記があります。 以上のマッチャ等をしっかり理解した上で、再度テストコードを見てみるとその意味はかなりわかりやすくなるのではないでしょうか 解説をコメントアウトにいれてありますのでご覧ください。 system/users_spec.rb it '正しい情報を入力すればユーザー新規登録ができてトップページに移動する' do # トップページに移動する visit root_path # トップページにサインアップページへ遷移するボタンがあることを確認する expect(page).to have_content('新規登録') # 新規登録ページへ移動する visit new_user_registration_path # ユーザー情報を入力する fill_in 'Nickname', with: @user.nickname fill_in 'Email', with: @user.email fill_in 'Password', with: @user.password fill_in 'Password confirmation', with: @user.password_confirmation # サインアップ(登録)ボタンを押すとユーザーモデルのカウントが1上がることを確認する expect{ find('input[name="commit"]').click }.to change { User.count }.by(1) # 現在のページがトップページへ遷移したことを確認する expect(current_path).to eq(root_path) # ある要素にカーソルを合わせるとログアウトボタンが表示されることを確認する expect( find('.user_nav').find('span').hover ).to have_content('ログアウト') # サインアップページへ遷移するボタンや、ログインページへ遷移するボタンが表示されていないことを確認する expect(page).to have_no_content('新規登録') expect(page).to have_no_content('ログイン') end (参考)その他よく使う表記まとめ have_selector 指定したセレクタが存在するかどうかを判断するマッチャ。have_selector ".クラス名または#id名"という形で記述できます。 have_link expect('要素').to have_link 'ボタンの文字列', href: 'リンク先のパス'と記述することで、要素の中に当てはまるリンクがあることを確認する。 なお、have_content同様に、have_no_linkもある all all('クラス名')でpageに存在する同名のクラスを持つ要素をまとめて取得できます。all('クラス名')[0]のように添字を加えることで指定版目のものを取得できます find_link().click 個人的にはあまり使いませんが、、、a要素で表示されているリンクをクリックするために用います。find().clickと似ていますが、find_link().clickはa要素のみに対して用いることができます。 おわりに お疲れ様でした。 今回は結合テストについて、簡単にまとめてみました。他にもいろいろなパターンがあるかと思いますので、ぜひ考えてみてください!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Railsインストール時のエラー

概要 Ruby on Railsをインストールする際に下記のようなエラーが発生しインストールが出来ませんでした。 gem install rails実行時のエラー内容 console > gem install rails Temporarily enhancing PATH for MSYS/MINGW... Building native extensions. This could take a while... ERROR: Error installing rails: ERROR: Failed to build gem native extension. current directory: C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/websocket-driver-0.7.3/ext/websocket-driver C:/Ruby30-x64/bin/ruby.exe -I C:/Ruby30-x64/lib/ruby/site_ruby/3.0.0 -r ./siteconf20210504-17032-oj9pk4.rb extconf.rb creating Makefile current directory: C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/websocket-driver-0.7.3/ext/websocket-driver make DESTDIR\= clean C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/make-0.3.1/bin/make:4:in `<top (required)>': undefined local variable or method `make' for main:Object (NameError) from C:/Ruby30-x64/bin/make:23:in `load' from C:/Ruby30-x64/bin/make:23:in `<main>' current directory: C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/websocket-driver-0.7.3/ext/websocket-driver make DESTDIR\= C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/make-0.3.1/bin/make:4:in `<top (required)>': undefined local variable or method `make' for main:Object (NameError) from C:/Ruby30-x64/bin/make:23:in `load' from C:/Ruby30-x64/bin/make:23:in `<main>' make failed, exit code 1 対処方法 結論から言うと、"gem uninstall make" を実行してmakeをアンインストールすれば解決しました。 VSCodeの拡張機能でRuby on Railsをインストールしていたことが原因かもしれません。 makeをアンインストール後に、再度Railsのインストールを実行したところ、無事インストールが完了しました。 console > gem uninstall make Remove executables: make in addition to the gem? [Yn] y Removing make Successfully uninstalled make-0.3.1 > gem install rails Temporarily enhancing PATH for MSYS/MINGW... Building native extensions. This could take a while... Successfully installed websocket-driver-0.7.3 Building native extensions. This could take a while... Successfully installed nio4r-2.5.7 Successfully installed actioncable-6.1.3.1 Successfully installed rails-6.1.3.1 Parsing documentation for websocket-driver-0.7.3 Installing ri documentation for websocket-driver-0.7.3 Parsing documentation for nio4r-2.5.7 Installing ri documentation for nio4r-2.5.7 Parsing documentation for actioncable-6.1.3.1 Installing ri documentation for actioncable-6.1.3.1 Parsing documentation for rails-6.1.3.1 Installing ri documentation for rails-6.1.3.1 Done installing documentation for websocket-driver, nio4r, actioncable, rails after 1 seconds 4 gems installed 参考URL
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ルーティングを理解する

ルーティングの役割 ルーティングはコントローラーを繋ぐ役割を持っていて、以下の順番で処理を行う。 ①URLを検索(リクエスト) ②ルーティング ③コントローラー ④ビュー ⑤レスポンスを返す ルーティング理解する 公式 get "URL" => "コントローラー名#アクション名" 実践 ブラウザから、「localhost:3000/home/top」というURLが送信された時 Rails.application.routes.draw do get "home/top" => "home#top" end 上記のように記述すると、homeコントローラーのtopアクションで処理されるようになる。 ⚠︎ "home/top"はURLを表している ⚠︎ "home#top"は"コントローラー名#アクション名" もし、上記のようなルーティングの記述がしてあるのに、「localhost:3000/home/hello」を送信すると、 ルーティングエラーが起きる。これは、URL(home/hello)に対応するルーティングが存在しないためである。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンフリクトが起こったGemの修正とActiveRecord::PendingMigrationErrorの対応

はじめに コンフリクトが起こった際に、下記の内容を修正したので、備忘録としてまとめました。 ・Gemファイルのインストール&アンイストールの手順(コンフリクトの原因) ・ActiveRecord::PendingMigrationErrorの対応手順 ※コンフリクトの状況を確認する方法は、この記事には記載しておりません。 状況 作業ブランチで編集したのち、プルリクを出したらコンフリクト (しかもGemファイル!Gemファイル修正したら次のエラー!) 環境 Rails 6.1.2.1 手順 ①コンフリクトの状況を確認する コンフリクトの解消手順は、この記事を参考にすすめました。(わかりやすかったです。) -Git コンフリクト解消手順 ②Gemのインストール ・インストール済みのGemの確認(差分がないか確認) $ gem list ・導入したいGemをインストール $ gem install gemの名前 導入後、必ずrails sで、起動確認しよう ③Gemのアンインストール ・削除したいGemをアンインストール $ bundle exec gem uninstall gem名 ・次にGemfileから直接削除する $ bundle install ・Gemfile.lockを確認すると、消えています。 これで、Gemの修正は完了です ④ActiveRecord::PendingMigrationErrorの対応 さて、これでプルリクだそうと起動確認すると、下記のエラーあり。 ・なぜこのエラーが起こるのか? データベースの整合性が取れていないため、起こるエラーです。 エラー文をよく読むと、解決方法も一緒に示しています。 (なんて親切なんでしょう!) ・エラー文どおり、下記を実行 $ bin/rails db:migrate RAILS_ENV=development ・実行結果 ○○○○○@xxxxxx % bin/rails db:migrate RAILS_ENV=development == 20210403052917 CreateReads: migrating ====================================== -- create_table(:reads) -> 0.0536s -- add_index(:reads, [:user_id, :text_id], {:unique=>true}) -> 0.0014s == 20210403052917 CreateReads: migrated (0.0553s) ============================= これで無事にrails sで、サーバー起動できました。 エラー文を焦らず、きちんと読んだので対応できました
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

独学・未経験者がポートフォリオを作った【Rails / Vue.js / Docker / AWS】

はじめに はじめまして! 未経験者が独学でポートフォリオを作成しましたので、紹介させてください。 About Me 私は33歳で現在、看護師として病院に勤務しています。 看護師として働く前は、テレコミュニケーターとして某プロバイダーの派遣社員として働いていました。 そういった経歴でパソコンを触るのが好きだったこともあり、独自で作成した動画を使って、部署内で勉強会を開催するなどしていました。 昨今の新型コロナウイルスの影響で、従来の「集まって教育・学習・研修をする」ということができなくなり、IT機器やオンラインの重要性を個人的にも感じていました。 「もっと看護業界、医療業界が加速するようなことがしたい」と考えるようになり、以前から興味のあった「Webエンジニア」への転職を目指しました。 2020年5月からプログラミング学習を開始しました。 紹介させていただく、ポートフォリオの作成期間は約3ヶ月半(2021年1月〜4月はじめ) ポートフォリオ紹介 アプリ名「With Nurse」 URL:https://www.withnurse.net/ Githubリポジトリ TOPページ レスポンシブ対応 ログイン後TOPページ 概要 看護師専用の記事投稿+学習サービスです。 記事投稿機能やVue.jsを使った一部SPAのゲーム機能を実装しています。 なぜ開発したか About Meでもお話したように、私は看護師として働きながら、プログラミング学習をしています。 その中で感じた、以下の理由で開発しようと考えました。 1.同業者との情報共有でスッと理解できる 現場では先輩から看護について教育を受けます。その中でテキストや文献を読むだけではなかなか理解できなかったことを、その先輩の説明でスッと理解できる場面がたくさんあります。 それは先輩だけでなく、同期や後輩の言葉が理解を深めるということも多々あります。 もちろん二次情報になるので、勘違いや見当違いなこともあるわけなので、盲信できるわけではありませんが、現場で起きている「スッと理解できる」という場面をネット上でもできないかと考え、このアプリを開発しました。 2.Qiitaに感動した プログラミング学習を初めて、一番驚いたのがたくさんの人が情報発信をしていたということです。 ポートフォリオを作成しながら、わからない・できないところはググって実装していきました。 ほとんどのことをネット上で調べることができました。 たくさんの情報発信をエンジニアの方たちがされていること、そして同じエンジニアの人たちと知識を共有しているエンジニア業界の仕組みに感動したからです。 公式ドキュメントなどで一次情報を調べることは必須ですが、理解しづらいことが、ある人の情報発信で「スッと理解できる」Qiitaのようなサービスがあればいいなと考えたからです。 3.学習サイト、スキルトレーニングサイトの重要性 私は物事を学習する上でアウトプットに勝るものはないと考えています。 私自身も勉強会に参加するときよりも、開催するほうが自分の知識やスキルが向上することを実感していたからです。 自分の得意な分野や学習を始めた分野のアウトプットをする場所がインターネット上にあればとても有用だと思い、記事投稿サービスを作りました。 また看護師として10年勤務して、「このスキルを鍛えておくと、非常に有用である」と思った内容をゲームとして提供しています。 タイピングゲームを実装しているのですが、それもその一つです。 使用技術 HTML/CSS Bootstrap SASS javascript jQuery Vue.js Ruby 2.6.3 Ruby on Rails 6.0.3 Rubocop (コード解析ツール) RSpec (テスト) Docker/Docker-compose(ローカル開発環境からデプロイまで) AWS(VPC, EC2, Route53, ELB, S3, RDS, ACM) 実装機能 基本機能 新規会員登録・ログイン機能 Googleアカウントログイン機能 パスワードリセット機能 記事一覧機能(ページネーション) 記事ソート機能(タグで絞り込み) 記事検索機能(テキスト検索) タグ機能 記事詳細画面閲覧機能 タイピングゲーム機能(データー登録はしない) いいね数表示機能 お問い合わせ機能 ログイン後機能 ユーザーマイページ表示機能( フォロー・フォロワー表示・My記事一覧表示・My記事投稿数表示機能 ) ユーザー情報変更機能(アイコン画像・ユーザーネーム・メールアドレス・パスワード) MYユーザー削除機能 タイピングゲーム機能(スコア、プレイ回数表示、保存) 記事いいね機能(非同期通信) ユーザーフォロー機能 記事投稿機能(ActionText) 記事編集機能 画像投稿機能(ActiveStorage経由でS3バケットに保存) 管理者機能 ユーザー一覧表示 ユーザー削除機能 インフラ構成図 ER図 ポートフォリオの工夫したところ ユーザー対象は看護師で、業務上よく使う、officeのWordのUIに比較的近い「Action Text」を採用 課題 技術選定、DB設計など、初期の段階で構想がめちゃくちゃ甘かった 未経験からのWEB系エンジニアへの転職が当面の目標であったため 多く採用されている 学習がしやすい(公式ドキュメント、実装事例が多い、ユーザーが多いため、質問しやすい) 上記の理由からRuby on Rails、Vueを使うとは決めていました。 しかしAWSやDockerの採用などはなぜこの技術なのかという技術的判断よりも、デファクトスタンダードであろう技術であり、ポートフォリオ作成時点でキャッチできた情報で手探りで作りました。 なぜその技術が必要なのかという判断をするための知識もまだまだ不足しているため、ポートフォリオの要件に合わせて、ブラッシュアップしていく必要があります。 体系的に技術の学習が今後も必要 これはポートフォリオ作成ではなく、プログラミングを行う上での課題です。 よく言われている「プログラミングは暗記しなくていい」「まず手を動かしながら覚えていく」精神でポートフォリオ作成をしてきました。 わからないところはすぐにググって、都度実装してきたが、「実装したコードをちゃんと理解しているか」と自分に問うと、非常に怪しい・・・ コードを見直せば、何をしているのかはなんとなくわかるが、しっかり言語化するまでに時間がかかる。 体系的に理解できていないことがその原因だと考えています。 独学にこだわりすぎたのも問題で、メンターサービスなどの活用もするべきでした。 まとめ 当初考えていた機能は実装できたので、完成としました。 リファクタリングや機能の追加は今後も継続していきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】お問い合わせ機能を実装する方法

テーマ RubyonRailsでお問い合わせ機能を実装する方法 はじめに お問い合わせ内容をGmailアドレスで受け取る方法を記載しています ローカル環境でお問い合わせ機能を実装するための記載になります(後日、本番環境追記します) 環境変数の設定とGoogleセキュリティ設定が必要が必要です。こちらに関しては、URLを参照願います。 環境 Ruby 2.6.5 Rails 6.0.3.6 Visual Studio Code ターミナル ローカル環境 実装の流れを把握する モデル作成 コントローラー作成 ルーティング viewファイル作成 ActionMailer作成 メール本文のレイアウト作成後、previewする Gmailアドレスへ送信するための設定 実際に送信してみる モデル作成 以下を順番に実行します % rails g model inquiry name:string message:string % rails db:migrate コントローラー作成 以下を実行します % rails g controller inquiries 色々なファイルが生成されたと思いますが「inquiries_controller.rb」に以下を記述します。 inquiries_controller.rb class InquiriesController < ApplicationController before_action :authenticate_user!, only: [:new, :create] def new @inquiry = Inquiry.new end def create @inquiry = Inquiry.new(inquiry_params) if @inquiry.save #保存処理 InquiryMailer.send_mail(@inquiry).deliver_now #メール送信処理 redirect_to root_path else render :new end end private def inquiry_params params.require(:inquiry).permit(:name, :message) end end ルーティング 以下を記述します routes.rb Rails.application.routes.draw do devise_for :users root to: "tweets#index" resources :inquiries, only: [:new, :create] #この行を追記する resources :users, only: :show resources :tweets, only: [:new, :create, :edit, :update, :destroy] do member do get 'search' end end end viewファイルを作成します app/views/inquiries/へnew.html.erbを作成します app/views/inquiries/new.html.erb <h2 class="page-heading">お問い合わせ内容</h2> <%= form_with model: @inquiry, local: true do |f| %> <div class="field"> <%= f.label :name, "ニックネーム"%><br /> <%= f.text_field :name%> </div> <div class="field"> <%= f.label :message, "お問い合わせ内容"%><br /> <%= f.text_area :message, class: :form__text %> </div> <div class ="actions"> <%= f.submit "送信", class: :form__btn %> </div> <%end%> ※class名とcssはご自身で設定お願いします。 ActionMailerを作成 以下を実行します % rails g mailer inquiry 色々なファイルが生成されたかと思います。 次に、「app/mailers/inquiry_mailer.rb」を編集します。 app/mailers/inquiry_mailer.rb class InquiryMailer < ApplicationMailer def send_mail(inquiry) @inquiry = inquiry mail to: system@example.com, subject: 'お問い合わせ通知' end end メール本文のレイアウトを作成後、previewする メール本文のレイアウトを作成するためは、以下の命名規則にしたがってerbファイルを作成します。 app/views/{メイラー名}_mailer/{メイラークラスのメソッド名}.text.erb よって、「app/views/inquiry_mailer/send_mail.text.erb」を作成しましょう。 send_mail.text.erb <%= @inquiry.name %> 様 から問い合わせがありました。 ・お問い合わせ内容 <%= @inquiry.message %> 受信するとこんな感じで表示される とはいえ、現段階で受信することはできないので、preview機能を使って確認してみる。 以下のファイルに記述していく。 spec/mailers/previews/inquiry_mailer_preview.rb # Preview all emails at http://localhost:3000/rails/mailers/inquiry class InquiryPreview < ActionMailer::Preview def inquiry @inquiry = Inquiry.new(name: "何食べる太郎", message: "問い合わせテストメッセージ") InquiryMailer.send_mail(@inquiry) end end 実際にpreviewしてみます % rails s サーバー起動後、inquiry_mailer_preview.rbに元から記載されているURLをコピペしアクセスします。 http://localhost:3000/rails/mailers/inquiry このように表示されるかと思います inquiryをクリックします。 以上で、無事メール本文を作成し、previewで確認することができました。 Gmailアドレスへ送信するための設定 メールサーバーを設定していきます 以下のファイル、こちらを追記しましょう config/environments/development.rb #メールサーバーの設定 config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: 'smtp.gmail.com', port: 587, domain: 'gmail.com', user_name: 'お問い合わせを受信したいgmailメールアドレス', password: 'gmailアドレスに対するパスワード', authentication: 'plain', enable_starttls_auto: true } そして、以下を修正します app/mailers/inquiry_mailer.rb class InquiryMailer < ApplicationMailer def send_mail(inquiry) @inquiry = inquiry mail to: 'お問い合わせを受信したいgmailメールアドレス', subject: 'お問い合わせ通知' end end 実際に送信してみる http://localhost:3000/inquiries/newこちらにアクセスして実際にフォームを送信してみましょう。 実際に送信できたかと思います…が、 Googleアカウントにアクセスすると警告文が表示されると思います。 要因としてはセキュリティの甘さが考えられます。 対策として 自分のGmailアドレスを環境変数に設定すること Googleアカウントのセキュリティを強化すること こちらの記事を参考にさせていただいた所、問題解決できましたので試してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsで作ったアプリをHerokuでデプロイする

作ったアプリをデプロイする Heroku CLIをインストール 一応念の為にデプロイしたいアプリのディレクトリで行いましょう。 brew tap heroku/brew && brew install heroku 完了を確認するために下記のコマンドを入力 % heroku --version heroku/7.40.0 darwin-x64 node-v12.16.2 上記のようにバージョンが出力されれば成功 Herokuにログイン # Herokuへログインするためのコマンド % heroku login --interactive => Enter your Heroku credentials. # メールアドレスを入力し、エンターキーを押す => Email: # パスワードを入力して、エンターキーを押す => Password: Heroku上にアプリケーションを作成 % heroku create アプリ名 アプリをデプロイする前に、まずデータベースの設定 Herokuでは、使用するデータベースの設定が、デフォルトでPostgreSQLというデータベースになっています。 今回は開発環境と同様にMySQLを使用するための設定を行います。 ClearDBアドオン ClearDBアドオンとは、MySQLを使うためのツール % heroku addons:add cleardb Creating cleardb on ⬢ ajax-app-123456... free Created cleardb-vertical-00000 as CLEARDB_DATABASE_URL Use heroku addons:docs cleardb to view documentation 上記のようにでればOK。 設定を変更 データベースをMySQLに設定できました。しかし、Ruby on Railsを使う場合は、MySQLに対応するGemについて考慮する必要があり、そちらの設定を変更します。 % heroku_cleardb=`heroku config:get CLEARDB_DATABASE_URL` これでClearDBデータベースのURLを変数heroku_cleardbに格納 % heroku config:set DATABASE_URL=mysql2${heroku_cleardb:5} これでデータベースのURLを再設定できました。 DATABASE_URLの冒頭がmysql2://に変更されていることも確認しておきましょう。 アプリケーションを開発する際、サーバーのアクセスキーやAPIキーなど、外部に漏らしたくない情報を扱う場面があります。デプロイをする際、それらの情報はセキュリティの観点から暗号化する必要があります。暗号化された情報は、開発環境および本番環境であらかじめ用意した鍵を用いて復号(暗号化された情報を使えるように)します。 Railsのバージョン5.2以降では、credentials.yml.encを利用して、それらの情報を暗号化します。 credentials.yml.encファイル Railsにて、外部に漏らしたくない情報を扱う際に用いるファイルです。 通常時は、英数字の文字列で構成された暗号文が表示され、ファイル内に何が書かれているのか分からないようになっています。しかし、このcredentials.yml.encと対になるmaster.keyが存在する場合、credentials.yml.encの暗号文を復号し、ファイル内の記述を確認できます。 master.keyファイル credentials.yml.encの暗号文を復号する、鍵の役割を持ったファイルです。特定のcredentials.yml.encと対になっているため、その他のcredentials.yml.encへは、効果を発揮しません。 また、master.keyは非常に重要なファイルなので、リモートリポジトリに反映されることは好ましくありません。そのため、デフォルトで.gitignoreに記述されており、Gitで管理されない仕組みになっています。 Heroku上にmaster.keyを設置 Heroku上には、環境変数としてmaster.keyの値を設置します。Herokuへ環境変数を設定するためには、 heroku configというコマンドを使用します。 環境変数 OSが提供するデータ共有機能の1つで、「どのディレクトリ・ファイルからでも参照できる変数」を設けることができます。一般的な用途の1つとして、「外部に漏らしたくない情報を環境変数にセットする」というものがあります。 heroku configコマンド Heroku上で環境変数の参照・追加・削除等をする場合に用います。環境変数の追加であればheroku config:set 環境変数名="値"と実行します。そうすることによって、Heroku上で環境変数を追加できます。 Heroku上で環境変数を設定 % heroku config:set RAILS_MASTER_KEY=`cat config/master.key` Heroku上で環境変数を確認 % heroku config 動作環境を変更 Stackとは、Herokuにおけるアプリケーションの動作環境のことです。Stackはデプロイされたアプリケーションを読み取り正常に稼働させるために用意されています。 デフォルトのStackは「Heroku-20」です。一方で、別のStackを選択することもできます。 今回私が作ったアプリのRubyのバージョンは、Heroku-20では使用できません。 そのため、Rubyのバージョン2.6.5が動作するStackを指定します。 % heroku stack:set heroku-18 -a アプリ名 Herokuへアプリケーションを追加 git push heroku masterを実行 % git push heroku master Heroku上でマイグレーションファイルを実行 % heroku run rails db:migrate 以下のコマンドで公開されたアプリケーションの詳細を見ることができます。 % heroku apps:info デプロイ後のエラー対処方法 heroku logs --tail --app <<アプリケーション名>> をしてエラーを探す。。 はじめてデプロイをする場合 Herokuにアカウント登録する Heroku CLIをインストールする masterブランチへcommitする Heroku上にアプリケーションを作成する MySQLを使用できるように設定する master.keyを環境変数として設定する Herokuへアプリケーションの情報をpushする Heroku上でマイグレーションを実行する デプロイ済みのアプリケーションに変更修正を加えた場合 変更修正をcommitする ブランチを作成していた場合は、masterブランチへマージする Heroku上にpushする (テーブルに変更を加えた場合は)Heroku上でマイグレーションを実行する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

model のテストコードの書き方について

はじめに 今回はモデルのテスト実装を行う中で、テストをわかりやすく書くために必要なテクニックなどを学習したため、まとめていきます。 モデルテストをわかりやすく書くためには テストを書く際に以下のテクニックを使用すると、読みやすいテストになります。 - subject を定義する - FactoryBot のアソシエーションを有効に利用する - テストするバリデーションに対して明示的な表現にする それぞれ見ていきましょう! subject を定義する subject は主題という意味になりますが、subject にテストしたいことを定義しておくことで、テストを英語的に読みやすい表現にすることができます。 以下具体例です。 ```ruby expect(:user).to be_valid expect(subject).to be_valid ``` 前者と後者を比較すると、前者でも悪くはないのですが、より英語的に読めるようになる表現は後者となります。 上記のように subject を定義し、英語的に読みやすいテストを書くようにしましょう! アソシエーションを有効活用する factories同士をアソシエーションすることで、テストをスッキリとした内容に変更することができます。アソシエーションの方法は以下の通りです。 spec/factories/user_directories.rb FactoryBot.define do factory :user_directory do name { "MyString" } # 以下 association :user factory: :user の略 user end end 上記のように記述することで user_directory を FactoryBot で作成した際に、user_directory と関連している user もFactoryBotで作ってくれます。 関連しているモデルを一緒に生成してくれると余計なコードを書くことがなくなるので、スッキリとしたテストコードを書くことができます。 こちらも具体例を見ていきましょう。 まずはspec/factories/user_directoriesに association の記述を行わなかった場合、 User_directory モデルのテストは以下のように記述しないとエラーしてしまいます。 subject { user_directory } context "name が20文字以下のとき" do # user を定義 let(:user) { create(:user) } # user_directory を定義し、user_id に上で定義した user を指定 let(:user_directory) { build(:user_directory, name: "a" * 20, user: user) } it "ユーザーディレクトリが作られる" do expect(subject).to be_valid end end 少し重い内容のコードになってしまいますね。 続いて アソシエーションをした場合はどのように書けば良いか見ていきましょう! subject { user_directory } context "name が20文字以下のとき" do # user_directory を定義すると同時に関連する user も生成 let(:user_directory) { build(:user_directory, name: "a" * 20) } it "ユーザーディレクトリが作られる" do expect(subject).to be_valid end end 一目瞭然ですね! factories 同士アソシエーションは積極的に活用していきましょう! テストするバリデーションに対して明示的な表現にする モデルのテストはバリデーションが正常に働いているかどうかのテストを中心に行います。 テストコードを見ただけでどのバリデーションのどんな値のテストを行っているのかわかりやすくするために、明示的な表現にすると読みやすいテストとなります。 こちらも具体例を見ていきましょう! subject { community } context "コミュニティ名が50文字以下の時" do # FactoryBot でcommunity を自動生成、name: は自動で50文字以下となる let(:community) { build(:community) } it "コミュニティが登録される" do expect(subject).to be_valid end end context "コミュニティ名が50文字を超える時" do let(:community) { build(:community, name: "a" * 51) } it "コミュニティ登録ができない" do expect(subject).not_to be_valid expect(subject.errors.details[:name][0][:error]).to eq :too_long end end 上記のテストでも問題はないのですが、よりわかりやすく書くためには下記のように区別すると良いです。 subject { community } # name が50文字の時と51文字の時に区別し、明示的に表現している context "コミュニティ名が50文字以下の時" do let(:community) { build(:community, name: "a" * 50) } it "コミュニティが登録される" do expect(subject).to be_valid end end context "コミュニティ名が50文字を超える時" do let(:community) { build(:community, name: "a" * 51) } it "コミュニティ登録ができない" do expect(subject).not_to be_valid expect(subject.errors.details[:name][0][:error]).to eq :too_long end end こちらの書き方の方がname の validation 50文字以内かどうかのテストをしているということがわかりやすく伝わりますね! このように - subject を定義する - FactoryBot のアソシエーションを有効に利用する - テストするバリデーションに対して明示的な表現にする この3つを意識することでモデルのテストコードをわかりやすく書くことができます。 終わりに 今回はモデルのテストコードを書く際に学習したテクニックなどを紹介しました。 正直なところまともにテストを書いたのは wonderful_editor 作成時のみなので、まだまだ理解不足な部分もあります。 今回紹介したこと以外にも良い書き方などありましたら、紹介していただければありがたいです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ポリモーフィック関連について

はじめに 今回はポリモーフィック関連について解説していきます。 ポリモーフィック関連とは ポリモーフィック関連とは一言で表すと、オブジェクトの実態を気にせず、メソッドを呼び出し、そのオブジェクトごとに振舞ってもらうことです。 この辺り少し難しい部分もあるのですが、実際の実装を確認しながらどのように使うか理解していきましょう! 実装例の紹介 実装例についての確認 今回の実装ではUser モデルと Community モデル の2つのモデルに対してDocument モデルと共通の外部キー(owner)で紐付けするということを実現させたいです。 そのためにポリモーフィック関連を使います。 各モデルの作成 User モデルと Community モデル rails gコマンドでUser とCommunity のモデルを作成します。ここは普通に自動生成コマンドで作成で問題なしです。 Document モデル の作成 documentモデルを作成します。 ただしモデルをコマンドで生成する際に以下のように実行します。 $ bundle exec rails g model document owner:references{polymorphic} 上記のコマンドを実行することでapp/models/documents.rbに以下のように記述されます。 app/models/community.rb class Document < ApplicationRecord belongs_to :owner, polymorphic: true end 上記の記述を行うことによりDocument モデルからownerを呼び出すことで他のモデルと関連付けることができるようになります。 ここまででモデル同士をポリモーフィック関連で紐づける準備は完了です。 モデル同士をポリモーフィック関連で紐付ける 各モデルをポリモーフィック関連で紐付けしていきます。 app/models/users.rbを以下のように修正します。 app/models/users.rb class User < ApplicationRecord has_many :documents, class_name: "Document", as: owner end これでUser モデルとDocument モデルを外部キーownerで紐付けすることが完了です。 同様にapp/models/community.rbを以下のように修正。 app/models/community.rb class Community < ApplicationRecord has_many :documents, class_name: "Document", as: owner end User モデルと同様に Community モデルも外部キーowner紐づけることが完了しました。 これで目的としていた User モデルとCommunity モデルの双方に対してDocument モデルから外部キーownerで紐づけるということを実現しました。 注意事項 便利そうに見えるポリモーフィック関連ですが、扱いが難しく、賛否両論あるようです。"ポリモーフィック アンチパターン"などでググると問題点を解説している記事も多数あります。 さいごに 今回はポリモーフィック関連について、簡単ではありますが調べたことをまとめてみました! 注意点でも書きましたが、便利そうな反面、問題点も多数あるようです。実装の際には十分に注意して実装していきましょう!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails × Carrierwave で作成した画像アップロードAPIをReact側から叩くためのチュートリアル

概要 バックエンド: Rails(APIモード) フロントエンド: React(もしくはVue) 最近、こんな感じで役割分担しつつ開発する人が増えていると思います。(特に未経験からのエンジニア転職を目指している人の多くは大体そんな感じでポートフォリオを作られている印象) 自分自身も上記の構成が好きでシコシコ個人開発してたりするわけですが、画像アップロード機能を実装しようと思った際に少し手こずったのでメモ書きとして残しておきます。 ※「チュートリアル」と銘打ってはいるものの、細かいコードの説明などはあまりありません。一応、手順通りに進めれば同じものは作れるはずなのでまずは自分の手を動かした後に各コードを読み込んでみてください。 完成イメージ 画像プレビュー機能なども付けてそれっぽく仕上げてみました。 使用技術 Rails6(APIモード) React TypeScript バックエンド まず最初にバックエンド側から作っていきましょう。 ディレクトリ&各種ファイルを作成 $ mkdir mkdir rails-react-carrierwave-backend && cd rails-react-carrierwave-backend $ touch Dockerfile $ touch docker-compose.yml $ touch entrypoint.sh $ touch Gemfile $ touch Gemfile.lock ./Dockerfile FROM ruby:3.0 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs ENV APP_PATH /myapp RUN mkdir $APP_PATH WORKDIR $APP_PATH COPY Gemfile $APP_PATH/Gemfile COPY Gemfile.lock $APP_PATH/Gemfile.lock RUN bundle install COPY . $APP_PATH COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"] ./docker-compose.yml version: "3" services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password command: --default-authentication-plugin=mysql_native_password volumes: - mysql-data:/var/lib/mysql - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 3306:3306 api: build: context: . dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - "3001:3000" depends_on: - db volumes: mysql-data: ./entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" ./Gemfile # frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails", "~> 6" /Gemfile.lock # 空欄でOK rails new おなじみのコマンドでプロジェクトを作成します。 $ docker-compose run api rails new . --force --no-deps -d mysql --api Gemfileが更新されたので再ビルド。 $ docker-compose build 「.config/database.yml」を編集 ./config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # デフォルトだと空欄になっているはずなので変更 host: db # デフォルトだとlocalhostになっているはずなので変更 development: <<: *default database: myapp_development test: <<: *default database: myapp_test production: <<: *default database: <%= ENV["DATABASE_NAME"] %> username: <%= ENV["DATABASE_USERNAME"] %> password: <%= ENV["DATABASE_PASSWORD"] %> host: <%= ENV["DATABASE_HOST"] %> データベースを作成 $ docker-compose run api rails db:create 動作確認 $ docker-compose up -d localhost:3001 にアクセスしていつもの画面が表示されればOK。 APIを作成 carrierwaveをインストール。 ./Gemfile gem 'carrierwave' Gemfileを更新したので再ビルド。 $ docker-compose build アップローダーを作成。 $ docker-compose run api rails g uploader Image すると「./app/uploaders/image_uploader.rb」が自動生成されるので次のように変更します。 .app/uploaders/image_uploader.rb class ImageUploader < CarrierWave::Uploader::Base storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # 受け付け可能なファイルの拡張子を指定 def extension_allowlist %w(jpg jpeg png) end end また、「./config/initializers/」配下にcarrierwave設定用のファイルを作成。 $ touch config/initializers/carrierwave.rb ./config/initializers/carrierwave.rb CarrierWave.configure do |config| config.asset_host = "http://localhost:3001" config.storage = :file config.cache_storage = :file end Postモデルを作成。 $ docker-compose run api rails g model Post content:text image:string $ docker-compose run api rails db:migrate 先ほど作成したアップローダーをマウントします。(ついでにバリデーションもやっておきましょう。) ./app/models/post.rb class Post < ApplicationRecord mount_uploader :image, ImageUploader validates :content, presence: true, length: { maximum: 140 } end コントローラーを作成。 $ docker-compose run api rails g controller api::v1::posts ./app/controllers/api/v1/posts_controller.rb class Api::V1::PostsController < ApplicationController before_action :set_post, only: %i[destroy] def index render json: { posts: Post.all.order("created_at DESC") } end def create post = Post.new(post_params) post.save end def destroy post = Post.find(params[:id]) post.destroy end private def set_post @post = Post.find(params[:id]) end def post_params params.permit(:content, :image) end end ルーティングを記述。 ./config/routes.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :posts, only: %i[index create destroy] end end end これで準備は完了です。 あとは動作確認のためルートディレクトリに適当な画像を「sample.jpg」という名前配置し、次のcurlコマンドを実行しましょう。 $ curl -F "content=test" -F "image=@sample.jpg" http://localhost:3001/api/v1/posts 特にエラーっぽいレスポンスが返ってこなければ上手くいっているはずなので、次のcurlコマンドで確認します。 $ curl -X GET http://localhost:3001/api/v1/posts { "posts": [ { "id": 1, "content": "test", "image": { "url": "http://localhost:3001/uploads/post/image/1/sample.jpg" }, "created_at": "2021-05-03T17:36:33.147Z", "updated_at": "2021-05-03T17:36:33.147Z" } ] } こんな感じで画像のパスが保存されていればOK。 CORSの設定 これでAPIは完成ですが、今の状態のままReact側から呼び出そうとするとセキュリティ的な問題でエラーが生じます。 そこで、CORSの設定を行わなければなりません。 参照: CORS とは? RailsにはCORSの設定を簡単に行えるgemが存在するのでインストールしましょう。 rb./Gemfile gem 'rack-cors' APIモードで作成している場合、すでにGemfile内に記載されているのでそちらのコメントアウトを外せばOKです。 Gemfileを更新したので再度ビルド。 $ docker-compose build あとは「./config/initializers/」配下にある設定ファイルをいじくり外部からアクセス可能なようにしておきます。 ./config/initializers/cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins "localhost:3000" # React側はポート番号3000で作るので「localhost:3000」を指定 resource "*", headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end これで設定は完了。 フロントエンド フロントエンド側は別リポジトリで作成します。(その方がコードの見通しも良くなるので) create-react-app $ mkdir rails-react-carrierwave-frontend && cd rails-react-carrierwave-frontend $ npx create-react-app . --template typescript 不要なファイルを整理 $ rm src/App.css src/App.test.tsx src/logo.svg src/reportWebVitals.ts src/setupTests.ts 「./src/index.tsx」と「./src/App.tsx」を次のように変更します。 ./src/index.tsx import React from "react" import ReactDOM from "react-dom" import "./index.css" import App from "./App" ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ) ./src/App.tsx import React from "react" const App: React.FC = () => { return ( <h1>Hello World!</h1> ) } export default App 一旦、動作確認してみましょう。 $ yarn start localhost:3000 にアクセスして「Hello World!」と返ってくればOK。 各種ディレクトリ・ファイルを準備 $ mkdir components $ mkdir components/post $ mkdir interfaces $ mkdir lib $ mkdir lib/api $ touch components/post/PostForm.tsx $ touch components/post/PostItem.tsx $ touch components/post/PostList.tsx $ touch interfaces/index.ts $ touch lib/api/client.ts $ touch lib/api/posts.ts $ mv components interfaces lib src 最終的に次のような構成になっていればOK。 rails-react-carrierwave-frontend ├── node_modules ├── public │   ├── favicon.ico │   ├── index.html │   ├── logo192.png │   ├── logo512.png │   ├── manifest.json │   └── robots.txt ├── src │   ├── components │   │   └── post │   │   ├── PostForm.tsx │   │   ├── PostItem.tsx │   │   └── PostList.tsx │   ├── interfaces │   │   └── index.ts │   ├── lib │   │   └── api │   │   ├── client.ts │   │   └── posts.ts │   ├── App.tsx │   ├── index.css │   ├── index.tsx │   ├── react-app-env.d.ts ├── .gitignore ├── package.json ├── README.md ├── tsconfig.json └── yarn.lock 各種ライブラリをインストール $ yarn add @material-ui/core@next @material-ui/icons@next @material-ui/lab@next @material-ui/styled-engine @emotion/react @emotion/styled axios react-router-dom @types/react-router-dom material-ui UIを整える用のライブラリ。(ついこの前最新バージョンv5が発表されたので試しにそちらを使用) emotion material-uiの最新バージョンを使う際に追加で必要なライブラリ。 axios APIリクエスト用のライブラリ。 react-router-dom ルーティング設定用のライブラリ。 型定義 ./src/interfaces/index.ts export interface Post { id: string content: string image?: { url: string } } export interface PostApiJson { posts: Post[] } APIクライアントを作成 ./src/lib/api/client.ts import axios, { AxiosInstance, AxiosResponse } from "axios" let client: AxiosInstance export default client = axios.create({ baseURL: "http://localhost:3001/api/v1", headers: { "Content-Type": "multipart/form-data" // 画像ファイルを取り扱うのでform-dataで送信 } }) client.interceptors.response.use( (response: AxiosResponse): AxiosResponse => { const data = response.data return { ...response.data, data } } ) ./src/lib/api/posts.ts import { AxiosPromise } from "axios" import client from "./client" import { PostApiJson } from "../../interfaces/index" // post取得 export const getPosts = (): AxiosPromise<PostApiJson> => { return client.get("/posts") } // post作成 export const createPost = (data: FormData): AxiosPromise => { return client.post("/posts", data) } // post削除 export const deletePost = (id: string): AxiosPromise => { return client.delete(`/posts/${id}`) } ビュー部分を作成 ./src/components/PostList.tsx import React, { useEffect, useState } from "react" import { Container, Grid } from "@material-ui/core" import { makeStyles } from "@material-ui/core/styles" import PostForm from "./PostForm" import PostItem from "./PostItem" import { getPosts } from "../../lib/api/posts" import { Post } from "../../interfaces/index" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" } })) const PostList: React.FC = () => { const classes = useStyles() const [posts, setPosts] = useState<Post[]>([]) const handleGetPosts = async () => { const { data } = await getPosts() setPosts(data.posts) } useEffect(() => { handleGetPosts() }, []) return ( <Container maxWidth="lg" className={classes.container}> <Grid container direction="row" justifyContent="center"> <Grid item> <PostForm handleGetPosts={handleGetPosts} /> { posts?.map((post: Post) => { return ( <PostItem key={post.id} post={post} handleGetPosts={handleGetPosts} /> )} )} </Grid> </Grid> </Container> ) } export default PostList ./src/components/post/PostItem.tsx import React, { useState } from "react" import { makeStyles } from "@material-ui/core/styles" import Card from "@material-ui/core/Card" import CardHeader from "@material-ui/core/CardHeader" import CardMedia from "@material-ui/core/CardMedia" import CardContent from "@material-ui/core/CardContent" import CardActions from "@material-ui/core/CardActions" import Avatar from "@material-ui/core/Avatar" import IconButton from "@material-ui/core/IconButton" import Typography from "@material-ui/core/Typography" import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder" import FavoriteIcon from "@material-ui/icons/Favorite" import ShareIcon from "@material-ui/icons/Share" import DeleteIcon from "@material-ui/icons/Delete" import MoreVertIcon from "@material-ui/icons/MoreVert" import { Post } from "../../interfaces/index" import { deletePost } from "../../lib/api/posts" const useStyles = makeStyles(() => ({ card: { width: 320, marginTop: "2rem", transition: "all 0.3s", "&:hover": { boxShadow: "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)", transform: "translateY(-3px)" } }, delete: { marginLeft: "auto" } })) interface PostItemProps { post: Post handleGetPosts: Function } const PostItem = ({ post, handleGetPosts }: PostItemProps) => { const classes = useStyles() const [like, setLike] = useState<boolean>(false) const handleDeletePost = async (id: string) => { await deletePost(id) .then(() => { handleGetPosts() }) } return ( <> <Card className={classes.card}> <CardHeader avatar={ <Avatar> U </Avatar> } action={ <IconButton> <MoreVertIcon /> </IconButton> } title="User Name" /> { post.image?.url ? <CardMedia component="img" src={post.image.url} alt="post image" /> : null } <CardContent> <Typography variant="body2" color="textSecondary" component="span"> { post.content.split("\n").map((content: string, index: number) => { return ( <p key={index}>{content}</p> ) }) } </Typography> </CardContent> <CardActions disableSpacing> <IconButton onClick={() => like ? setLike(false) : setLike(true)}> { like ? <FavoriteIcon /> : <FavoriteBorderIcon /> } </IconButton> <IconButton> <ShareIcon /> </IconButton> <div className={classes.delete}> <IconButton onClick={() => handleDeletePost(post.id)} > <DeleteIcon /> </IconButton> </div> </CardActions> </Card> </> ) } export default PostItem ./src/components/post/PostForm.tsx import React, { useCallback, useState } from "react" import { experimentalStyled as styled } from '@material-ui/core/styles'; import { makeStyles, Theme } from "@material-ui/core/styles" import TextField from "@material-ui/core/TextField" import Button from "@material-ui/core/Button" import Box from "@material-ui/core/Box" import IconButton from "@material-ui/core/IconButton" import PhotoCameraIcon from "@material-ui/icons/PhotoCamera" import CancelIcon from "@material-ui/icons/Cancel" import { createPost } from "../../lib/api/posts" const useStyles = makeStyles((theme: Theme) => ({ form: { display: "flex", flexWrap: "wrap", width: 320 }, inputFileBtn: { marginTop: "10px" }, submitBtn: { marginTop: "10px", marginLeft: "auto" }, box: { margin: "2rem 0 4rem", width: 320 }, preview: { width: "100%" } })) const Input = styled("input")({ display: "none" }) const borderStyles = { bgcolor: "background.paper", border: 1, } interface PostFormProps { handleGetPosts: Function } const PostForm = ({ handleGetPosts }: PostFormProps) => { const classes = useStyles() const [content, setContent] = useState<string>("") const [image, setImage] = useState<File>() const [preview, setPreview] = useState<string>("") const uploadImage = useCallback((e) => { const file = e.target.files[0] setImage(file) }, []) // プレビュー機能 const previewImage = useCallback((e) => { const file = e.target.files[0] setPreview(window.URL.createObjectURL(file)) }, []) // FormData形式でデータを作成 const createFormData = (): FormData => { const formData = new FormData() formData.append("content", content) if (image) formData.append("image", image) return formData } const handleCreatePost = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const data = createFormData() await createPost(data) .then(() => { setContent("") setPreview("") setImage(undefined) handleGetPosts() }) } return ( <> <form className={classes.form} noValidate onSubmit={handleCreatePost}> <TextField placeholder="Hello World" variant="outlined" multiline fullWidth rows="4" value={content} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setContent(e.target.value) }} /> <div className={classes.inputFileBtn}> <label htmlFor="icon-button-file"> <Input accept="image/*" id="icon-button-file" type="file" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { uploadImage(e) previewImage(e) }} /> <IconButton color="inherit" component="span"> <PhotoCameraIcon /> </IconButton> </label> </div> <div className={classes.submitBtn}> <Button type="submit" variant="contained" size="large" color="inherit" disabled={!content || content.length > 140} className={classes.submitBtn} > Post </Button> </div> </form> { preview ? <Box sx={{ ...borderStyles, borderRadius: 1, borderColor: "grey.400" }} className={classes.box} > <IconButton color="inherit" onClick={() => setPreview("")} > <CancelIcon /> </IconButton> <img src={preview} alt="preview img" className={classes.preview} /> </Box> : null } </> ) } export default PostForm ./src/App.tsx import React from "react" import { BrowserRouter as Router, Switch, Route } from "react-router-dom" import PostList from "./components/post/PostList" const App: React.FC = () => { return ( <Router> <Switch> <Route exact path="/" component={PostList} /> </Switch> </Router> ) } export default App http://localhost:3000 にアクセスしてこんな感じになっていればOKです。 投稿を作成できるか、画像プレビュー機能が動いているか、投稿を削除できるかなど一通り確認してください。 あとがき お疲れ様でした。もし手順通りに進めて不具合などありましたらコメント欄にて指摘していただけると幸いです。 今回作成したコード
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】resoucesを使用しても、editやdestroyでActionController::RoutingErrorとエラーが発生してしまう場合の対処法

症状 Rails6.0.3のAPIモードでeditアクションに対してアクセスしようとしたとき、下記エラーが発生しアクセスできませんでした。 No route matches「ルーティングがマッチしていない」と怒られています。 route.rb ActionController::RoutingError (No route matches [GET] "/api/v1/hoges/2/edit"): actionpack (6.0.3.6) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call' railties (6.0.3.6) lib/rails/rack/logger.rb:37:in `call_app' railties (6.0.3.6) lib/rails/rack/logger.rb:26:in `block in call' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:80:in `block in tagged' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:28:in `tagged' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:80:in `tagged' railties (6.0.3.6) lib/rails/rack/logger.rb:26:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/request_id.rb:27:in `call' rack (2.2.3) lib/rack/runtime.rb:22:in `call' activesupport (6.0.3.6) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/executor.rb:14:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/static.rb:126:in `call' rack (2.2.3) lib/rack/sendfile.rb:110:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/host_authorization.rb:82:in `call' rack-cors (1.1.1) lib/rack/cors.rb:100:in `call' railties (6.0.3.6) lib/rails/engine.rb:527:in `call' puma (4.3.7) lib/puma/configuration.rb:228:in `call' puma (4.3.7) lib/puma/server.rb:713:in `handle_request' puma (4.3.7) lib/puma/server.rb:472:in `process_client' puma (4.3.7) lib/puma/server.rb:328:in `block in run' puma (4.3.7) lib/puma/thread_pool.rb:134:in `block in spawn_thread' ルーティングを確認すると、何となく問題なさそうです。 resouceだと、特定のデータにアクセスするようなルーティングは制限されますが、今回使っているのはeditやshowなどの特定のデータにアクセスできるアクションにも対応できるようにしています。 route.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :fugasdo resources :hoges end resources :hoges end end end 実際にどのようなルーティングが存在しているかをrake routesで確認します。 rakeroutes rake routes Prefix Verb URI Pattern Controller#Action api_v1_fuga_hoges GET /api/v1/fugas/:fuga_id/hoges(.:format) api/v1/hoges#index POST /api/v1/fugas/:fuga_id/hoges(.:format) api/v1/hoges#create api_v1_fugat_food GET /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#show PATCH /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#update PUT /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#update DELETE /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#destroy api_v1_fugas GET /api/v1/fugas(.:format) api/v1/fugas#index POST /api/v1/fugas(.:format) api/v1/fugas#create api_v1_fugat GET /api/v1/fugas/:id(.:format) api/v1/fugas#show PATCH /api/v1/fugas/:id(.:format) api/v1/fugas#update PUT /api/v1/fugas/:id(.:format) api/v1/fugas#update DELETE /api/v1/fugas/:id(.:format) api/v1/fugas#destroy api_v1_hoges GET /api/v1/hoges(.:format) api/v1/hoges#index POST /api/v1/hoges(.:format) api/v1/hoges#create api_v1_food GET /api/v1/hoges/:id(.:format) api/v1/hoges#show PATCH /api/v1/hoges/:id(.:format) api/v1/hoges#update PUT /api/v1/hoges/:id(.:format) api/v1/hoges#update DELETE /api/v1/hoges/:id(.:format) api/v1/hoges#destroy rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mandrill_inbound_health_check GET /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#health_check rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show PATCH /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update PUT /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#destroy rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#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 上記の中に「edit」に該当するルーティングは存在しませんでした。 resoucesを使っても[edit」のルーティングが存在しないようです。 調べてみると、Railsガイドの「浅いネスト」に今回の原因となりそうな記述がありました。 要約すると、「親スコープ(fuga)の中でidを指定しないアクションのみを生成できる」というものでした。 つまり、今回のhugaに関するルーティングでは、routeファイルが上から読み込まれている関係上、最初にfuga以下にあったhogeリソースにヒットし、そこではid指定があるアクションが生成されていなかったから、このようになってしまったのかと思いましたが、resoucesの順序を変更してもルーティングにeditが追加されませんでした。 解決方法 resoucesで使用するアクションを明示的に書いたら、ルーティングエラーが出なくなりました。 route.rb resources :hoges,only: [:index, :show, :new, :create, :destroy, :edit, :update] do end RailsAPIモードだったので、生成されるルーティングは下記になります。 ゆえに、editやdestroyなどがルーティングでそもそも生成されない仕様で、resoucesでも使えないようです。 rakeroutes rake routes api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index POST /api/v1/posts(.:format) api/v1/posts#create api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show PATCH /api/v1/posts/:id(.:format) api/v1/posts#update PUT /api/v1/posts/:id(.:format) api/v1/posts#update DELETE /api/v1/posts/:id(.:format) api/v1/posts#destroy 参考 Railsガイド「浅い」ネスト https://railsguides.jp/routing.html#%E3%80%8C%E6%B5%85%E3%81%84%E3%80%8D%E3%83%8D%E3%82%B9%E3%83%88  Railsのresourcesとresourceついて https://qiita.com/Atsushi_/items/bb22ce67d14ba1abafc5 Railsで超簡単API https://qiita.com/k-penguin-sato/items/adba7a1a1ecc3582a9c9
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】resoucesを使用しても、editやdestroyでActionController::RoutingErrorとエラーが発生してしまう場合について

症状 Rails6.0.3のAPIモードでeditアクションに対してアクセスしようとしたとき、下記エラーが発生しアクセスできませんでした。 No route matches「ルーティングがマッチしていない」と怒られています。 route.rb ActionController::RoutingError (No route matches [GET] "/api/v1/hoges/2/edit"): actionpack (6.0.3.6) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call' railties (6.0.3.6) lib/rails/rack/logger.rb:37:in `call_app' railties (6.0.3.6) lib/rails/rack/logger.rb:26:in `block in call' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:80:in `block in tagged' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:28:in `tagged' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:80:in `tagged' railties (6.0.3.6) lib/rails/rack/logger.rb:26:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/request_id.rb:27:in `call' rack (2.2.3) lib/rack/runtime.rb:22:in `call' activesupport (6.0.3.6) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/executor.rb:14:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/static.rb:126:in `call' rack (2.2.3) lib/rack/sendfile.rb:110:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/host_authorization.rb:82:in `call' rack-cors (1.1.1) lib/rack/cors.rb:100:in `call' railties (6.0.3.6) lib/rails/engine.rb:527:in `call' puma (4.3.7) lib/puma/configuration.rb:228:in `call' puma (4.3.7) lib/puma/server.rb:713:in `handle_request' puma (4.3.7) lib/puma/server.rb:472:in `process_client' puma (4.3.7) lib/puma/server.rb:328:in `block in run' puma (4.3.7) lib/puma/thread_pool.rb:134:in `block in spawn_thread' ルーティングを確認すると、何となく問題なさそうです。 resouceだと、特定のデータにアクセスするようなルーティングは制限されますが、今回使っているのはeditやshowなどの特定のデータにアクセスできるアクションにも対応できるようにしています。 route.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :fugasdo resources :hoges end resources :hoges end end end 実際にどのようなルーティングが存在しているかをrake routesで確認します。 rakeroutes rake routes Prefix Verb URI Pattern Controller#Action api_v1_fuga_hoges GET /api/v1/fugas/:fuga_id/hoges(.:format) api/v1/hoges#index POST /api/v1/fugas/:fuga_id/hoges(.:format) api/v1/hoges#create api_v1_fugat_food GET /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#show PATCH /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#update PUT /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#update DELETE /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#destroy api_v1_fugas GET /api/v1/fugas(.:format) api/v1/fugas#index POST /api/v1/fugas(.:format) api/v1/fugas#create api_v1_fugat GET /api/v1/fugas/:id(.:format) api/v1/fugas#show PATCH /api/v1/fugas/:id(.:format) api/v1/fugas#update PUT /api/v1/fugas/:id(.:format) api/v1/fugas#update DELETE /api/v1/fugas/:id(.:format) api/v1/fugas#destroy api_v1_hoges GET /api/v1/hoges(.:format) api/v1/hoges#index POST /api/v1/hoges(.:format) api/v1/hoges#create api_v1_food GET /api/v1/hoges/:id(.:format) api/v1/hoges#show PATCH /api/v1/hoges/:id(.:format) api/v1/hoges#update PUT /api/v1/hoges/:id(.:format) api/v1/hoges#update DELETE /api/v1/hoges/:id(.:format) api/v1/hoges#destroy rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mandrill_inbound_health_check GET /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#health_check rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show PATCH /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update PUT /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#destroy rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#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 上記の中に「edit」に該当するルーティングは存在しませんでした。 resoucesを使っても[edit」のルーティングが存在しないようです。 調べてみると、Railsガイドの「浅いネスト」に今回の原因となりそうな記述がありました。 要約すると、「親スコープ(fuga)の中でidを指定しないアクションのみを生成できる」というものでした。 つまり、今回のhugaに関するルーティングでは、routeファイルが上から読み込まれている関係上、最初にfuga以下にあったhogeリソースにヒットし、そこではid指定があるアクションが生成されていなかったから、このようになってしまったのかと思いましたが、resoucesの順序を変更してもルーティングにeditが追加されませんでした。 解決方法 resoucesで使用するアクションを明示的に書いたら、ルーティングエラーが出なくなりました。 route.rb resources :hoges,only: [:index, :show, :new, :create, :destroy, :edit, :update] do end RailsAPIモードだったので、生成されるルーティングは下記になります。 ゆえに、editやdestroyなどがルーティングでそもそも生成されない仕様で、resoucesでも使えないようです。 rakeroutes rake routes api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index POST /api/v1/posts(.:format) api/v1/posts#create api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show PATCH /api/v1/posts/:id(.:format) api/v1/posts#update PUT /api/v1/posts/:id(.:format) api/v1/posts#update DELETE /api/v1/posts/:id(.:format) api/v1/posts#destroy 参考 Railsガイド「浅い」ネスト https://railsguides.jp/routing.html#%E3%80%8C%E6%B5%85%E3%81%84%E3%80%8D%E3%83%8D%E3%82%B9%E3%83%88  Railsのresourcesとresourceついて https://qiita.com/Atsushi_/items/bb22ce67d14ba1abafc5 Railsで超簡単API https://qiita.com/k-penguin-sato/items/adba7a1a1ecc3582a9c9
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む