- 投稿日:2020-11-23T23:47:15+09:00
seed_fu: herokuにrailsアプリをデプロイしたら,初期データが反映されてなかった
- 投稿日:2020-11-23T23:47:09+09:00
[初心者向け]怪奇!Herokuで画像投稿する際の謎のエラー
- 投稿日:2020-11-23T23:23:44+09:00
初期データをseeds.rbに記述して、ゲストログイン機能のエラーを解決してみた
はじめに
ゲストログイン機能を実装したが、ユーザー登録以前に、2つの情報を登録し、ユーザーと紐づけていたので、それを解決するために、試行錯誤した結果、初期データの作成にたどり着いた。
seeds.rbとは
Rails6.0では、(もう少し前からだとは思うが、)デフォルトで入っているファイル。dbディレクトリの配下にある。最初から、コメントアウトで色々記述されているが、説明なので、消してしまって構わない。
記述方法
seeds.rb
には、初期データとして作成しておきたい(テスト用などの目的)データを直接作る。seeds.rbUser.create!(name: 'ゲスト', email: 'gest@sample.com')
モデル名.create!(カラム名: 値)
が基本形。
create!
の部分は他にも、いくつか使えるメソッドがある。(次回、紹介予定)
上記のように、書けば、いくつでも初期データを作成できる。モデル名の部分を他に変えれば、別のテーブルにも作成可能。カラム名にidを用いることもできるので、いつもは自動で振り当てられるidについても、任意で作成可能。
ちなみに、eachメソッドやtimesメソッドを使って、繰り返し処理によって、大量の初期データを作成することも可能。
初期データ生成方法
ターミナルで、
rails db:seedを実行。特にエラーが無ければ、特に反応なく、次の行にいき、待機状態となる。(success!みたいに、表示してくれれば安心なのに…)。テーブルで実際に保存されているか、確認するとよい。
最後に
これで本番環境でも、生成のコマンドさえ実行すれば、無事に、ゲストログインもできるはず!
- 投稿日:2020-11-23T23:14:18+09:00
「N+1問題」とは??
1. 「N+1問題」とは??
アソシエーションを利用した際に、データベースへのアクセス回数が多くなってしまう問題を「N+1問題」といいます
例えば1つのツイート投稿(tweet)は1人のユーザー(user)とアソシエーションで結びついているとします。そして、データベースから全てのツイート投稿を取得するとアソシエーションによりユーザー情報も取得しようとします。
この時、次のコードだとターミナルではこのようなログを示します。class TweetsController < ApplicationController def index @tweets = Tweet.all end end
水色の箇所がTweetsテーブルとUsersテーブルにアクセスしているログです。
Tweetsテーブルに対しては1回で全ての情報を取得していますが、Usersテーブルには6回アクセスしています。
(このときTweetsテーブルに存在するTweetのレコードは6つです。)
取得したTweet1つ1つに対して、アソシエーションを使ってユーザーのレコードを取得する処理を繰り返していることを示しています。つまり100個のツイート投稿を取得すると、100回Usersテーブルにアクセスすることになり、データ取得に時間がかかります。その結果、アプリケーションのパフォーマンスが著しく下がることになります。これが「N+1問題」です。これを解決するためには、includesメソッドを利用します。2. includesメソッド
モデル名.includes(:アソシエーションで紐付くモデル名)includesメソッドは、引数に指定された関連モデルを1度のアクセスでまとめて取得するメソッドです。このメソッドを用いて以下のようにコードを書き換えます。
class TweetsController < ApplicationController def index @tweets = Tweet.includes(:user) end end
結果、水色の箇所が2つに変わりました。Tweetsテーブルに対して1回、Usersテーブルに対しても1回のアクセスで全ての情報を取得できました。3. さいごに
私自身、今まで勉強する中で膨大なデータの使用を想定した開発をしていませんでした。そのため「N+1問題」を勉強して、アプリケーションのパフォーマンスを意識した開発する重要性を感じました。この記事を読んで、内容が良かったらLGTMお願いします!また、ご意見があれば是非コメントもお願いします。
- 投稿日:2020-11-23T22:44:40+09:00
[Rails]collection_selectについて勉強してみた![初心者]
はじめに
現在作成しているアプリの中で、
collection_select
を使用する場面があり、備忘録のためにアウトプットします。
正直、このメソッドにたどり着くまでに、メチャクチャ時間がかかりました笑
collection_select
とは、モデルの情報を元に、セレクトボックスを生成できるメソッドです。
具体的に見ていきましょう!!やりたかったこと
セレクトボックス内に、ユーザーの登録済住所を選択肢として用意したかったのですが、どのような記載方法が適切なのか、全然分からなくて、けっこう長い時間グーグル検索して辿り着いたのが、
collection_select
です。
このセレクトボックスをクリックすると、
というように、登録している住所一覧が出るようにしたかったです!!使い方
collection_select(オブジェクト名, メソッド名, 要素の配列, value属性の項目, テキストの項目 [, オプション or HTML属性 or イベント属性])f.collection_select(メソッド名, オブジェクトの配列, value属性の項目, テキストの項目 [, オプション or HTML属性 or イベント属性])使用例
<%= f.collection_select(:address_id, @addresses, :id, :order_address, prompt: "選択してください") %>上の
collection_select
がHTMLでどうなっているのか確認すると、こうなっていました。<select name="order[address_id]" id="order_address_id"><option value="">選択してください</option> <option value="1">1111111京都市田中</option> <option value="2">2222222アメリカ佐藤</option> <option value="3">3333333インド安井</option> </select>それぞれの引数について確認しましょう!!
第一引数 → :address_id
第一引数(プロパティ名)は、selectタグのid属性とname属性に関係しています。
今回で言うと、
name="order[〜〜〜]" id="order_〜〜〜"
という風に反映されています。第二引数 → :@addresses
第二引数(オブジェクトの配列)は、コントローラで
@addresses = Address.where(customer_id: current_customer.id)
と定義しています。
つまり、現在ログインしているユーザー(cuurent_customer)の登録済住所を全て取得しているということです。第三引数 → :id
第三引数(value属性の項目)は、
<option value="〜〜〜">
において。設定したい値のカラム名が入ります。
今回はidカラムの値を設定するので、上記記述となります。第四引数 → :order_address
第四引数(テキストの項目)は、optionタグ内のテキスト
<option>〜〜〜</option>
に設定したい値のカラム名が入ります。第五引数 → prompt: "選択してください")
最後はオプションですが、これを付け足すことにより、
選択してください
の文言が一番上に表示されるようになります。
- 投稿日:2020-11-23T21:50:16+09:00
README に記述するアプリ開発の背景
はじめに
個人開発したオリジナルアプリのREADMEを記述しています。このアプリを開発した背景や問題、課題を記載しました。問題と背景の切り分け、考え方の本質を書き記します。
目次
1.問題と課題の違い
2.オリジナルアプリの開発背景から設定した問題と課題1.問題と課題の違い
辞書などにおける意味は下記の通り。言葉そのもの意味に差異はない。
問題:批判・論争・研究などの対象となる事柄。解決すべき事柄。課題。
課題:解決しなければならない問題。果たすべき仕事。しかし、ビジネスにおいては明確な違いがある。主に下記のような意味で使われる。
問題:好ましくない状態
課題:問題を解決すべく行うこと問題と課題の因果関係は上位に問題、下位に課題となる。基本的には1つの問題に対し、複数の課題がぶら下がる。逆算的に見れば複数ある下位の課題を全て解決すると、上位の問題も解決されることになる。
オリジナルアプリの開発背景から設定した問題と課題
背景
コロナ渦をきっかけとしてリモートワークが増えており、それに伴い自宅にいる時間も増えてる。普段の職場における労働と比べ、具体的には下記2点の傾向が増加していると考えた。・一人で部屋にこもりがち。
・仕事とプライベートの切り替えが曖昧。これまで仕事をしない時間帯や場所で仕事をする。問題
下記2点の理由から「メンタルヘルスが悪化する可能性がある」を問題として設定した(状態)。・人との接点が減り孤独を感じやすくなる
・デスク周りの汚れや整理整頓に対し、他者から指摘されない。部屋が汚く無意識にストレスを感じる。課題
メンタルヘルスが悪化するという問題に対し、下記2点を課題とした(やること)。
・対面に変わる、孤独を感じない仕組みの確立
・清潔が保たれている空間の確保
そして各課題をさらに細かく分類し、具体的な機能を7つ抽出した。逆算的にみれば抽出した7つの機能を満足することにより2つの課題は解決され、最終的には問題も解決できる。
以上
- 投稿日:2020-11-23T21:37:20+09:00
個人アプリ開発日記 #4
では前回書いた様々なメソッドが機能してくれるのか簡単なビューを使って確かめてみましょう、、!
まずは新規登録から
このような簡単なビューで新規登録
users/new.html
<% if current_user %> <h1>welcome to my app</h1> <% else %> <%= link_to "ログイン", login_path%> <% end %> <h2><%= link_to "ユーザー一覧",users_path %></h2> <h2><%= link_to "投稿一覧", drinks_path %></h2> <% unless logged_in? %> <div class="col-md-6 col-md-offset-3"> <%= render 'shared/form' %> </div> <% end %> <% if current_user %> <p>ログインしてます</p> <%= link_to @current_user.nickname, user_path(@current_user) %> <%= link_to "ログアウト",logout_path,method: :delete %> <%= link_to "投稿する", new_drink_path %> <%= link_to "退会する", user_path(current_user),method: :delete %> <% else %> <strong>ログインしてません</strong> <% end %>前回定義したcurrent_userメソッドや、logged_in?メソッドを使ってみました
これが今回のルート画面です
ログインしてみます
しっかり前回書いたコードが機能してるみたいですね、、、!!
users/show.html.erb
<h1><%= current_user.nickname%></h1> <%= link_to "toppage", root_path %> <%= link_to "setting", edit_user_path(current_user)%> <p><%= current_user.nickname %>さんの投稿一覧</p> <% @drinks.each do |drink| %> <%= drink.price%> <span class="name"><%= drink.name %></span> <p><%= drink.explain %></p> <% end %>drinkリソースの実装は次回で書きます
前回書いたusers#showあたりがしっかり機能していて、sessions_helperのcurrent_userも使えます
便利メソッドはビューでも呼び出せるので、ビューでも呼び出したいメソッドはhelperメソッドに定義した方がいいのかもしれません次はuserの更新を見てみましょう
先ほどのユーザー詳細ページにsettingというところをクリックすると
という画面に遷移します
コードはこんな感じ
users/edit.html.erb
<h1>Update <%= @user.nickname %>'s profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'shared/form' %> </div> </div><%= render 'shared/form' %>部分テンプレートを使用してるのでそちらもみてみましょう
shared/_form.html.erb
<%= form_with(model: @user, local: true) do |f| %> <%= render 'shared/error_messages', object: f.object %> <%= f.label :nickname %> <%= f.text_field :nickname, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%unless @current_user%> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation, class: 'form-control' %> <% end %> <%= f.submit "submit"%> <% end %>ここは新規登録ページのユーザーに関する入力フォームと一緒です
ですが
<%unless @current_user%>
<%end%>
とパスワードに関するフォームを囲むことにより、現在ログインしてる本人ならパスワードを改めて入力せずに
プロフィールの更新ができます。パスワードを入力せずに値を更新するためには
user.rb
validates :password, presence: true, length: { minimum: 6 },allow_nil: trueallow_nil: true にしなければなりません
パスワードがなくても新規登録できちゃうじゃん!
と思うかもしれませんが、has_secure_passwordをuser.rbに書いてるので、
新規登録の時はhas_secure_passwordくんがパスワードあるかどうか確かめてくれるらしいです、
賢いですね。とりあえず「そうくん」に名前を変更しときましょう
ログインしたことによるトップページの変化
では早速ログアウトしてみましょう
ログインしてないバージョンのtoppageに戻ります
そしてもう一回ログイン
ユーザーの詳細ページにリダイレクトされました
ユーザーが投稿したコーヒーの感想がチラッと見えてますが、これは次回投稿しますちなみにremember me on this computerにチェックしたので、ブラウザを閉じたりしてもログイン情報は保持される仕組みです
どのような仕組みかは前回解説しております
何はともあれ前回書いたコードがしっかり機能することが確かめられました、、、!
本当はrails consoleとかでもうちょい上手く確かめられると思うのですが、、、、、次回はコーヒーの感想を投稿したり、削除したり、一覧表示したり、誰の投稿かを定義したり、drinkリソースに関することをやっていきたいと思います
- 投稿日:2020-11-23T21:04:02+09:00
Rails wheneverをdockerで実行する
動機
Railsの定期実行を行う
whenever
を使おうとしたのですが、Mac上で直接実装すると環境変数や権限管理で大変だったのでdockerで行うことにしました前提条件
実行環境は下記のようになります
・ ruby 2.6.3
・ Rails 5.2.4
・ MySQL 8.0.19Docker関係
Dockfile
、docker-compose.yml
は以下のようになります
通常のdockerの設定とあまり変わらないのですが、今回はwheneverを利用するのでcorn
のインストールとcronをフォアグラウンド実行するための設定を追記しています。FROM ruby:2.6.3 #rubyのバージョン指定 #gemのインストール RUN apt-get update -qq && \ apt-get install -y build-essential \ libpq-dev \ nodejs # cronインストール RUN apt-get install -y cron RUN mkdir /my_app WORKDIR /my_app COPY Gemfile /my_app/Gemfile COPY Gemfile.lock /my_app/Gemfile.lock RUN gem install bundler RUN bundle install COPY . /my_app # wheneverでcrontab書き込み RUN bundle exec whenever --update-crontab # cronをフォアグラウンド実行 CMD ["cron", "-f"]docker-compose.ymlversion: '2' services: db: image: mysql:8.0.19 command: --default-authentication-plugin=mysql_native_password volumes: - ./mysql-confd:/etc/mysql/conf.d - mysql-data:/var/lib/mysql #データの永続化のため ports: - "3306:3306" restart: always environment: MYSQL_ALLOW_EMPTY_PASSWORD: 1 # MYSQL_DATABASE: app_development MYSQL_USER: root # MYSQL_PASSWORD: password TZ: Asia/Tokyo app: build: . command: bundle exec rails s -p 3000 -b '0.0.0.0' volumes: - .:/my_app - bundle:/usr/local/bundle ports: - "3000:3000" links: - db volumes: mysql-data: bundle: #bundle installした後buildし直さなくてよくなるwhenever関係
config/schedule.rb# wheneverにrailsを起動する必要があるためRails.rootを使用 require File.expand_path(File.dirname(__FILE__) + "/environment") # 環境変数をうまい感じにやってくれる ENV.each { |k, v| env(k, v) } # ログを書き出すようファイル set :output, error: 'log/crontab_error.log', standard: 'log/crontab.log' set :environment, :development #2分毎に`sample_task`の`scheduled_task`を実行する every 2.minutes do rake 'sample_task:scheduled_task' # runner "Test.yakisoba", :environment => :development # runnnerの例 end実行したいコマンド
/lib/tasks/sample_task.rbnamespace :sample_task desc "scheduled_task" task scheduled_task: :environment do ..... 実行したい関数 end end end
- 投稿日:2020-11-23T20:46:03+09:00
【RSpec】Ruby on Rails チュートリアル「第3章」のテストを RSpec で書いてみた
はじめに
Ruby on Rails チュートリアルでは「minitest」を使用してテストが実施されていますが、実際の現場では主に「RSpec」を使用してテストを実施するとのことなので、チュートリアルのテストを実際の現場に近づけるために「RSpec」で実施するようにしました。まず手初めに「第3章」のテストを RSpec で書き換えてみたので、同じようにチュートリアルのテストを RSpec で実施してみたい人の参考になれば幸いです。
対象者
- Ruby on Rails チュートリアルのテストを Rspec で実施予定、または実施してみたい人
- Ruby on Rails チュートリアル「第3章」のテストを Rspec で書きたいけど、書き方が分からない人
テストコード
実際にテストコードを書き換えた結果が下記になります。
Minitest
static_pages_controller_test.rbrequire 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest def setup @base_title = "Ruby on Rails Tutorial Sample App" end test "should get home" do get static_pages_home_url assert_response :success assert_select "title", "Home | #{@base_title}" end test "should get help" do get static_pages_help_url assert_response :success assert_select "title", "Help | #{@base_title}" end test "should get about" do get static_pages_about_url assert_response :success assert_select "title", "About | #{@base_title}" end endRSpec
static_pages_spec.rbrequire "rails_helper" RSpec.describe "StaticPages", type: :request do before do @base_title = "Ruby on Rails Tutorial Sample App" end describe "Home page" do it "should get home" do visit static_pages_home_url # /static_pages/home/ へアクセス expect(page.status_code).to eq(200) # HTTP ステータスコードが "200" か判定 expect(page).to have_title "Home | #{@base_title}" # タイトルに 「Home | Ruby on Rails Tutorial Sample App」 が含まれているか判定 end end describe "Help page" do it "should get help" do visit static_pages_help_url expect(page.status_code).to eq(200) expect(page).to have_title "Help | #{@base_title}" end end describe "About page" do it "should get about" do visit static_pages_about_url expect(page.status_code).to eq(200) expect(page).to have_title "About | #{@base_title}" end end end次回
「第4章」のテストコードを RSpec に書き換える予定です。
- 投稿日:2020-11-23T18:53:38+09:00
Rails 幹事向けアプリの作り方 複数レコードの同時登録編
最初に
この記事は、幹事向けのアプリを作成方法を記載します。
どんな構造を取るかアプリと言うと、2段階の複数登録を行うものです。①複数のテーブルと複数レコードを同時登録(イベントと複数の日付の登録)
②①で登録した複数レコード(日付など)の中間テーブルレコードの一括登録(出欠状態など)
開発時に、②複数のレコードを登録する方法について
参考となる情報がなく苦労しました。
そこで、作り方を公開して、同じ悩みを持った方の
助けに慣れればと思い記載致します。※UI部分については今回紹介していません。Gitにて確認お願いします。
目次
- 概要
- テーブルデータについて
- イベント(親)と複数の日程(子)、お店(子)の登録機能
- 参加者(親)と参加状況(子)の登録機能
- 参加者の参加状況の編集機能
- 参加状況の削除機能
1.アプリの概要
イベントを開催して、参加者の状況を管理。最終的にはイベントの日時・場所を決定するアプリです
ユーザーは2種類あり、「イベントの主催者」と「参加者」を想定しています。
Git:https://github.com/tsuyatsuya-april/ikang
HP:http://kyomo-ikang.com/events/1機能
主催者
・イベントの概要・候補日・候補店を登録・編集・削除する機能
2.テーブルデータについて
Userテーブル...主催者の名前・email・passwordを登録
Eventテーブル...イベントの名称と概要を登録
Scheduleテーブル...イベントの候補日を登録
Shopテーブル...イベントの開催場所候補を登録
Joinテーブル...参加者の名前を登録
DateAnswerテーブル...JoinとScheduleを親として、参加日程の状況を登録
ShopAnswerテーブル...JoinとShopを親として、開催場所の評価を登録作成するアプリでは、主に2つのフォームで下記の関係での登録を行う
Event(親)、Schedule(子)、Shop(子)のイベント登録フォーム
Join(親)、DateAnswer(子)、ShopAnswer(子)の参加者登録フォーム3.イベント(親)と複数の日程(子)、お店(子)の登録機能
イベントの登録機能を下記の小項目に沿って説明を行う。
尚、ユーザーの登録は、メジャーなので割愛させていただきます。
また、コードの一部抜粋した形で説明させていただきます。小項目
- 注目コード記載(Model,Controller,HTML,JS)
- fields_forメソッド
- name属性の修正
1.注目コード記載
model/event.rbclass Event < ApplicationRecord belongs_to :user has_many :shops, inverse_of: :event, dependent: :destroy accepts_nested_attributes_for :shops, allow_destroy: true endmodel/shop.rbclass Shop < ApplicationRecord belongs_to :event, inverse_of: :shops validates_presence_of :event endevents_controller.rbdef new @event = Event.new 1.times { @event.shops.build } end def create @event = Event.new(event_params) @event.user_id = current_user.id if @event.save redirect_to event_path(@event.id) else render "new" end end private def event_params params.require(:event).permit(:name, :description, schedules_attributes: [:savedate, :savetime], shops_attributes: [:shop_name, :shop_url, :map_url, :comment]) endevents/new.html<%= form_for @event,id:"event-new-form", local: true do |f| %> <%= render "share/error_messages", model: f.object %> <% 中略%> <div id="shop-add-btn"> <%= link_to "お店追加", "#", class:"btn-flat-border" %> </div> </div> <%= f.fields_for :shops do |shop_fields| %> <div id="new-shop-top"> <div id="new-shop"> <div id="shop-name-box"> <div id="shop-name"><p>店名(必須)</p></div> <%= shop_fields.text_field :shop_name, class:"shop-name-input"%> </div> <% 中略 %> <div id="shop-delete-box"> <%= link_to "お店削除", "#", class:"btn-flat-border-red", id:"shop-delete" %> </div> </div> </div> <% end %> </div> <div class="submit-box"> <%= f.submit "イベントの登録",class:"btn-flat-border-submit", id:"new-submit" %> </div> <% end %>main.js//お店の追加 function newShopAdd(){ nameNumberShopAdjust(); const shopParent = document.getElementById("new-shop-top"); const addShopBtn = document.getElementById("shop-add-btn"); let currentShopLength = document.querySelectorAll("#new-shop").length; let nextNum = currentShopLength; let shopHtml = ` <div id="new-shop"> <div id="shop-name-box"> <div id="shop-name"><p>店名(必須)</p></div> <input class="shop-name-input" type="text" name="event[shops_attributes][${nextNum}][shop_name]" id="event_shops_attributes_${nextNum}_shop_name"> </div> ~<中略>~`; addShopBtn.onclick = function(){ shopParent.insertAdjacentHTML("beforeend", shopHtml); shopDelete(); newShopAdd(); }; } //お店の削除 function shopDelete(){ let shopParent = document.querySelectorAll("#new-shop"); let shopDeleteBtn = document.querySelectorAll("#shop-delete"); let shopParentLength = shopParent.length; for (let i=0; i < shopParentLength; i++){ shopDeleteBtn[i].onclick = function(){ let conformShopLength = document.querySelectorAll("#new-shop").length; if(conformShopLength != 1){ return shopParent[i].remove(); } }; }; } //お店のname属性の値に入る数値の調整 function nameNumberShopAdjust(){ let saveShop = document.querySelectorAll(".shop-name-input"); let saveShopLength = saveShop.length; for(let j=0; j<saveShopLength; j++){ saveShop[j].removeAttribute("name"); saveShop[j].setAttribute("name",`event[shops_attributes][${j}][shop_name]`); } } }2.fields_forメソッド
このメソッドは、公式ドキュメントにて下記のような説明をしている。
モデルを固定してフォームを生成
form_for内で異なるモデルを編集できるようになる。つまり、今回でいうとEvent(親)とは別のShop(子)モデルの編集ができるようになるというものである。
ただし、fields_forを使う為にはいくつか準備が必要である。アソシエーションの設定
model/parent.rb親と子のモデルの双方向の関連付けができるようにする has_many :子モデル(複数形), invers_of: :(親モデル), dependent: :destroy 子モデルが同時に登録できるようにする accepts_nested_attributes_for :子モデル(複数形), allow_destroy: true(空白のフォームがあれば登録できないようにする)model/child.rb親と双方向の関連付けしてバリデーション設定などができるようにする。(例 shops.eventなど) belongs_to :親モデル, inverse_of: :子モデルコントローラの設定
controller/test.rbdef new @event = Event.new 関連した子の要素を生成する時はbuild(newのエイリアス)を使用する 1times { @event.shops.build } ちなみにtimesの数値の分だけ子の要素の登録フォームができる end 通常に保存する場合と同じ def create @event = 親モデル.new(event_params) if @event.save redirect_to 指定のパス else render アクション名 end end private def event_params params.require(:親モデル).permit(:親カラム1, :親カラム2, 子モデル名(複数系)_attributes: [:子カラム1]) .merge(user.id: :current_user.id) endビューの設定
views/test.html<%= form_for @event,id:url, local: true do |f| %> <%= f.text_field :親カラム名 %> ここで子モデルの登録ができるように設定を行う <%= f.fields_for :子モデル do |sf| %> <%= sf.text_field :子モデルカラム名 %> <% end %> <%= f.submit "イベントの登録" %> <% end %>上記のビュー・モデル・コントローラーの設定を行えば、
フォームの送信ボタンを押した後にデータが下記のようなパラメータで送られます。paramater.rb通常の場合 親モデル名 = { :first => 1, :second => 2} fields_forを使用した場合 親モデル名 = {:first => 1, :second => 2, 子モデル名 => {:first => 1, :second => 2 }}親モデルの中に子モデルがネストされてデータが受け渡され保存されるということになります。
これで複数モデルの複数レコードの保存の下地が整いました。3. name属性の修正
2では子モデルの複数のレコードが保存できる設定の説明を行ってきた。
ただし、javascriptを用いてお店と日程フォームの増減が行えるように設定をしています。
この時に保存が上手く行かなくなることがあったので、その原因と解消方法について記載します。問題
javascriptでフォームの追加を実行した後に全てのフォームが保存されない時があった。前提条件
fields_forを使った時のフォームのname属性がhtml上で下記のように変換されて表示されるtest.htmlfields_for内のinputタグの中にあるname属性の名前 event[schedules_attributes][0][savedate] event[schedules_attributes][1][savedate] event[schedules_attributes][3][savedate] 公式化 親モデル名[子モデル名(複数系)_attributes][要素番号][子モデルの対象カラム名]解消方法
javascriptを使ってデータの送信前にname属性の要素番号が被らないように調整する。main.js//(送信ボタンを押した時に要素番号を調整するメソッドを実行) function nameNumberShopAdjust(){ //お店のフォームのセレクタを取得 let saveShop = document.querySelectorAll(".shop-name-input"); //要素の数がいくらあるかを取得 let saveShopLength = saveShop.length; //要素数分だけループを実行 for(let j=0; j<saveShopLength; j++){ //フォームの既存で設定されたname属性を削除する saveShop[j].removeAttribute("name"); //フォームのname属性について、現在のループ回数を要素番号として付け直す。 saveShop[j].setAttribute("name",`event[shops_attributes][${j}][shop_name]`); } } }ここの部分に関しては、ドキュメントに記載されておらず挙動を読んで
仮説検証をたてたものですが上記方法で解決致しました。4.参加者(親)と参加状況(子)の登録機能
大項目3で指定した日程とお店について、参加状況を登録する方法について記載する。
join(親)、shop(親)、shop_answer(子)の中間テーブル、
join(親)、schedule(親)、date_answer(子)の中間テーブル
上記2つの登録を主に行う。
尚、解説はお店の方のみさせていただく小項目
- 注目コード記載(Model,Controller,HTM, routes)
- viewのname属性の調整
- 中間テーブルの親_idの格納
- event/showページで子のjoinコントローラにパラメータを渡す方法
- joinコントローラーのupdateアクション
1. 注目コード記載(Model,Controller,HTM, routes)
model/join.rbhas_many :shop_answers, dependent: :destroy accepts_nested_attributes_for :shop_answers, allow_destroy: truemodel/shop.rbhas_many :shop_answesr, dependent: :destroymodel/shop_answer.rbbelongs_to :join validates_presence_of :join belongs_to :shopevents_controller.rbdef show if params[:join_id] set_join else @join = Join.new 1.times { @join.shop_answers.build } end endjoins_controller.rbdef create @join = Join.new(join_params) if @join.save redirect_to event_path(params[:event_id]) else render "events/show" end end private def join_params params.require(:join).permit(:nickname, :email, shop_answers_attributes: [:shop_id, :status, :vote]) .merge(event_id: params[:event_id]) endevents/show.html<%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form", local: true do |f| %> <div id="join-box"> <div id="join-name-label"> <label>ユーザー名</label> </div> <div class="join-users"> <%= f.text_field :nickname, id:"join-user",placeholder:"ニックネームを入力ください"%> </div> <div id="shop-answer"> <h1 id="shop-answer-title">店舗選択</h1> <div class="circle-text">説明: お店を◯×△で評価,一番良いと思う店に一番ボタンで投票下さい</div> <table id="shop-answer-table"> <tbody> <% @event.shops.each do |es| %> <%= f.fields_for :shop_answers do |shop_fields| %> <tr id="join-shops" class="bottom-line"> <th class="shop-label"> <div> <%= link_to es.shop_name, es.shop_url, target: :_blank %> </div> </th> <td class="shop-vote-balance"> <%= shop_fields.hidden_field :shop_id,class:"shop-id", value: es.id %> <%= shop_fields.hidden_field :status, id:"shops-status" %> <%= shop_fields.hidden_field :vote, id:"shops-vote" %> <div class="change-status"> <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow " id="shop-yes">◯</h1> <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow choice" id="shop-delta">△</h1> <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow cross-vote" id="shop-no">×</h1> </div> <div id="shop-change-vote"> <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow" id="shop-vote">一番</h1> </div> </td> </tr> <% end %> <% end %> </tbody> </table> </div> <div id="join-submit-box"> <%= f.submit "回答",class:"btn-flat-border", id:"join-submit-inputbox", :onclick => "return check_name()" %> </div> <% end %>routes.rbRails.application.routes.draw do root to: "events#index" resources :events do resources :schedules resources :shops resources :joins resources :comments resources :date_decisions resources :shop_decisions end end2. viewのname属性の調整
大項目3で登録した複数データに対して、複数の中間テーブルの保存を行うには下記のコードの構造をとる。
events/show.html<%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form", local: true do |f| %> 親モデルのnicknameカラムの保存フォーム <%= f.text_field :nickname, id:"join-user",placeholder:"ニックネームを入力ください"%> イベントページに紐づいているお店のレコードを全て出力する。 <% @event.shops.each do |es| %> fields_forで複数レコード登録できるようにする。 <%= f.fields_for :shop_answers do |shop_fields| %> 中間テーブルの親IDを登録するフォーム。事前にid番号をvalueに格納する <%= shop_fields.hidden_field :shop_id, value: es.id %> javascriptを使って、数字1(=◯),2(=△),3(=×)が格納されるようにしている <%= shop_fields.hidden_field :status, id:"shops-status" %> javascriptを使って投票数0,1を格納する。 <%= shop_fields.hidden_field :vote, id:"shops-vote" %> onclickでユーザーのニックネームが格納されていない場合にフォームを送信できないようにする <%= f.submit "回答",:onclick => "return check_name()" %> <% end %> <% end %> <% end %>
解決方法
大項目3と同様な形でjavascriptを用いて、
出力されたお店の分、name属性の要素番号を付け直す
これで出力されたお店のレコード分だけ、中間テーブルのレコードが保存できるようになった。3. 中間テーブルの親_idの格納
中間テーブルであるshop_answerテーブルに親IDを紐付ける方法を記載する。
・1点目のjoin_idはfields_forメソッドを使用しているので親子関係となり自動的にIDが割り振られる。
・2点目のshop_idは下記の通りeachメソッドを用いて、各ID番号をshop_idのフォームに格納している。events/show.html<% @event.shops.each do |es| %> <%= f.fields_for :shop_answers do |shop_fields| %> 先ほども記述したがes.idでshop(親)のID番号をshop_answer_idに渡している。 <%= shop_fields.hidden_field :shop_id, value: es.id %> <% end %> <% end %>以上で中間テーブルの保存ができる状態になった
4. events/showページで子のjoinコントローラにパラメータを渡す方法
初学者にとってevents/showページで登録を行う場合、
events_controllerにパラメータをpostするという印象が強い。
しかし今回はjoins_controllerにパラメータをpostして登録を行うので
その場合のurl指定方法を記載する。まず下記コマンドを実行しURLの確認を行う
test.rbrails routes次に確認したURLの内joinの登録であるURLをform_withメソッドの中に適応させる
events/show.html<%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form", local: true do |f| %>以上を行うことで、events/showページからjoins_controllerにデータを受け渡すことができるようになる。
5.参加者の参加状況の編集機能
events/show.htmlにて参加者毎の編集フォームを表示する方法について記載を行う。
複数の情報を出力する上で小項目
- 同一ページ内で参加者IDの情報をコントローラに受け渡す方法
- fields_forを用いた場合の編集フォームのviewを表示について。
- objectメソッド、親データの店名やURLを引き出す方法
1. 同一ページ内で参加者IDの情報をコントローラに受け渡す方法
まず、events/show.html上に参加者の編集フォームを出力するには、
コントローラに各参加者のjoin_idの数値を送り、データを抽出する必要がある。その為に、link_toメソッドを用いて、join_idをパラメータとしてコントローラに引き渡す手法を用いた。
events/show.html<% if @event.joins %> 登録されている参加者全てを出力する <% @event.joins.each do |event_join| %> ループ中の参加者のID、join.idをjoin_idというキーに格納してコントローラに渡している <%= link_to event_join.nickname, event_path(@event.id,join_id: event_join.id) %> <% end %> <% end %>コントローラではjoin_idというパラメータの有無によって
編集フォームの出力か登録ページの出力かの分岐を行っている。events_controller.rbdef show @joins = Join.all if params[:join_id] set_join else @join = Join.new 1.times { @join.shop_answers.build } end end private def set_join @join = Join.find(params[:join_id]) end編集フォームの場合は、決まった値をフォームに格納した状態で表示される。
登録フォームの場合は、新規にデータを作成できる状態で表示される
これでビューに表示できる条件が整った。2. fields_forを用いた場合の編集フォームのviewを表示について
登録時には、shop.each doとfields_forを用いたが編集時には、fields_forのみを用いれば、選択したJoinレコードに紐づくshop_answerレコードが全て出力される。
events/show.html<% unless params[:join_id] %> 新規登録の時の処理 <% else %> 編集時の処理 <%= form_with model: @join, url:event_join_path(@event.id, @join.id), id:"join-edit-form" , local: true do |f| %> ユーザー名の編集 <%= f.text_field :nickname,id:"join-edit-user", placeholder:"ニックネームを入力ください"%> fields_forを用いて、選択したJoinレコードに紐づく全てのshop_answerレコードを出力 <%= f.fields_for :shop_answers, @join.shop_answers do |shop_edit_fields| %> 編集ページでは新たに、joinのidカラムを格納するフォームを用意する。それ以外は登録と同じ。 <%= shop_edit_fields.hidden_field :id,class:"shop-answers-id" %> <%= shop_edit_fields.hidden_field :shop_id,class:"shop-edit-id" %> <%= shop_edit_fields.hidden_field :status, id:"shops-edit-status" %> <%= shop_edit_fields.hidden_field :vote, id:"shops-edit-vote" %> <% end % > <%= f.submit "更新",class:"btn-flat-border", id:"join-edit-submit-inputbox", :onclick => "return check_name()" %> <% end %> <% end %>同じ躓きをする方もいると思うので私が失敗した時の場合も下記の画像にて記載します。
3. objectメソッド、親テーブルの店名やURLを引き出す方法
小項目2では、フォームが正しく表示されたが実は1つ問題がある。
それは、fields_forでは、
「中間テーブルの子モデルに対し、親モデルのデータが引き出せない」ことである。
私のアプリでは店名やURLが表示されず、どのデータに紐づいているか判断出来なくなる。これに対して、shops.each do を用いて、店名を引き出せば良いと考えたこともあった。
しかし、結果は小項目2の最後の画像の通り、フォームが正しく表記されない。そこで、objectメソッドを用いることにした。
このメソッドを変数の後につけることで、変数に格納しているデータを取得できる。test.HTMLshop_answer = { id => 1, join_id => 1, shop_id => 1, status => 1} shop = { id => 1, shop_name => "吉野家", shop_url => "yoshinoya" } <%= f.fields_for :shop_answers, @join.shop_answers do |shop_edit_fields| %> snum = shop_id = 1 <% snum = shop_edit_fields.object.shop_id%> sn = Shop.find(1) <% sn = Shop.find(snum) %> これでfields_for内でshop_answerの親モデルのお店情報が出力できるようになった sn.shop_name = 吉野家 sn.shop_url = yoshinoya5. joinコントローラーのupdateアクション
fields_forを用いた時は、paramsの表記方法が異なる。
登録時には子モデルのID番号が必要なかったが
更新時には小モデルのID番号が必要となるまた、:_destroyをつけることで親モデルに紐づく子モデルのデータを削除することができる。
例えば、編集時に子モデルのフォームを空白に変更して更新するとデータが削除されるなど。joins_controller.rbdef update if @join.update(join_update_params) redirect_to event_path(params[:event_id]) else render "events/show" end end private 通常時 def join_params params.require(:join).permit(:nickname, :email, date_answers_attributes: [:schedule_id, :status], shop_answers_attributes: [:shop_id, :status, :vote]) .merge(event_id: params[:event_id]) end 更新時 def join_update_params params.require(:join).permit(:nickname, :email, date_answers_attributes: [:id,:schedule_id, :status, :_destroy], shop_answers_attributes: [:id,:shop_id, :status, :vote, :_destroy]) end6. 参加状況の削除機能
アソシエーションの設定で親レコードの削除に伴い紐づく子のレコードが削除されるようにする必要がある。
allow_destroy: trueは
親要素が削除された時、関連付けている情報もまとめて削除するためのオプションです。model/join.rbhas_many :date_answers, dependent: :destroy accepts_nested_attributes_for :date_answers, allow_destroy: truemodel/shop.rbhas_many :shop_answer, dependent: :destroymodel/shop_answer.rbbelongs_to :join validates_presence_of :join belongs_to :shop以上のアソシエーションをつけることで通常時と変わりなく、削除を実行することができる。
今回はビューとコントローラーの記述を割愛させていただきます。最後に
ここまで読んで下さってありがとうございます。
はじめて個人で作成したアプリなのでアラが目立つかとは存じますが
ご容赦下さいますようお願いします。また、何かご質問があればコメントお願いします。
お答えできる限りはお答え致します。参考
- 投稿日:2020-11-23T17:32:00+09:00
GitHub Actions を使った Rails アプリの自動デプロイを10分で理解する
こんにちは、駆け出しエンジニアのよしこ @k2_yoshikoukiです
CI/CDってカッコいいですよね。なんかこう、カッコいいんですよね(語彙力)
しかし「どうやって実装したらいいのかワケワカメ\(^o^)/」というエンジニアの方は多いと思います。でも実際はとても簡単なんだよということを知ってほしかったので、10分で読める記事で CI/CD のうちのCD(自動デプロイ)を実装していきたいと思います。
実作業時間は詰まらなかったら30分かからないくらいです。
ゴール
CI/CD をやってみたいがやったことがないし何から手を付けたら良いか分からないエンジニア向けに、Rails アプリ (6系) で自動デプロイを最速で実装する。
結果、GitHub Actions を使ったCI/CDの大枠を把握できて自力で実装できるようになる
実装フローを見ていく
- Rails アプリを作る
- Heroku に手動デプロイして動作確認
- GitHub Actions の初期設定
./.github/workflows/{好きな名前}.yml
の作成- 必要な鍵などの取得・GitHubへ設定
すでにアプリケーションがある方は 3 からです。たった2ステップで自動デプロイが実装できます。早速やっていきましょう
手順
Railsアプリの準備
rails new sample-rails-with-gha
~/tmp/sample-rails-with-gha ❯ rails -v Rails 6.0.3.4 ~/tmp/sample-rails-with-gha ❯ ruby -v ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19] # 後から2.7.2にアップデートします(デバックのため)bundle install rails server
リポジトリの準備
- GitHub上にリポジトリを作る
https://github.com/newローカルにリポジトリを登録
git remote add origin https://github.com/yoshikouki/sample-rails-with-gha.git
アップロード
git add -A git commit -m "rails new" git push -u origin masterデプロイの初期設定と初回デプロイ
今回はデプロイ先に Heroku を使う(簡単なので)
~/tmp/sample-rails-with-gha master ❯ heroku --version zsh: command not found: herokuHeroku 入ってなかった・・・
brew tap heroku/brew && brew install heroku # https://devcenter.heroku.com/articles/heroku-cliHeroku アカウントを作っておく
https://signup.heroku.com/Heroku にログイン(ターミナル上)
heroku login --interactive
Heroku 用のアプリケーションを作る(アップロードする場所を作る)
heroku create # URL を控えておく # https://sleepy-beyond-32826.herokuapp.com/ # `$ heroku open` でも開けるHeroku は sqlite3 に対応していないため、
./Gemfile
を操作するHeroku へデプロイする
git push heroku masterあらら
- エラーログ
$ heroku logs 2020-11-22T14:39:05.610528+00:00 app[web.1]: I, [2020-11-22T14:39:05.610419 #4] INFO -- : [a6b71e5b-f13a-4224-a857-9b791da3ecc4] Started GET "/" for 133.32.232.41 at 2020-11-22 14:39:05 +0000 2020-11-22T14:39:05.611222+00:00 app[web.1]: F, [2020-11-22T14:39:05.611152 #4] FATAL -- : [a6b71e5b-f13a-4224-a857-9b791da3ecc4] 2020-11-22T14:39:05.611223+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] ActionController::RoutingError (No route matches [GET] "/"): 2020-11-22T14:39:05.611224+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] 2020-11-22T14:39:05.611225+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call' 2020-11-22T14:39:05.611225+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call' 2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:37:in `call_app' 2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:26:in `block in call' 2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:80:in `block in tagged' 2020-11-22T14:39:05.611227+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:28:in `tagged' 2020-11-22T14:39:05.611227+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:80:in `tagged' 2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:26:in `call' 2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/remote_ip.rb:81:in `call' 2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/request_id.rb:27:in `call' 2020-11-22T14:39:05.611229+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/method_override.rb:24:in `call' 2020-11-22T14:39:05.611229+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/runtime.rb:22:in `call' 2020-11-22T14:39:05.611230+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call' 2020-11-22T14:39:05.611230+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/executor.rb:14:in `call' 2020-11-22T14:39:05.611231+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/static.rb:126:in `call' 2020-11-22T14:39:05.611231+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/sendfile.rb:110:in `call' 2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/host_authorization.rb:76:in `call' 2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/engine.rb:527:in `call' 2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/configuration.rb:228:in `call' 2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:713:in `handle_request' 2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:472:in `process_client' 2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:328:in `block in run' 2020-11-22T14:39:05.611234+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/thread_pool.rb:134:in `block in spawn_thread' 2020-11-22T14:39:05.612663+00:00 heroku[router]: at=info method=GET path="/" host=sleepy-beyond-32826.herokuapp.com request_id=a6b71e5b-f13a-4224-a857-9b791da3ecc4 fwd="133.32.232.41" dyno=web.1 connect=1ms service=4ms status=404 bytes=1902 protocol=https 2020-11-22T14:39:05.858618+00:00 heroku[router]: at=info method=GET path="/favicon.ico" host=sleepy-beyond-32826.herokuapp.com request_id=831fe7f9-bc01-496d-a9ac-380c9c89dd3c fwd="133.32.232.41" dyno=web.1 connect=1ms service=2ms status=200 bytes=143 protocol=https
- なるほどねえ(分かっていない)
ActionController::RoutingError (No route matches [GET] "/"):
# ./config/routes Rails.application.routes.draw do # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html endGitHub Actions を設定していく
空の設定ファイルを作る
mkdir -p ./.github/workflows tocuh -p ./.github/workflows/sample_cd.yml設定していく
設定ファイル
name: Deploy to sample-rails-with-gha on: # master ブランチにプッシュされた場合に動作する push: branches: - master jobs: deploy: runs-on: ubuntu-latest steps: # GitHub リポジトリからコードを引っ張ってくる - name: Checkout uses: actions/checkout@v2 # 前回のコンテナキャッシュがあれば使用する。なければキャッシュを作る - name: Cache multiple paths uses: actions/cache@v2 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- # Ruby をインストールする - name: Set up Ruby 2.7 uses: actions/setup-ruby@v1 with: ruby-version: 2.7 # バンドラーをインストールし、初期化する - name: Bundle install run: | gem install bundler bundle config path vendor/bundle bundle install --jobs 4 --retry 3 # ヘロクにデプロイする - uses: akhileshns/heroku-deploy@v3.6.8 # This is the action with: heroku_api_key: ${{secrets.HEROKU_API_KEY}} heroku_app_name: "sleepy-beyond-32826" heroku_email: "yoshikouki@gmail.com"簡単な設定ファイルなので説明はファイル内のコメントを参考にしてください。
- アクションという Ruby でいう Gem のような機能を使用しています。
ドキュメント
secrets.HEROKU_API_KEY
が必要なので Heroku で取得する
- https://dashboard.heroku.com/account もしくは
自分のアイコン
>Account settings
- 下部
API Key
ここのKeyをコピーする取得した Key をGitHub で使用できるように設定する
sample-rails-with-gha
のリポジトリ >Settings
>Secrets
- GitHub Actions で使用する非公開環境変数の設定画面になる >
New repository secret
- Name に
HEROKU_API_KEY
Value に先程Herokuで取得したキーをセット >Add secret
準備は整ったので、master/main ブランチにプッシュしてみる
- そして順調にエラーになった
- エラーになった画面
https://github.com/yoshikouki/sample-rails-with-gha/runs/1438564960?check_suite_focus=trueデバッグ
- actions/cache のドキュメントは bundle がインストールしてある前提だったので
gem install bundler
を追加したGemfile に記述されている ruby-version と GitHub Actions のコンテナ内のRubyを一致させること
uses: actions/setup-ruby@v1 with: ruby-version: 2.7
- GitHub Actions setup-ruby ではマイナーバージョンまでしか指定できないみたいなので、手元Rubyのメンテナンス (ビルド) バージョンをあげることになるかもしれない
- マイナーバージョンが上がるわけではないので、素直に上げておけばいいと思う
そして...
まとめ
以上で Rails アプリ (6系) で自動デプロイまでを最速で実装しました。
以後は、master (main) ブランチへ push するだけで heroku へ自動デプロイされます。これで2〜3コマンド分の実行の手間が省けたことになりますし、あなたはもう完全に CI/CD を習得したエンジニアです。
(実際のところ、今回はCI部分を何も扱っていませんし、デプロイに関してもデータベースを触っていないので中途半端な状態ではあるのですが、最低限は実装できたので良しとしましょう。してください。)
参考URL
- 投稿日:2020-11-23T17:18:42+09:00
【Rails】formの検索ボタンを虫眼鏡のアイコンにする
Ruby on RailsでFont-Awesomeを初めて使ったの投稿しました。
Font Awesome HP
https://fontawesome.com/icons?d=galleryポイント:使いたいアイコンのUnicodeの前に \u をつける
1 . CDNでfont-awesomeを読み込む
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">2 . submitの記述の後を下記に変更。
<%= f.submit "\uF002" %>3 . cssで下記を追加
font-family: FontAwesome;
- 投稿日:2020-11-23T17:18:42+09:00
【Rails / FontAwesome】formの検索ボタンを虫眼鏡のアイコンにする
Ruby on RailsでFont-Awesomeを初めて使ったの投稿しました。
Font Awesome HP
https://fontawesome.com/icons?d=galleryポイント:使いたいアイコンのUnicodeの前に \u をつける
1 . CDNでfont-awesomeを読み込む
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">2 . submitの記述の後を下記に変更。
<%= f.submit "\uF002" %>3 . cssで下記を追加
font-family: FontAwesome;
- 投稿日:2020-11-23T16:25:17+09:00
【Rails】form_withの使い方について解説【初心者向け】
はじめに
Rails開発をする中で、form_withを使う機会は非常に多いですが、
- 結局form_withとは何なのか?
- form_withにはどんな役割があるのか?
- form_withはどのように用いたらいいのか?
といった悩みを抱えている方は多いと思います。
本記事はそのような方達に向けて、書いた記事となります。
ぜひ最後までご覧下さい。
使用技術
Ruby on Rails 5.1〜
Ruby 2.6〜筆者の自己紹介
- テックキャンプ86期生(2020/9/21~)
- 執筆時点で学習開始から約9週間経過
- 約6週間で最終課題(メルカリクローンアプリの作成)を終了
本記事の内容
1.form_withとはそもそも何者なのか?
2.form_withはどのような役割を果たしているのか?
3.form_withの具体的な記述法
4.まとめ
5.最後に
6.参照サイト1.form_withとはそもそも何者なのか?
form_withは①ヘルパーメソッドの一種で、ビューファイル上で②フォーム入力欄を作成できるメソッドのことを言います。
①ヘルパーメソッド
ヘルパーメソッドとはRails内部で定義されているメソッドのことで、ビューファイルにおける記述をシンプルにするためのものです。
ビューファイルではERBタグ(本記事ではビューファイルにテンプレートエンジンとしてerbを使用)を使えば、直接Rubyの記述を書くことはできますが、DRYの視点から見た場合そのようなコードは綺麗なコードとは言えません。
そういったことを防ぐためにヘルパーメソッドは使用されます。
ヘルパーメソッドを使用しない場合<%= form action="/posts" method="post" %> <%= input type="text" name="content" %> <%= input type="submit" value="投稿する" %> <%= /form %>ヘルパーメソッドを使用した場合<%= form_with url: "/posts", method: :post, local: true do |form| %> <%= form.text_field :content %> <%= form.submit '投稿する' %> <% end %>
またヘルパーメソッドはform_withの他にも様々なものが存在していて、有名なものでいえばlink_toメソッドが存在します。※他のヘルパーメソッドが気になる方は下記で詳細を確認してみてください
ActionView::Helpers②フォーム入力欄を作成できる
またヘルパーメソッドはビューファイル上で用いられ、HTMLの代わりのような役割を果たします。
具体的にはHTMLタグを出現させたり、テキストの加工を行うことが可能です。
form_withの場合は、HTMLにおけるinputタグにあたるものを生成できます。
つまりフォーム入力欄が作成できることになります。
HTMLのフォーム記述<form action="/posts" method="post"> <input type="text" name="content"> <input type="submit" value="投稿する"> </form>ヘルパーメソッドを用いたフォーム記述<%= form_with url: "/posts", method: :post, local: true do |form| %> <%= form.text_field :content %> <%= form.submit '投稿する' %> <% end %>③まとめ
ヘルパーメソッドとはRails内部で定義されているメソッドのことで、ビューファイルでの記述をシンプルにするためのもの
form_withはヘルパーメソッドの一種である
form_withはHTMLでinputタグにあたるものを作成し、フォーム入力欄を作成することができる
2. form_withはどのような役割を果たしているのか?
次にform_withメソッドは実際どのような役割を果たしているのか?について考えていきます。
結論からいうと、form_withメソッドは
ユーザーからサーバー側に情報を送信する
という役割を果たしています。
これだけでは少しわかりづらいので、実際のアプリケーションであるTwitterを例に考えてみましょう
Twitterでform_withがどういう役割を果たしているのか?考えてみる
Twitterには様々な機能がありますが、その中でも今回は投稿機能に着目して考えてみます
Twitterの投稿機能では
①ユーザーが投稿フォームに値を入力し、ボタンをクリックする
②その投稿がサーバー側に送られる
③サーバーに送られた投稿内容が投稿一覧表示画面に反映されるという流れで処理が行われています。
この際にform_withがどのような動きをしているのか?を考えてみましょう。
①ユーザーが投稿フォームに値を入力し、ボタンをクリックする
まず最初に、form_withで作成された投稿フォームにユーザーが値を入力し、送信(投稿)ボタンをクリックします。
作成した投稿フォーム<%= form_with url: "/posts", method: :post, local: true do |form| %> <%= form.text_field :content %> <%= form.submit '投稿する' %> <% end %>②その投稿がサーバー側に送られる
次に、送信(投稿)ボタンを押したため、先ほど作成したツイートの内容が送信されます。
ここで大半の人が疑問に思っていることが
「ツイートが送信されるのは感覚的にはわかるが、どこへ送信されているのか?」
「ツイートはどのようにして送信されているのか?」だと思います。
なので、この送信される過程についてもう少し詳しく見ていきましょう。
この過程は大きく分けると、次の3つに分かれます。
- リクエストが送信される
- ルーティングによって、「どのコントローラーのどのアクションを行うのか?」が決められる
- 2で指定されたコントローラーへツイートの内容が渡される
1. リクエストが送信される
まず送信ボタンをクリックすると、使用しているブラウザからサーバー側にリクエストが送られます。
リクエストに関してわからない人がいれば、「HTTPリクエスト」と検索してみてください。
簡単にいうと、ユーザーからサーバーへの「〜して欲しい!」という要望みたいなものです。そしてこの時リクエストには パラメーターと呼ばれるものが含まれます
※ パラメーター
パラメーターとはリクエストに含まれてサーバーの外部から渡されるデータのことを指します。
わかりやすく言い換えると、外から入ってくる値のことです。
少し身近な例に例えて考えてみましょう。
例えば、自動販売機を想像してみてください。
自販機は100円を入れたら、「100」と料金表示の欄に表示されます。また1000円札を入れれば、「1000」と表示されます。
この時、自販機の料金表示機能をプログラムと見立てた場合に、
自販機に入金したお金というのは自販機側からは外から入ってくるものであるので、パラメータになります。
これがパラメータのイメージです。
そしてパラメータにはいくつか種類があります。
例えば、URLに含まれるパラメータはURLパラメータと呼ばれたりします。これは http://tweets.jp/tweets/1 の
/1
ように、URLの末尾に文字列が含まれているものを指します。2. ルーティングによって、「どのコントローラーのどのアクションを行うか」が決定される
次に送信されたリクエストは、サーバー側のルーターに遷移します。
このルーターでは受け取ったリクエストを見て、「どのコントローラのなんの処理を行うか」を決定します。
このように、
「このリクエストに対してはこのコントローラーのこの処理を行う」
みたいなルールのことをルーティング
と言います。ここでは詳細の説明は省きます。
また「コントローラーやアクション」などの詳しい説明に関しても、今回は省かせていただきます。
RailsにおけるMVCの理解は最低限できているものとして本記事は進ませていただきますので、ご注意ください。3. 2で指定されたコントローラーへツイート内容(パラメーター)が渡される
最後に、2で指定されたコントローラーへ
リクエストに含まれるパラメーター
が渡されますただこの時、パラメーターには様々な情報があるため、そのまま渡されると非常にわかりづらくなってしまいます。
そこで、パラメータはparamsと呼ばれるものに一度格納されてからコントローラーへ渡されます。
※ params
paramsとはハッシュのような構造を取るパラメーターを格納するためのものです。
実際には以下のような形をとっています。
しかし、これだと少しわかりづらい方もいらっしゃると思うので、
「paramsはパラメータを格納するための箱のようなもの」
と考えると、イメージしやすいかもしれません。以下のようなイメージです。
フォームに入力された値は、リクエストの中に含まれるパラメータとしてリクエストと共にサーバー側のルーターに送信され、そこでparamsに格納されてサーバー側のコントローラーへ送信される
ということになります。
③サーバーに送られた投稿内容が投稿一覧表示画面に反映される
次にサーバーに送られた投稿内容は、コントローラーによって処理が行われます。
今回だと、投稿一覧画面に投稿内容を反映させるために、送られてきた投稿内容を保存するという処理が行われます。
具体的には
tweets_controller.rbdef create Tweet.create(tweet_params) endのような処理が行われ、投稿一覧画面に反映されます。
Twitterの投稿機能の場合は、このように処理が行われます。
ではこの時、
form_withはどのような役割を果たしていたのか?考えてみましょう。
form_withは、上の一連の処理の中では①で登場しました。
つまり、サーバーに投稿内容を送信するという役割を果たしていることがここでわかるかと思います。
3. form_withの具体的な記述法
form_withの表記の仕方は大きく2つに分かれます。
①基本形
基本形<%= form_with url: "パス" do |form| %> フォーム内容 <% end %>urlにはリクエスト先のパス(URLと考えてもらって大丈夫です)が入ります。
そして、この基本形の場合はオプションをつけることが可能です。
※オプションについて
オプションとは
メソッドに補足的に付けられるもので、メソッドの情報を補足する働きがあります。
基本形の場合は以下のようなオプションをつけることが可能です。
methodオプション
form_withで送信されるHTTPリクエストのデフォルトはPOSTであり、methodオプションはそのHTTPリクエストを変更したい場合に用います
localオプション
form_withから送られたリクエストはデフォルトではHTTP通信が行われていないため、それをHTTP通信にする場合localオプションを用います
オプションを付けた場合<%= form_with url: "/posts", method: :post, local: true do |form| %> フォーム内容 <% end %>②応用形
①のようにurlにリクエスト先を指定したり、methodオプションでHTTPメソッドを指定したりするのが面倒な場合はそれらを省略して、以下のように記述することができます
応用形<%= form_with model: モデルクラスのインスタンス do |form| %> フォーム内容 <% end %>そして、この応用形には他にも様々なメリットがあるためform_withを用いる場合は
こちらがよく使われる傾向にあります。
4. まとめ
- form_withはヘルパーメソッドの一種で、ビューファイル上でフォーム入力欄を作成できるメソッドのことを言います。
- form_withメソッドはユーザーからサーバー側に情報を送信するという役割を果たしている
- form_withの表記は大きく分けると、urlオプションとmomdelオプションを使う場合の2種類が存在する
5. 最後に
本記事の内容がみなさんの参考になれば嬉しいです。
最後までご覧いただきありがとうございました。
6. 参照サイト
https://qiita.com/snskOgata/items/44d32a06045e6a52d11c
https://qiita.com/tomoharutt/items/46e6358acc8b45cd7db1
https://qiita.com/hayulu/items/5bf26656d7433d406ede
- 投稿日:2020-11-23T16:18:59+09:00
【Rails6】Active Job + Sidekiqを動かしてみた
Railsの非同期処理をActive Job + Sidekiqで実装したのでメモを残します。
※RailsアプリケーションはDocker環境で構築済みの前提です。環境構築はこちら。
※Active Jobとバックエンドの比較はこちら。環境
- Ruby 2.7.2
- Rails 6.0.3.4
- MySQL 8.0.20
- Redis 6.0.9
- Sidekiq 6.1.2
- Docker version 19.03.13
1. Redisの導入
まずredisコンテナを用意します。
ポート番号はdocker-compose.override.yml
で指定していますが、下記で設定して問題ないと思います。docker-compose.ymlversion: '3.7' services: db: image: mysql:8.0.20 volumes: - mysql:/var/lib/mysql:delegated command: --default-authentication-plugin=mysql_native_password env_file: .env web: build: context: . dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" tty: true stdin_open: true env_file: .env depends_on: - db - chrome - redis volumes: - .:/app:cached - bundle:/usr/local/bundle:delegated - node_modules:/app/node_modules chrome: image: selenium/standalone-chrome:3.141.59 volumes: - /dev/shm:/dev/shm redis: image: redis:6.0.9 env_file: .env command: redis-server --appendonly yes volumes: - redis:/data volumes: mysql: bundle: node_modules: redis:docker-compose.override.ymlversion: '3.7' services: db: ports: - 3306:3306 web: ports: - 3000:3000 chrome: ports: - 4444:4444 redis: ports: - 6379:6379Redisの設定ファイルを追加します。
host名に注意してください。config/redis.ymldefault: &default db: sidekiq: 0 # cache: 1 # session: 2 development: <<: *default host: redis test: <<: *default host: redis2. Sidekiqの設定
まずはGemを追加します。
ruby:Gemfile
gem 'sidekiq'
bundle install
を実行し、設定ファイルを追加します。
詳しくは、こちらを参照してください。config/sidekiq.yml:verbose: false :max_retries: 1 :concurrency: 10 :pidfile: ./tmp/pids/sidekiq.pid :logfile: ./log/sidekiq.log :queues: - development_default続いてSidekiqとRedisの接続情報も追加します。
config/initializers/sidekiq.rbredis_config = YAML.load_file('config/redis.yml')[Rails.env] redis_config['db'] = redis_config['db']['sidekiq'] Sidekiq.configure_server do |config| config.redis = { url: "redis://#{redis_config['host']}/#{redis_config['db']}" } end Sidekiq.configure_client do |config| config.redis = { url: "redis://#{redis_config['host']}/#{redis_config['db']}" } endダッシュボードのルーティングも設定します。
config/routes.rbrequire 'sidekiq/web' Rails.application.routes.draw do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end3. Active Jobの設定
Active Jobで非同期処理を実装していきます。
config/application.rbrequire "active_job/railtie" module App class Application < Rails::Application config.active_job.queue_adapter = :sidekiq config.active_job.queue_name_prefix = Rails.env # これは任意 end end4. ジョブの作成と動作確認
ようやくジョブを作成します。
rails g job sampleapp/jobs/sample_job.rbclass SampleJob < ApplicationJob queue_as :default def perform puts '--------------------------------' puts '------------ Test ------------' puts '--------------------------------' end end続いて動作確認を行います。
dockerコンテナを起動し、RailsとRedisが動いていることを確認します。
コンテナ内でsidekiqを起動します。$ bundle exec sidekiq -C config/sidekiq.yml 2020-11-23T07:06:57.513Z pid=74 tid=9om INFO: Booting Sidekiq 6.1.2 with redis options {:url=>"redis://redis/0"} m, `$b .ss, $$: .,d$ `$$P,d$P' .,md$P"' ,$$$$$b/md$$$P^' .d$$$$$$/$$$P' $$^' `"/$$$' ____ _ _ _ _ $: ,$$: / ___|(_) __| | ___| | _(_) __ _ `b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` | $$: ___) | | (_| | __/ <| | (_| | $$ |____/|_|\__,_|\___|_|\_\_|\__, | .d$$ |_| 2020-11-23T07:06:57.938Z pid=74 tid=9om INFO: Booted Rails 6.0.3.4 application in development environment 2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: Running in ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux] 2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: See LICENSE and the LGPL-3.0 for licensing details. 2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org 2020-11-23T07:06:57.946Z pid=74 tid=9om INFO: Starting processing, hit Ctrl-C to stop別タブのターミナルでRailsコンソールから
SampleJob
をキューイングしてみます。$ rails c Loading development environment (Rails 6.0.3.4) > SampleJob.set(wait: 5.second).perform_later Enqueued SampleJob (Job ID: fdcf5c60-3542-4b26-bfd2-9662ffafada9) to Sidekiq(development_default) at 2020-11-23 07:08:49 UTC => #<SampleJob:0x00005567e535e4c0 @arguments=[], @exception_executions={}, @executions=0, @job_id="fdcf5c60-3542-4b26-bfd2-9662ffafada9", @priority=nil, @provider_job_id="7c42f602bd499e75efecad26", @queue_name="development_default", @scheduled_at=1606115329.3860393> >キューイングすると先ほど起動したSidekiq側でジョブが実行されたことが確認できました。
2020-11-23T07:08:52.800Z pid=74 tid=b6y class=SampleJob jid=7c42f602bd499e75efecad26 INFO: start -------------------------------- ------------ Test ------------ -------------------------------- 2020-11-23T07:08:53.067Z pid=74 tid=b6y class=SampleJob jid=7c42f602bd499e75efecad26 elapsed=0.267 INFO: done
- 投稿日:2020-11-23T16:08:09+09:00
【自分メモ】モデル/コントローラー/ビューの役割
モデル…コントローラーで定義したインスタンス変数にデータを入れるため、コントローラーからモデルへデータを取ってくるよう指示が出る(モデル名.all)→指示を受けたモデルはデータベースからデータを取得後、コントローラーのインスタンス変数へ入れる。
またコントローラーから指示を受けて新しいオブジェクトを作成する(モデル名.new)。コントローラー…インスタンス変数に入れるデータをモデルに持ってくるよう指示を出したり、そのインスタンス変数をビューに渡したりする。
ビュー…コントローラーから受け取ったインスタンス変数を元にhtmlファイルを作成する。
※自分の考えをメモとして残していますので、間違っていたり修正した方が良い点がございましたら、ドシドシつっこみいただけると嬉しいです!
- 投稿日:2020-11-23T14:59:57+09:00
【自分メモ】ストロングパラメータ
外部からの不正なアクセスを受け付けないようにするものです。
例えば、新規投稿したデータをデータベースに保存する際、新規投稿ではない別のデータを保存するように書き換える人がいました。そうすることで、本来なら公開されないはずの情報を操作されてしまい、不正に情報が流出してしまうきっかけになります。
そのような不正なアクセスを受けなくするため、ストロングパラメータにて「カラムを登録したモデル(オブジェクト)しかデータは持って来れませんよ〜」と記入し、加えてprivate下に置くことで特定のコントローラーでしか呼び出せないようにしました。(また、private下に定義することでストロングパラメータはアクションとして認識されなくなります)また、ストロングパラメータはマスアサインメント脆弱性というセキュリティ上の問題を解決するために設定するものになります。
※自分の考えをメモとして残していますので、間違っていたり修正した方が良い点がございましたら、ドシドシつっこみいただけると嬉しいです!
- 投稿日:2020-11-23T14:43:33+09:00
Rails + Docker でhello worldを表示するまでを簡単に
アプリ作成時に毎回調べている気がするので、備忘録的にまとめてみました。
下記のapp名
の箇所には適宜アプリ名を入力して下さい。※間違いがありましたら変更しますのでコメント頂けると嬉しいです^^
新規ディレクトリ作成 〜 hello world!!まで
まず、アプリの土台となるディレクトリを作ります。
さらに、touchコマンドで、2つの空ファイルを作成します。$ mkdir app名 && cd app名 $ touch Gemfile Gemfile.lock私は、VScodeを使用して開発しているので
code コマンドを使用して起動しています。
ちなみに、code コマンドは起動と作成をしてくれます。Gemfileを編集します。
$ code GemfileGemfilesource 'https://rubygems.org' gem 'rails', '~>5.2'Dockerfileを作成して編集します。
$ code DockerfileDockerfileFROM ruby:2.5 RUN apt-get update RUN apt-get install -y \ build-essential \ libpq-dev \ nodejs \ postgresql-client \ yarn \ vim WORKDIR /app名 COPY Gemfile Gemfile.lock /app名/ RUN bundle installdocker-compose.ymlファイルを作成して編集します。
$ code docker-compose.ymldocker-compose.ymlversion: "3" volumes: db-data: services: web: build: . ports: - "3000:3000" volumes: - ".:/app名" environment: - "DATABASE_PASSWORD=postgres" tty: true stdin_open: true depends_on: - db links: - db db: image: postgres volumes: - "db-data:/var/lib/postgresql/data" environment: - "POSTGRES_HOST_AUTH_METHOD=trust" - "POSTGRES_USER=postgres" - "POSTGRES_PASSWORD=postgres"コンテナの起動を行い、webコンテナに入って、rails new します。
$ docker-compose up --build -d $ docker-compose exec web bash $ rails new . --force --database=postgresqlrails new で作成された database.yml ファイルに追記します。
database.ymldefault: &default adapter: postgresql encoding: unicode host: db #追記 user: postgres #追記 port: 5432 #追記 password: <%= ENV.fetch("DATABASE_PASSWORD") %> #追記 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>$ rails db:migrate $ rails s -b 0.0.0.0Chromeの検索バーに、localhost:3000 と入力してアクセスするとhello worldが表示されているかと思います!
- 投稿日:2020-11-23T14:34:22+09:00
【自分用】マイグレーション関連【ロールバック、カラムの追加、型の変更】
完全に自分用の書き殴りです。
誤り等ございましたら、コメント下さい!ロールバック
rails db:migrate:down VERSION=20190611235049
VERSION
で指定した箇所まで戻れるrails db:rollback STEP=3
- 現在の migration ファイルから遡って、
STEP
で指定した回数戻れるカラムの追加
rails g migration Addカラム名Toテーブル名 @@@:integer @@@:string
rails db:migrate
- Add 以下はキャメルケースで記載
- カラム名のところは何を書いてもいいが、追加したいカラム名の複数形に統一しておく
- テーブル名にはカラムを追加するテーブル名の複数形を書く
@@@
はカラム名を単数形で記入以下は user テーブルにふりがなを追加したときの例。
class AddKanaToUser < ActiveRecord::Migration[5.2] def change add_column :users, :kana, :string end endカラム型の変更
rails g migration change_data_<カラム名>_to_<テーブル名の複数形>
class ChangeColumnToUsers < ActiveRecord::Migration[5.2] def change end end上記のように空の migration ファイルが作成されるので、下記のように編集
class ChangeColumnToUser < ActiveRecord::Migration[5.2] def up change_column :users, :detail, :text end def down change_column :users, :detail, :string end end注意点
カラムの追加とカラム型の変更では、方法がやや異なっていて、migration ファイルを触る時は、追加(up)するだけでなく、削除(down)するときのことも考えなければならない。
つまり、カラムの追加では、change で書いても rollback 時など down する際にもよしなにしてくれるが、カラム型の変更で change を使ってしまうと、up 時は良くても、down 時に rollback できないというようなことが起こってしまう。ということ。
簡単なことだが、ここを理解したときに、migration ファイルに対する怖さが一気に消えた。おまけ
これからは上記の様に書くとして、これまでに書いてしまった箇所を rollback するさいはどうすればいいのか...
これは簡単で、今ある migration ファイルの change と記載している箇所を、up に上書き変更して、その下に down 時の動作を追記すればよい。down 時の動作は変更前のカラム型のこと。
なんとなく、migration ファイルを上書きするのは怖かったが、これで問題なく動作した。
- 投稿日:2020-11-23T13:59:25+09:00
Docker×Rails6(メモ)
事前準備
環境構築
Docker 関連ファイルを用意
FROM ruby:2.6.3-alpine ENV LANG=ja_JP.UTF-8 ENV TZ=Asia/Tokyo ENV ROOT=/myapp \ GEM_HOME=/bundle \ BUNDLE_PATH=$GEM_HOME ENV BUNDLE_BIN=$BUNDLE_PATH/bin ENV PATH /app/bin:$BUNDLE_BIN:$PATH WORKDIR $ROOT RUN apk update && \ apk upgrade && \ apk add --no-cache \ gcc \ g++ \ libc-dev \ libxml2-dev \ linux-headers \ make \ nodejs \ postgresql \ postgresql-dev \ tzdata \ imagemagick \ yarn && \ apk add --virtual build-packs --no-cache \ build-base \ curl-dev COPY Gemfile $ROOT COPY Gemfile.lock $ROOT RUN bundle install -j4 # 不要ファイル削除 RUN rm -rf /usr/local/bundle/cache/* /usr/local/share/.cache/* /var/cache/* /tmp/* && \ apk del build-packs COPY . $ROOT # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["sh", "/usr/bin/entrypoint.sh"] EXPOSE 3000docker-compose.ymlversion: "3.8" services: db: image: postgres:11.0-alpine volumes: - postgres:/var/lib/postgresql/data:cached ports: - "5432:5432" environment: PGDATA: /var/lib/postgresql/data/pgdata POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8" TZ: Asia/Tokyo app: build: . command: ash -c "rm -f tmp/pids/server.pid && ./bin/rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp:cached - rails_cache:/myapp/tmp/cache - node_modules:/myapp/node_modules:cached - bundle:/bundle:cached tmpfs: - /tmp tty: true stdin_open: true ports: - "3000:3000" environment: RAILS_ENV: development NODE_ENV: development DATABASE_HOST: db DATABASE_PORT: 5432 DATABASE_USER: postgres DATABASE_PASSWORD: password WEBPACKER_DEV_SERVER_HOST: webpacker depends_on: - db - webpacker webpacker: build: . command: ./bin/webpack-dev-server volumes: - .:/myapp:cached - node_modules:/myapp/node_modules:cached environment: RAILS_ENV: development NODE_ENV: development WEBPACKER_DEV_SERVER_HOST: 0.0.0.0 tty: false stdin_open: false ports: - "3035:3035" volumes: rails_cache: node_modules: postgres: bundle:source 'https://rubygems.org' gem 'rails', '6.0.3' gem 'devise' # to upload images gem 'carrierwave', '~> 2.0' gem "mini_magick"Gemfile.lock(empry)```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 "$@"cmd
docker-compose run app rails new . --force --no-deps --database=postgresql --skip-bundle
Gemfile を編集する
source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.6.3' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6.0.3', '>= 6.0.3.4' # Use postgresql as the database for Active Record gem 'pg', '>= 0.18', '< 2.0' # Use Puma as the app server gem 'puma', '~> 4.1' # Use SCSS for stylesheets gem 'sass-rails', '>= 6' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem 'webpacker', '~> 4.0' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.7' gem 'devise' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use Active Model has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use Active Storage variant # gem 'image_processing', '~> 1.2' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.2', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '~> 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end 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 # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] # to upload images gem 'carrierwave', '~> 2.0' gem "mini_magick"その後のコマンド
docker-compose run app bundle update docker-compose run app rails webpacker:installdatabase.yml を修正
config/database.ymldefault: &default adapter: postgresql encoding: unicode host: db username: postgres password: password pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> ...Troubleshooting
up 時に check_yarn_integrity関連 というエラーが出たら webpacker.yml を修正
config/webpacker.yml... development: <<: *default compile: true # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules check_yarn_integrity: false # true -> falseに変更 ...docker-compose build docker-compose up -d docker-compose run app rake db:createまた up 時に "webpack-dev-server" not found が出たら
docker-compose run app yarn add webpack-dev-server
- 投稿日:2020-11-23T13:21:57+09:00
Rails Tutorial 拡張機能のメッセージ機能を作ってみた(その3):DMの作成
Rails Tutorialの第14章にある、メッセージ機能を作る件の続きです。
前回までで表示する画面ができました。作成する画面を作ります。
DMを作成
DMを作成する機能を作ります。micropostを作成するところを参考にします。tutorialのリスト 13.36: を参考にコントローラーにcreateアクションを作ります。
app/controllers/dms_controller.rbclass DmsController < ApplicationController before_action :logged_in_user, only: [:index, :create, :destroy] def index @user = current_user @dms = @user.sent_dms.paginate(page: params[:page]) end def create @dm = current_user.sent_dms.build(dm_params) if @dm.save flash[:success] = "DM sent!" else render 'dms/index' end end private def dm_params params.require(:dm).permit(:content,:receiver) end endapp/views/dms/index.html.erb<section class="micropost_form"> <%= form_for(@dm) do |f| %> <%= render 'shared/error_messages', object: f.object %> <div class="field"> <%= f.label :receiver_name %> <%= f.text_field :receiver_name, class: 'form-control' %> <%= f.text_area :content, placeholder: "new DM..." %> </div> <%= f.submit "Send", class: "btn btn-primary" %> <% end %> </section>rails serverで画面を表示してみます。
エラーになりました。Showing /home/ubuntu/environment/sample_app/app/views/dms/index.html.erb where line #13 raised: First argument in form cannot contain nil or be empty@dmがnilなのでエラーになったと考えました。リスト 13.40を参考に修正します。
app/controllers/dms_controller.rb<%= form_for(@dm) do |f| %> def index @user = current_user @dms = @user.sent_dms.paginate(page: params[:page]) @dm = current_user.sent_dms.build if logged_in?別のエラーになりました。
ActionView::Template::Error (undefined method `receiver_name' for #<Dm:0x00007ff01420e4a8> Did you mean? receiver_idformについて理解が足りないようで、読み直します。
7.2.1でform_forの引数はActive Recordのオブジェクトを取り込むとあります。
一方で、8.1.2ではform_forにActive RecordにはないSessionを扱っています。
リスト8.7で検索をしているところを参考に使えそうです。
form_forの引数にハッシュを使っていることが分かりました。
form_forを修正します。
修正前: <%= form_for(@dm) do |f| %>
修正後: <%= form_for(:dm) do |f| %>app/views/dms/index.html.erb<section class="micropost_form"> <%= form_for(:dm, url:new_dm_path) do |f| %>同じエラーです。
ActionView::Template::Error (undefined method `receiver_name' for #<Dm:0x00007f962c6ab110> <%= f.text_field :receiver_name, class: 'form-control' %>メソッドがないというので、モデルにある列名ならよいのかと考え、試しに変えてみます。
変更前:receiver_name
変更後:receiver_id<%= f.text_field :receiver_id, class: 'form-control' %>画面が表示されました。生成されたページのソースを見てみます。
<form action="/dms/new" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓" /><input type="hidden" name="authenticity_token" value="rSSWCwPnDGlcWrtuhvyk4BZdbOzImu6+XGx9ZVGKqJcUdwsYZdqCxsBOVAPsSHYj0L6plKtBiqAigicYUtCyYA==" /> <div class="field"> <label for="dm_receiver_name">Receiver name</label> <input class="form-control" type="text" name="dm[receiver_id]" id="dm_receiver_id" /> <textarea placeholder="new DM..." name="dm[content]" id="dm_content">form_forの引数がActiveRecordと関連していて、列名かどうかをチェックする動きをしています。
試しにdmをdm1と変えてみたところ、列名でなくてもエラーは起きなくなりました。app/views/dms/index.html.erb<%= form_for(:dm1, url:new_dm_path) do |f| %> <%#= render 'shared/error_messages', object: f.object %> <div class="field"> <%= f.label :receiver_name %> <%= f.text_field :receiver_name, class: 'form-control' %> <%= f.text_area :content, placeholder: "new DM..." %> </div> <%= f.submit "Send", class: "btn btn-primary" %> <% end %>試しにPOSTを画面で実行
receiver_nameをキーとしてユーザーを見つけ、DMをデータベースに追加します。
リスト12.5を参考にします。
Sendボタンをクリックします。エラーが起きました。
No route matches [POST] "/dms/new"ページのソースを調べると
<form action="/dms/new"となっています。
urlに指定するのはPOSTのpathだと分かりましたので、修正します。<%= form_for(:dm1, url:dms_path) do |f| %>別のエラーが起きました。
Param is missing or the value is empty: dm params.require(:dm).permit(:content,:receiver) app/controllers/dms_controller.rb:28:in `dm_params'パラメーターを調べます。
{"utf8"=>"✓", "authenticity_token"=>"QnULF0hViVk9ty/LggtG1fcwzTYNvfp8HMJ6nvq2Z/H9qG/1nSzPgR1+97W7osFg250OdLAO1q8vplnjjIbvNQ==", "dm1"=>{"receiver_name"=>"gege", "content"=>"agege"}, "commit"=>"Send"}dmをdm1に変更する必要がありそうなので、修正します。
変更前:params.require(:dm).permit(:content,:receiver)
変更後:params.require(:dm1).permit(:content,:receiver)別のエラーが起きました。
'nil' is not an ActiveModel-compatible object. It must implement :to_partial_path. <%= render @dms %>@dmsがnilとは?、@dmsを設定していないのでエラーになったと考えます。
createでリストを表示するところに、indexと同じ行をコピーします。重複感があるのですが、まとめるのは後廻しにします。その結果、メッセージが正常に表示されました。
受信者がいないケースのテスト
テストを作ります。
リスト8.9を参考にします。test/integration/dms_test.rbtest "send dm with invalid receiver" do log_in_as(@user) get dms_path post dms_path, params: { dm1: {receiver_name: "" }} assert_template 'dms/index' assert_not flash.empty? endリスト13.36を参考に、DMを送るアクションを作ります。
app/controllers/dms_controller.rbdef create @user = current_user @dms = @user.sent_dms.paginate(page: params[:page]) # receiverを探す @receiver = User.find_by(name: params[:dm1][:receiver_name]) if @receiver @dm = @user.sent_dms.build @dm.receiver_id = @receiver.id @dm.content = params[:dm1][:content] @dm.save flash[:success] = "DM sent!" redirect_to dms_path else flash.now[:danger] = 'Receiver not found' render 'index' endcontentがブランクのテスト
contentをブランクにしてDMを送ってみます。
画面にはエラーが表示されません。コンソールのログを調べます。(0.1ms) rollback transaction Redirected to https://7eca7b943b584a2296afeb1d7ceb9db2.vfs.cloud9.us-east-2.amazonaws.com/dms Completed 302 Found in 10ms (ActiveRecord: 0.7ms)NoMethodError in Dms#index undefined method `errors' for nil:NilClass <% if object.errors.any? %>8章と12章をもう一度読みます。
receiverがnot foundのときを再テストしてみます。
エラーが表示されました。@dmをbuildしていないので、nilになっているのではと考え、buildしている行をreceiverを探す判定の前に移しました。
app/controllers/dms_controller.rbdef create @user = current_user @dms = @user.sent_dms.paginate(page: params[:page]) @dm = @user.sent_dms.build # receiverを探す @receiver = User.find_by(name: params[:dm1][:receiver_name]) if @receiver @dm.receiver_id = @receiver.id @dm.content = params[:dm1][:content] if @dm.save flash[:success] = "DM sent!" redirect_to dms_path else render 'index' end else flash.now[:danger] = 'Receiver not found' render 'index' end endnot foundのメッセージが表示されるようになりました。
無効・有効な送信のテスト
無効・有効な送信のテストを作ります。
リスト 13.55を参考にします。test/integration/dms_test.rbtest "DM interface" do log_in_as(@user) get dms_path # 無効な送信 assert_no_difference 'Dm.count' do post dms_path, params: { dm1: {receiver_name: @receiver.name, content: "" } } assert_select 'div#error_explanation' end # 有効な送信 content = "Dm test content1" assert_difference 'Dm.count', 1 do post dms_path, params: { dm1: {receiver_name: @receiver.name, content: content } } end assert_redirected_to dms_path follow_redirect! assert_match content, response.body end自分が受信者のDMを表示
自分が受信者のDMも表示するように変更します。
「13.3.3 フィードの原型」を読みます。Micropostとfeedの関係のように、DMに対してchatとchat_itemsを作ることにします。
app/models/user.rb# 試作 chat def chat Dm.where("sender_id = ?", id) endapp/controllers/dms_controller.rbdef index @user = current_user @dms = @user.sent_dms.paginate(page: params[:page]) @dm = current_user.sent_dms.build @chat_items = @user.chat.paginate(page: params[:page]) endapp/views/dms/index.html.erb<% if @user.sent_dms.any? %> <h3>DMs (<%= @user.sent_dms.count %>)</h3> <ol class= "microposts"> <%= render @chat_items %> <%#= render @dms %> </ol> <%= will_paginate @chat_items %> <%#= will_paginate @dms %> <% end %>receiverをブランクにして送信してみたところエラーになりました。
undefined method `any?' for nil:NilClass <% if @chat_items.any? %>chat_itemsを作る行をindexと同じようにcreateにも入れます。
app/controllers/dms_controller.rbdef index @user = current_user @dm = @user.sent_dms.build @chat_items = @user.chat.paginate(page: params[:page]) end def create @user = current_user @dm = @user.sent_dms.build @chat_items = @user.chat.paginate(page: params[:page]) # receiverを探す @receiver = User.find_by(name: params[:dm1][:receiver_name]) if @receiver @dm.receiver_id = @receiver.id @dm.content = params[:dm1][:content] if @dm.save flash[:success] = "DM sent!" redirect_to dms_path else render 'index' end else flash.now[:danger] = 'Receiver not found' render 'index' endテストしてすべてGreenです。
所要時間
11/15から11/22までの6.5時間です。
- 投稿日:2020-11-23T13:19:31+09:00
【Docker環境】ActiveSupport::MessageEncryptor::InvalidMessage の対処法
ポートフォリオをAWSにデプロイした後、ローカルで開発中にタイトルのエラーが出て解決に時間がかかったので、対処法を備忘録として投稿します。
credential.yml.encの再作成
master.keyを使ってcredential.yml.encを開く仕様になっているが、開けないためエラーを吐いている状態。そのため、credential.yml.encを作り直す必要がある。
$ EDITOR=vim bin/rails credetials:editこのコマンドは暗号化されたファイルをmaster.keyで複合して指定のエディタで編集し、その結果を再び暗号化して保存する。また、master.keyがなければ新しく作り、credentials.yml.encがなければ新しく作ってくれる。そのため、既存のcredential.yml.encを削除し、上記コマンドで解決する。
しかし、筆者のローカル環境はDockerコンテナ上にあるので、少々特殊なコマンドが必要。
まずは、サーバーにログインしvimをインストール。EC2サーバー上$ apt-get install -y vimローカル環境$ docker-compose run -e EDITOR=vim web rails credentials:edit Starting live_share_db_1 ... done File encrypted and saved.これで、エラーを吐かなくなりました^^
参考
https://qiita.com/at-946/items/8630ddd411d1e6a651c6
https://qiita.com/zenfumi/items/4a7cbab59f0f7ede0d6e
- 投稿日:2020-11-23T12:43:47+09:00
Vue(Nuxt)+Rails APIで、ネストした配列で画像とJSONのパラメーターを送る方法
なにこれ
Vue(Nuxt)で、子テーブルへ画像データとJSONを配列にしてパラメーターで送りたい!って時にかなりつまづいたので、備忘録として書き残しておきます。
この記事で得られること
1.Vue(Nuxt)で画像+JSONの形式でPOSTする方法
2.ネストさせた配列で、画像とJSON形式のパラメーターを送る方法大事なポイント
1.FormData型の変数を宣言して、その変数にappendしていく。
test.jsconst req = new FormData() req.append(`name`, this.product.name)2.ネストした配列で送りたい時はこうする。配列を明示してその中にオブジェクトを生成&appendする。
test.jsreq.append(`product_sub_attributes[][image]`, subs[i].image)3.axiosでリクエストを投げる時、
'Content-Type': 'multipart/form-data'
にすること。test.jsconst response = await axios.post('/products', req, { headers: { 'Content-Type': 'multipart/form-data' } })備考
- バックエンドはRails APIモードを仕様しています
- 画像アップ方法は、フロントはvue-cropeer、バックエンドはcarrierwaveを使用していますが、まだ理解しきれてない箇所があるのでそちらも後日記事にします。 なので、vue-cropperとcarrierwaveの説明は割愛します。今はJSON配列で送れるんだなーと知っていただければ幸いです。
- バックエンドはみんな大好き
gem 'active_model_serializers’
を使用しています。- 一部vuetify、bootstrap、axiosを使っています。
注意事項
今回はタイトルの方法を紹介するのがメインになるので、かなり端折っています。
全部解説すると膨大な量になってしまうので。
雰囲気を掴んでいただければ、と思っているのでご了承ください。
既存のコードをQiita用に抽出して載せているので、一部間違いがあるかも知れません。前提条件
テーブル構造はこんな感じです。
productsテーブル
とproducts_subs
テーブルがあって、1:Nの関係です。
両方とも画像投稿用のimageカラムを持っています。
登場する全てのカラムはstring型です。product.rbclass Product < ApplicationRecord has_many :products_subs accepts_nested_attributes_for :products_subs endproduct_sub.rbclass ProductSub < ApplicationRecord mount_uploader :image, ProductSubUploader belongs_to :product validates :name, presence: true validates :image, presence: true endやりたいこと
productsテーブルへcreateしたい時に、子テーブルのproducts_subsへ同時にデータを保存させたい。
そのためにはネストして送る必要がある。早速解説します。
画像アップのおおまかな流れ
プレビュー表示用のimgタグと画像アップ用のinputタグを用意して、
imgタグをクリックすると$refsでinputを参照してクリックしたことにします。配列なので$refs.subImageに[i]をつけて、番号を参照しています。
inputタグはクリックされるとsetSubImageメソッドを呼び出します。setSubImageで画像を読み込んだ後にvue-cropperに投げて、
らcropSubImageメソッドでsubImageCropperを参照してプレビューを表示する変数に突っ込んだり、画像格納用のオブジェクトに代入したりしてます。
余談だけど、このvue-cropperさんの動きが理解できてなくて、たまに$refsで参照できないバグが生まれます。笑test.vue<template v-for="(sub, i) in product.productsSubAttributes"> <div> <v-textarea v-model="sub.name" > </v-textarea> <img :src="sub.imageSrc ? sub.ImageSrc : '' " @click.prevent="$refs.subImage[i].click()" > <input ref="subImage" class="d-none" type="file" name="image" accept="image/*" @change="setSubImage($event)" /> <v-card> <vue-cropper ref=“subImageCropper" :src="imgSrc" /> <button @click="cropSubImage(sub), i)"> 保存 <button> </v-card> </div> </template> <script> export default { methods: { setSubImage(e) { const reader = new FileReader() reader.onload = (e) => { this.imgSrc = e.target.result } reader.readAsDataURL(e.target.files[0]) }, async cropSubImage(sub, i) { // imageSrcがプレビュー表示用のプロパティです。 sub.imageSrc = this.$refs.subImageCropper[0].getCroppedCanvas().toDataURL() this.$refs.subImageCropper[0].getCroppedCanvas().toBlob((blob) => { sub.image = blob }) } } } } </script>本題の画像とJSONオブジェクトをパラメーターで送る方法
今回はaxiosを使います。
その際に、configを設定してmultipart/form-data
という形式に変換します。
【axios】HTTPリクエストメソッド別の引数一覧表(エイリアスを使用した場合)
この記事がよくまとまってたのでリンク貼っておきます。multipart/form-dataとは、画像ファイル(Blob型)を送信できるようにするHTTPリクエストメソッドです。
application/jsonだと画像データが送れないです。パラメーターで送るための方法
細かく解説するためにコメント式にしました。
test.jsasync onClickCreate() { // FormData型の変数reqを定義して、そちらにappendしていきます。 const req = new FormData() // 今回は使用しませんが、こんな感じでバックエンドのカラム名に合わせて // オブジェクトを生成してappendすることで、パラメーターを送れます。 req.append('name', this.product.name) // 定数subにアップした画像や文字列のデータを代入して、for文で回します。 // for文を使っている理由は複数ありますが、今回は省略。状況によってはforEachでも代用可能です。 const subs = this.product.productSubAttributes for(let i = 0; i < subs.length; i++){ // 送りたいパラメーターに[]をつけることで、0から順番に配列で送ることが出来ます。 // ただ、配列にインデックスを指定する方法が分からないです。 req.append(`product_sub_attributes[][name]`, subs[i].name) // subs[i].imageがBlob型である=先程vue-cropeerで整形したデータなので、直接カラムに代入します。 if (subs[i].image instanceof Blob) { req.append(`product_sub_attributes[][image]`, subs[i].image) // 画像がない場合、番号を指定できないので、空文字を代入しないと順番が狂います。 // どういうことかと言うと、配列内のオブジェクトが2個以上あった場合、0番目から探して空いているカラムに代入されます。 // ちなみにここでかなりハマりました。 } else { req.append(`product_sub_attributes[][image]`, '') } } try { // 先程代入していった変数reqをパラメーターで送ります。 // 余談ですが、Railsの場合全てキー名をスネークケースにする必要があります。 const response = await axios.post('/products', req, { headers: { 'Content-Type': 'multipart/form-data' } }) } catch (error) { console.error(error.response) } }ちなみにバックエンドはこんな感じ
products_controller.rbdef product_params params.permit( :id, :name, { product_sub_attributes: [ :id, :name, :image, ] } ) endおわり
こんな感じでVue(Nuxt)からパラメーターで画像を配列+JSONで送ることが出来ます。
自分が実装しようとした時にこの方法にたどり着くまで苦労したので、
個人的な感想
やり方(How)を覚えるのは良いことなんですけど、
裏側のなぜこの方法なのか(How)の方を理解していきたいです。
- 投稿日:2020-11-23T11:48:00+09:00
プルダウンを使おうとするとエラーになる件
ActiveRecord::StatementInvalid
他のf.text_areaは使えるのに、、テーブルがないよと言ってますね。
いちよマイグレの確認upにされてるし、カラムも間違えてないのに。。。
datebase.ymlいじっても変化なし。ActiveHash::Base
ActiveHash::Baseは、あるモデル内(クラス内)でActiveHashを用いる際に必要となるクラスです。ActiveHashのGemに定義されています。
また、ActiveHash::Baseを継承することで、ActiveRecordと同じようなメソッドを使用できるmodel/category.rbclass Category < ApplicationRecord include ActiveHash::Associations has_many :events end継承をすっかり忘れていました。⬇️
model/category.rbclass Category < ActiveHash::Base include ActiveHash::Associations has_many :events end
- 投稿日:2020-11-23T09:56:37+09:00
ルーティングにおける、getとpostの使い分け (Ruby on Rails)
(学習備忘録)
Ruby on Railsのルーティングにおける、getとpostの使い分け
「get」
データベースを変更しないアクション「post」
データベースを変更するアクション
- 投稿日:2020-11-23T09:04:11+09:00
スクール4週目 学習環境の大切さ
スクール4週目では、問題をときながら一つずつ実装していくChatAppを完了することができました。
学習環境の大切さ
プログラミング学習時に皆さんは音楽を聴きますか?集中力を高める為や、気分を上げる為に聴く方は多いと思います。私は学習中、常にイヤホンで自然音を流しています。
自分はこれまで自然音を流しながら学習する習慣はなく無音で学習しては周りの雑音が気になり、集中が途切れることが多々ありました。ところが、自然音を流しながら学習をするとプログラミングに没頭できることを実感しています。
これは、一定のテンポで自然音を流すことでリラックス効果あるからだと思います。また、ポモドーロ・テクニックを活用して25分勉強して5分休憩するを繰り返すことで、集中力を維持できます。ぜひ、一度は実践をしてみてください。
応用カリキュラム
・ユーザー管理機能実装
・グループ作成編集機能実装
・メッセージ送信機能実装
・テスト
・非同期通信機能実装
・自動更新機能実装ユーザー管理機能実装
gemのdeviseを使用してのユーザー管理を実装①deviseのインストール
②Userモデルを作成する
③deviseのビューファイルの追加
④サインアップ機能の追加
⑤ユーザー情報編集機能の追加
⑥ログアウト機能の追加グループ作成編集機能実装
グループの新規登録画面で、グループ名と所属メンバーを入力して登録できる機能を実装。
①groupsコントローラーを作成(ルーティングの設定も合わせて実施)
②アイコンをクリックするとグループの新規登録画面へ移動するようリンクを設定
③groupモデルとgroup_userモデル(多対多のリレーションを組む為の中間モデル)を作成
④多対多のアソシエーションとバリデーションを設定
⑤binding.pryにより送信されるデータを確認
⑥配列の保存を許可するためのストロングパラメータの設定そして、グループの新規登録画面の実装とほぼ同じ手順でグループ編集機能を実装。
また、ビューを部分テンプレートを使用し整えます。メッセージ送信機能実装
ここらへんまでくるといよいよ完成形が見えてきて画像を投稿できた時は少し感動しました。この手順の中では⑧、⑨、⑩は全くのノーヒントで、力が試されます。基礎カリキュラムを見返したのと、検索でここは乗り切りました。
①Messageモデルを作成する
②ルーティングを設定する
③該当するアクションをコントローラに定義する
④画像送信の為のgem、Carrierwaveを導入
⑤メッセージ送信機能を実装する
⑥グループにメッセージを表示する
⑦サイドバーに最新のメッセージを表示する
⑧ヘッダーを修正しグループ名、メンバーが表示されるようにする
⑨グループ編集ページへのリンクを設置する
⑩グループ編集後のリダイレクト先を変更するテスト
Webアプリケーション作成のおいて重要なテストの概念、必要性、原則、書き方をRSpecを利用し学びました。まずは基礎的な書き方を学習、その後はfactory_botというgemを利用。そしてダミーのデータを作成するためのgem、Fakerの利用方法を学びました。
ひと通り学んだ後はChat-spaceで実施。factory_botでスッキリまとめられるかが課題です。
①単体テスト‥ひとつのプログラムのまとまりに関して、それ単体が正常に動くか確かめるテスト
②結合テスト‥複数のプログラムが連動して行われる処理が意図した通りに行われるかを確かめるテスト非同期通信機能実装
非同期通信機能とは、リクエスト後にレスポンスが帰ってきた際、ブラウザが再読み込みされること無く通信が行われる通信方法。Ajaxと呼ばれます。また、ここではJSONというデータ交換を行う為の記述形式を使用。
学習の流れとしては基礎カリキュラムで作成したアプリであるPictweetのコメント機能の非同期通信化を行いながら学び、その後Chat-spaceへ実装します。
Chat-space実装時にビューが崩れたのでclass名を確認し修正。ここらへんからうまくいかない時は検証ツールが大活躍です。
①APIを作成
②jQueryが使えるように設定し、jsファイルを作成する
③フォームが送信されたら、イベントが発火するようにする
④イベントが発火したときにAjaxを使用して、messages#createが動くようにする
⑤messages#createでメッセージを保存し、respond_toを使用してHTMLとJSONの場合で処理を分ける
⑥jbuilderを使用し、作成したメッセージをJSON形式で返す
⑦返ってきたJSONをdoneメソッドで受取り、HTMLを作成、それをメッセージ画面の一番下に追加する
⑧メッセージを送信したとき、メッセージ画面を最下部にスクロールするようにする
⑨連続で送信ボタンを押せるようにする
⑩非同期に失敗した場合の処理も準備する(エラー表示)自動更新機能実装
現行では自分でメッセージを投稿した場合は非同期通信機能により画面に表示されますが、別のユーザーの投稿メッセージはリロードしなければ表示されません。そこで、この自動更新機能を実装し自動で画面が更新されるようにします。
①表示されているメッセージのidが確認出来るようにhamlにカスタムデータ属性として追加
②新規メッセージ確認用のアクションをWebAPIにて実装
③投稿内容をレスポンスできるようにする
④取得した投稿データを表示できるようにする
⑤7秒毎にリクエストするよう実装
⑥メッセージを取得したら画面がスクロールするようにする
⑦自動更新が必要ない画面では行わないようにするそして無事LGTMを貰いとりあえず実装は完了。まだ、理解しきれていないところがあるので、余力と時間があればPicTweetとChatAppの2周目をしたいなと思います。
振り返り・感想
PicTweetとChatAppを作成して大事だと思ったことは、とりあえず何か作ってみることで覚えていくという点です。ひたすらコードを暗記していくよりもまずは一つ簡単でも良いのでアプリ作ることから始めた方が良いと思いました。
ポートフォリオもまさにそうで、とりあえず作って過程で追加実装をしていく考え方、手を動かすことの大事さを感じました。それでも、ユーザ目線で要件定義することも念頭に入れてポートフォリオのアイデアも言語化して行きたいと思います。
- 投稿日:2020-11-23T07:15:28+09:00
【Rails】2つの(複数)deviseを導入
はじめに
「ユーザーと業者」、「管理者とユーザー」、「先生と生徒」など、登録情報が2つ以上(複数)必要な場合、deviseも同様に2つ以上(複数)必要です。
2つの(複数)deviseの導入方法を紹介します。目次
- deviseのインストール
- モデルを作成
- ルーティングの設定
- コントローラーの作成
開発環境
ruby 2.6.5
rails 6.0.0
devise 4.7.3実装
それでは実装していきます〜
1. deviseのインストール
deviseのgemを導入します。
Gemfile.# 中略 gem 'devise'記述したら、bundle installを実行しましょう。
ターミナル.bundle installGemをインストールした後はrails sをcontrol + Cで一度停止し、サーバーを再起動する必要があります。
続いて、アプリケーション内でdeviseを使えるようにするため、下記のコマンドを実行します。
ターミナル.rails g devise:installターミナル.create config/initializers/devise.rb create config/locales/devise.en.yml .........#省略deviseがインストールされました。
次に以下の二行をコメントインして変更します。
config/initializers/devise.rb# ==> Scopes configuration # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you # are using only default views. config.scoped_views = true # ←複数のmodelで個別のログイン画面を使う。 これをtrueにします。 # Configure the default scope given to Warden. By default it's the first # devise role declared in your routes (usually :user). # config.default_scope = :user # Set this configuration to false if you want /users/sign_out to sign out # only the current scope. By default, Devise signs out all scopes. config.sign_out_all_scopes = false #複数のモデルを扱う際、いずれかがログアウトした時に全てログアウトする。 これをfalseにします。2. モデルを作成
deviseが2つなので当然2つのモデルを作成します。
rails g devise user rails g devise admin管理者アカウントになる admin に対して勝手に sign_up されたくないので Admin モデルを修正します。
:registerable
不要部分をコメントアウトします。app/models/admin.rbclass Admin < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, #:registerable, ←ここをコメントアウトします。 :recoverable, :rememberable, :validatable end3. ルーティングの設定
deviseを導入するとデフォルトでルーティングを設定してくれてますが、
rails routes
した時かぶって見にくいので再設定します。
deviseがずらっと並んでみにくい、、、、
下記に編集します。routes.rbRails.application.routes.draw do devise_for :admins devise_for :users # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html #ここまでが自動生成されてます。 #下記に編集します。 devise_for :admins, controllers: { sessions: 'admins/sessions', passwords: 'admins/passwords', registrations: 'admins/registrations' } devise_for :users, controllers: { sessions: 'users/sessions', passwords: 'users/passwords', registrations: 'users/registrations' } root to: 'home#index' endadminsとsuersに別れて見やすくなります。
4. コントローラーの作成
コントローラーも2つ作成します。
ターミナル.rails g devise:controllers users rails g devise:views adminsターミナル.rails g devise:views users rails g devise:controllers admins
localhost:3000/users/sign_in
とlocalhost:3000/admins/sign_in
のそれぞれページが表示されることを確認できたら完成です!!まとめ
2deviseの導入方法でした。
最後に
私はプログラミング初学者ですが、自分と同じ様にエンジニアを目指す方々の助けになればと思い、記事を投稿しております。
それではまた次回お会いしましょう〜参考
https://qiita.com/Yama-to/items/54ab4ce08e126ef7dade
https://ccbaxy.xyz/blog/2020/03/20/ruby33/
- 投稿日:2020-11-23T00:04:03+09:00
コントローラーのテスト時に画像をアップロードする方法
RSpecでコントローラーの単体テストを行っていた時にエラーが発生したため、解決方法を記録として残します。
開発環境
- rails (6.0.3.3)
- rspec-rails (4.0.1)
コード内容
店舗情報の登録に関するテストを行いました。
laundries.spec.rbcontext "管理者の場合" do before do @user = FactoryBot.create(:user, admin: true) end it "店舗情報を追加できること" do laundry_params = FactoryBot.attributes_for(:laundry) sign_in @user expect { post laundries_path, params: { laundry: laundry_params } }.to change { Laundry.count }.by(1) end内容としては、
1.店舗情報を投稿するためにユーザーを生成する。
2.FactoryBot.attributes_for
でテスト用の属性値を生成してlaundry_params
に代入。
3.ログインした後に情報を送り、Laundryモデルのカウントが1上がることを確認する。テストを実行したところ、以下のようなエラーが表示されました。
expected `Laundry.count` to have changed by 1, but was changed by 0どうやら、データが正しく登録されていないみたいです...。
原因
画像がパラメーターに含まれていなかった。
とりあえずログを確認。
test.logProcessing by LaundriesController#create as HTML Parameters: {"laundry"=>{"name"=>"コインランドリー名古屋店", "address"=>"愛知県名古屋市1-1", "opening_date"=>"2012-01-13", "open_time"=>"7:00", "close_time"=>"23:00", "shoe_washing"=>"true", "futon_washing"=>"true", "dryer"=>"true", "washing_machine"=>"true"}} Rendering laundries/new.html.erb within layouts/applicationパラメーターに値は含まれているが
new.html.erb
がレンダリングされている...
登録に失敗した場合new.html.erb
をレンダリングするようにしてあるため、エラーが生じていることはわかるが何が原因なのかはわからず。
「もしかしてパラメーターのキー名が間違っていてコントローラーで正しく受け取れていないのでは?」と思いストロングパラメーターを確認したところ...contoroller.rbdef laundry_params params.require(:laundry).permit(:name, :address, :opening_date, :open_time, :close_time, :shoe_washing, :futon_washing, :dryer, :washing_machine, :image) end
:image(permitメソッドの最後)
・・・これだ!!!これが抜けているから画像がないぞ!とエラーになっていることが判明。原因が判明したのはいいが、FactoryBotではデータを生成した後に画像をアタッチするようにしていたためどのようにしてパラメーターに含めればいいのかわからず苦戦しました。
FabtoryBotFactoryBot.define do factory :laundry do name { 'コインランドリー名古屋店' } address { '愛知県名古屋市1-1' } opening_date { '2012-01-13' } open_time { '7:00' } close_time { '23:00' } shoe_washing { true } futon_washing { true } dryer { true } washing_machine { true } after(:build) do |item| item.image.attach(io: File.open('public/images/test_image.png'), filename: 'test_image.png') end end end解決方法
fixture_file_uploadメソッドを使う
fixture_file_uploadメソッドはRSpecに用意されているメソッドです。
以下の記事を参考にさせていただきました。
Active Storage 導入環境下での単体テスト以下が、コードのbefore/afterです。
エラー発生時のテストコードit "店舗情報を追加できること" do laundry_params = FactoryBot.attributes_for(:laundry) sign_in @user expect { post laundries_path, params: { laundry: laundry_params } }.to change { Laundry.count }.by(1) endエラー解決時のテストコード
it "店舗情報を追加できること" do #laundry_paramsにimageを追加 laundry_params = FactoryBot.attributes_for(:laundry, image: fixture_file_upload("/files/test_image.png")) sign_in @user expect { post laundries_path, params: { laundry: laundry_params } }.to change { Laundry.count }.by(1) end無事にテストが実行されログにもimageが含まれていました!