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

renderするとURLが変わり、リロード問題に直面するやつ

はい!僕です!!

railsでポートフォリオを作成しています。

表題の問題にぶち当たり半日消耗したので、記録しておく。

環境

ruby2.7
rails6.0

問題の詳細

例えばblogs_controllerのcreateアクションを実装するとして、バリデーションが失敗するとrenderで新規投稿画面をrenderするコードを書きます。

blogs_controller.rb
 def create
    @blog = current_user.blogs.build(blog_params)
    if @blog.save
      flash[:success] = "投稿完了!!"
      redirect_to @blog
    else
      render "new"
      flash.now[:alert] = "失敗しました"  
    end
  end

投稿が失敗するとフラッシュメッセージとともにnew画面が表示されます。

ここでリロードするとルーティングエラーかもしくは別画面が表示されます。

なぜか?〜renderは画面の描写だけ

例の流れは

①新規作成画面のURLはblogs/new
②投稿するとcreateアクションが作動する。
③投稿が失敗する
④renderで画面はnewだがrenderは画面を描写しているだけなので、URLはcreateアクションのURLblogsになっている。
⑤リロードやF5とか押しちゃうと、blogsをgetしちゃうのでindexアクションが動く。(ルーティングを設定してなかったら、ルーティングエラーが発生)

rails.routesコマンド
blogs     GET      /blogs(.:format)                 blogs#index
          POST     /blogs(.:format)                  blogs#create
new_blog  GET   /blogs/new(.:format)     blogs#new
edit_blog GET  /blogs/:id/edit(.:format)   blogs#edit

blog GET      /blogs/:id(.:format)         blogs#show
     PATCH    /blogs/:id(.:format)         blogs#update
     PUT      /blogs/:id(.:format)         blogs#update
     DELETE   /blogs/:id(.:format)         blogs#destroy

解決策

①redirect_toを使う

renderを使っていることで起きているエラーなので、redirect_toを使う

blogs_controller.rb
 def create
    @blog = current_user.blogs.build(blog_params)
    if @blog.save
      flash[:success] = "投稿完了!!"
      redirect_to @blog
    else
      redirect_to new_blog_path
      flash.now[:alert] = "失敗しました"  
    end
  end

こうするとURLの問題は解決する。

ただ新たな問題としてフォームに入力していたものは消えてしまう。
(余談だけど、railsチュートリアルみたいにパーシャルを使ってエラーメッセージを描写している場合はそれも表示されなくなる)

この場合の対策はこちらの方の記事にあった
https://qiita.com/yuyasat/items/49e3296f3c64fccc7811

②JavaScriptを使う

僕はこの方法で解決した。

①新しいJavaScriptファイルを作成して、その中でhistory.replaceStateを使う。

app/javascript/blogs/render.js
history.replaceState('', '', '/blogs/new')

history.replaceStateとは

現在のブラウザの履歴を更新する。
第3引数でURLを入力する。

createアクションの失敗した際、上述したように、画面はviews/new、URL/blogsになっている。

この画面とURLの差異で問題が起きているので、現在のURL/blogs
blogs/newに変えます。

②applocation.jsに設定する
そのまま引用させていただきます。

対象のview用にコンパイルを行うために、新しいコンパイル用の設定をapp/javascript/packs/application.jsに作成する。

app/javascript/packs/application.js
.
.
.

require("blogs/render")

ちなみにコンパイルの言葉の意味がいまいちわかりません。
コンパイルってなんやねん・・・?

https://employment.en-japan.com/tenshoku-daijiten/14875/
コンパイルとは、プログラミング言語で書かれた文字列(ソースコード)を、コンピュータ上で実行可能な形式(オブジェクトコード)に変換することです。

・・・後回しで・・

③対象のviewに埋め込む

app/views/blogs/new.html.erb
<div class="container">
  <h2>新規作成</h2>
  <%= form_with model: @blog, local:true do |form| %>

  <%= render '/shared/error_messages', object: form.object %>

.
.
.
.

 <% end %>
</div>


# 下記を追加 

<% if @blog.errors.any? %>
  <%= javascript_pack_tag 'blogs/render' %>
<%end%>


これはバリデーションエラーがあったら、renderするときにjsを読み込んでねということを記述している。

読み込まれたjsは当然①のURLを変更するため、urlがblogs/newになる。

課題

解決はしたけど、問題は諸々ある

①とりあえず初回投稿失敗時にめちゃくちゃ時間かかる。

②各リソースごとにjsファイルを用意しないといけない
他に方法はあるのかもしれないけど、ユーザー新規登録にも同様の実装をしており、バリデーションをかけるものすべてに、やるのはDRYじゃない気がする(ただの直感)

このあたりは今後考えていく。

参考

https://laptrinhx.com/rails-render-houniurlga-bianwatteshimaukotoheno-dui-chu-fa-2616278188/
https://qiita.com/yuyasat/items/49e3296f3c64fccc7811
https://developer.mozilla.org/ja/docs/Web/API/History/replaceState

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bundler: not executable: bin/railsエラーがwheneverでパッチ処理しようとして出た時の対処法

wheneverを用いたパッチ処理の練習中のエラー。

log/cron.log
bundler: not executable: bin/rails
bundler: not executable: bin/rails
bundler: not executable: bin/rails
・・・続く

と同じエラーが続き、超初心者につきよくわからず悩んで途方に暮れていた時。。。

解決方法を見つけました!!!

参考:https://tebasaki.xyz/2019/01/19/bin-rails%E3%81%8C%E5%AE%9F%E8%A1%8C%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E6%99%82%E3%81%AE%E5%AF%BE%E5%87%A6%E6%B3%95/

ターミナル
$ bundle exec rake app:update:bin

をしてひたすらエンターエンターエンター
で解決しました!!!やった!!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

carrierwaveの導入

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Hashの導入

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsカラム名変更

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseを使ってユーザーの情報をパスワードなしで変更できるようにする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] view パスで条件分岐する方法 [current_page?]

current_page?

current_page?は、表示中のページのパスを判定できるメソッドです。
これはUrlHelperとして実装されているため、ビューの中から呼び出すことができます。

・URL

current_page?(http://hoge.com/hoge)

・パス

current_page?(/tweets/new)

・prefix

current_page?(new_tweet_path)

・アクション指定

current_page?(action: "new")

・コントローラー&アクション指定

current_page?(controller: "tweets", action: "new")


show.html.erb
<% if current_page?(tweet_path(@tweet)) %>

<% end %>

のような感じで、パスで条件分岐することが可能です。

筆者は同クラスでshowページを2画面作りたくてこの書き方をしました。
あまり良くない書き方の場合は、ご指摘いただけますと幸いです。


こちら
の記事を参照させていただきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecにて、引数が必要なsubjectの使い方

はじめに

RSpecで、同じメソッドに対して同様のテストを複数回書くシーンでは、subjectでDRYなコードを書くことができます。
ここでは引数が必要なメソッドのテストをsubjectを用いて書きます。

実装

ユーザー名(user_name)、自己紹介(pr)カラムがあるユーザーを、フリーワードで検索するuser_search(free_word)メソッドをテストするとします。以下のように繰り返し同じ処理が出てきます。

spec/models/user_spec.rb
describe "#user_search" do
  context "when user_name1 entered in free word" do
    it "return 1 results" do
      expect(Job.user_search("user_name1").count).to eq 1
    end
  end

  context "when pr1 entered in free word" do
    it "return 1 results" do
      expect(Job.user_search("pr1").count).to eq 1
    end
  end
end

これはrspec-its gemを使うことで、以下のようにsubjectでまとめて処理を書くことができます。

spec/models/user_spec.rb
describe "#user_search" do
  subject { ->(field) { Job.user_search(field).count } }

  context "when user_name1 entered in free word" do
    its(["user_name1"]) { is_expected.to eq 1}
  end

  context "when pr1 entered in free word" do
    its(["pr1"]) { is_expected.to eq 1}
  end
end

ますはgemでspec-itsを入れます。

Gemfile
group :development, :test do
  #省略
  gem 'rspec-its'
end
$ bundle install

これで、上記のコードが動作します。

参考
https://github.com/rspec/rspec-its

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【超かんたん】Active Storageで画像機能を実装しよう!

Active Storageを利用して画像を機能を実装します。
今回も初心者向けにレシピ投稿アプリを例に作成していきます。

完成イメージ

teri.jpg

Active Storageとは

ファイルアップロード機能を簡単に実装できるGem。Railsガイド

事前準備

ImageMagickの導入

ImageMagickとは

ImageMagickとは画像処理ライブラリです。
コマンドラインから画像に処理を加えることができるツールです。
Homebrewからインストールするので一度構築すれば次回以降この作業は不要になります。

ターミナル
brew install imagemagick

必要なGemのインストール

MiniMagickとImageProcessingをインストールします。

MiniMagickとは

mageMagickの機能をRubyで扱えるようにするためのGem。公式ドキュメント

ImageProcessingとは

画像サイズを調整するためのGem。公式ドキュメント

Gemfile
gem 'mini_magick'
gem 'image_processing'
ターミナル
bundle install

Active Storageの導入

Active Storageをアプリケーション内で使用するために以下のコマンドを実行します。

ターミナル
rails active_storage:install

インストールが完了するとActive Storageに関連したマイグレーションが作成されるので、マイグレートします。

ターミナル
rails db:migrate

マイグレートが完了したらactive_storage_blobsテーブルとactive_storage_attachmentsテーブルが作成されているかスキーマファイルを確認しましょう。

db/schema.rb
#中略
ActiveRecord::Schema.define(version: 20xx_xx_xx_xxxxxx) do

  create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "name", null: false
    t.string "record_type", null: false
    t.bigint "record_id", null: false
    t.bigint "blob_id", null: false
    t.datetime "created_at", null: false
    t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
    t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
  end

  create_table "active_storage_blobs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "key", null: false
    t.string "filename", null: false
    t.string "content_type"
    t.text "metadata"
    t.bigint "byte_size", null: false
    t.string "checksum", null: false
    t.datetime "created_at", null: false
    t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
  end
#以下略

画像を投稿しよう

アソシエーションの設定

レコードとファイルを1対1の関係で紐づけるためにhas_one_attachedメソッドを利用します。
今回はレシピ投稿アプリなのでrecipesテーブルに画像ファイルを紐付けます。

app/models/recipe.rb
class Recipe < ApplicationRecord
  has_one_attached :image
end

コントローラーの編集

画像の保存を許可するストロングパラメーターにしましょう。

app/controllers/recipes_controller.rb
class RecipesController < ApplicationController

#中略

  private
  def recipe_params
    params.require(:recipe).permit(:title, :text, :category_id, :time_required_id, :image) #「:image」を追加
  end
end

投稿画面の編集

画像を投稿するためにはfile_fieldというメソッドを使用します。Railsドキュメント

app/views/recipes/new.html.erb
<%= form_with model: @recipe, local: true do |f| %>

#中略
    <div class="form-group">
      <label class="text-secondary">画像</label><br>
      <%= f.file_field :image %>
    </div>

#以下略

<% end %>

それでは、投稿してみましょう!
new.png

保存されているか確認してみましょう。

ターミナル
rails c

[1] pry(main)> Recipe.find(1).image
   (0.4ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Recipe Load (0.2ms)  SELECT `recipes`.* FROM `recipes` WHERE `recipes`.`id` = 1 LIMIT 1
=> #<ActiveStorage::Attached::One:0x00007fc173c392e0
 @name="image",
 @record=
  #<Recipe:0x00007fc177cc2780
   id: 53,
   title: "てりやきチキン",
   text:
    "1. 鶏もも肉の両面にフォークで穴を開け、味をしみやすくします。\r\n\r\n2. ジッパー付き保存袋に鶏もも肉を入れ、調味料を加えてよく揉み込みます。\r\n\r\n3. 鶏もも肉が重ならないよう平らにならして空気を抜き、半分に折って冷凍庫に入れます。※保存期間は2週間です。\r\n\r\n4. フライパンにサラダ油を引いて中火に熱し、冷蔵庫で半解凍した鶏肉を、皮目を下にして入れます。フタをして5分蒸し焼きにします。裏返し、フタをしてさらに3分焼いたら完成です!",
   category_id: 5,
   time_required_id: 2,
   created_at: Wed, 03 Mar 2021 13:54:36 UTC +00:00,
   updated_at: Wed, 03 Mar 2021 13:54:36 UTC +00:00>>

投稿した画像を表示しよう

コントローラーの編集

app/controllers/recipes_controller.rb
class RecipesController < ApplicationController

#中略

  def show
    @recipe = Recipe.find(params[:id])
  end

  def create
    @recipe = Recipe.new(recipe_params)
    if @recipe.save
      redirect_to recipe_path(@recipe) #リダイレクト先をshowに変更
    else
      render :new
    end
  end

#以下略

end

ビューファイルの作成

保存した画像を表示するにはimage_tagメソッドを使います。Railsドキュメント
基本的な使い方は以下のとおりです。

<%= image_tag 画像ファイルのパス %>

#app/assets/imagesにcooking.jpgを配置している場合
<%= image_tag 'cooking.jpg' %>

#モデルから画像ファイルを呼び出す場合
<%= image_tag @recipe.image %>

それではビューファイルを作成していきましょう。

ターミナル
touch app/views/recipes/show.html.erb

show.html.erbを以下のように編集します。

app/views/recipes/show.html.erb
<div class="text-center">
  <div class="card recipe-card ">
    <%= image_tag @recipe.image, class: "card-img-top" %>
    <div class="card-body">
      <div class="recipe-name">
        <%= @recipe.title %>
      </div>
      <div class="recipe-content">
        カテゴリー: <span class="recipe-category"><%= @recipe.category.name %></span>
        所要時間: <span class="recipe-time"><%= @recipe.time_required.name %></span>
      </div>
      <hr>
      <p class="card-text">
        <div class="recipe-title">作り方</div>
        <div class="recipe-text d-flex justify-content-start">
          <%= safe_join(@recipe.text.split("\n"),tag(:br)) %>
        </div>
      </p>
    </div>
  </div>
</div>

しかし、このままでは画像が存在しない場合でも、画像を表示する記述が読み込まれて、エラーを引き起こしてしまいます。
この問題を解決するためにはattached?メソッドを使います。

attached?メソッドとは
ファイルが添付されていればtrue、添付されていなければfalseを返してくれるメソッドです。Railsガイド

それでは、ビューファイルの修正をしましょう。

app/views/recipes/show.html.erb
#中略
<div class="card recipe-card ">
  <%= image_tag @recipe.image, class: "card-img-top" if @recipe.image.attached?%>
  <div class="card-body">
#以下略

以上で完成になります。
実際に投稿が表示されるか確認してみましょう。
35752e8fc8e7033eb1122f524ce04b35.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【超かんたん】Active Storageで画像投稿機能を実装しよう!

Active Storageを利用して画像を機能を実装します。
今回も初心者向けにレシピ投稿アプリを例に作成していきます。

完成イメージ

teri.jpg

Active Storageとは

ファイルアップロード機能を簡単に実装できるGem。Railsガイド

事前準備

ImageMagickの導入

ImageMagickとは

ImageMagickとは画像処理ライブラリです。
コマンドラインから画像に処理を加えることができるツールです。
Homebrewからインストールするので一度構築すれば次回以降この作業は不要になります。

ターミナル
brew install imagemagick

必要なGemのインストール

MiniMagickとImageProcessingをインストールします。

MiniMagickとは

mageMagickの機能をRubyで扱えるようにするためのGem。公式ドキュメント

ImageProcessingとは

画像サイズを調整するためのGem。公式ドキュメント

Gemfile
gem 'mini_magick'
gem 'image_processing'
ターミナル
bundle install

Active Storageの導入

Active Storageをアプリケーション内で使用するために以下のコマンドを実行します。

ターミナル
rails active_storage:install

インストールが完了するとActive Storageに関連したマイグレーションが作成されるので、マイグレートします。

ターミナル
rails db:migrate

マイグレートが完了したらactive_storage_blobsテーブルとactive_storage_attachmentsテーブルが作成されているかスキーマファイルを確認しましょう。

db/schema.rb
#中略
ActiveRecord::Schema.define(version: 20xx_xx_xx_xxxxxx) do

  create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "name", null: false
    t.string "record_type", null: false
    t.bigint "record_id", null: false
    t.bigint "blob_id", null: false
    t.datetime "created_at", null: false
    t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
    t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
  end

  create_table "active_storage_blobs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "key", null: false
    t.string "filename", null: false
    t.string "content_type"
    t.text "metadata"
    t.bigint "byte_size", null: false
    t.string "checksum", null: false
    t.datetime "created_at", null: false
    t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
  end
#以下略

画像を投稿しよう

アソシエーションの設定

レコードとファイルを1対1の関係で紐づけるためにhas_one_attachedメソッドを利用します。
今回はレシピ投稿アプリなのでrecipesテーブルに画像ファイルを紐付けます。

app/models/recipe.rb
class Recipe < ApplicationRecord
  has_one_attached :image
end

コントローラーの編集

画像の保存を許可するストロングパラメーターにしましょう。

app/controllers/recipes_controller.rb
class RecipesController < ApplicationController

#中略

  private
  def recipe_params
    params.require(:recipe).permit(:title, :text, :category_id, :time_required_id, :image) #「:image」を追加
  end
end

投稿画面の編集

画像を投稿するためにはfile_fieldというメソッドを使用します。Railsドキュメント

app/views/recipes/new.html.erb
<%= form_with model: @recipe, local: true do |f| %>

#中略
    <div class="form-group">
      <label class="text-secondary">画像</label><br>
      <%= f.file_field :image %>
    </div>

#以下略

<% end %>

それでは、投稿してみましょう!
new.png

保存されているか確認してみましょう。

ターミナル
rails c

[1] pry(main)> Recipe.find(1).image
   (0.4ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Recipe Load (0.2ms)  SELECT `recipes`.* FROM `recipes` WHERE `recipes`.`id` = 1 LIMIT 1
=> #<ActiveStorage::Attached::One:0x00007fc173c392e0
 @name="image",
 @record=
  #<Recipe:0x00007fc177cc2780
   id: 53,
   title: "てりやきチキン",
   text:
    "1. 鶏もも肉の両面にフォークで穴を開け、味をしみやすくします。\r\n\r\n2. ジッパー付き保存袋に鶏もも肉を入れ、調味料を加えてよく揉み込みます。\r\n\r\n3. 鶏もも肉が重ならないよう平らにならして空気を抜き、半分に折って冷凍庫に入れます。※保存期間は2週間です。\r\n\r\n4. フライパンにサラダ油を引いて中火に熱し、冷蔵庫で半解凍した鶏肉を、皮目を下にして入れます。フタをして5分蒸し焼きにします。裏返し、フタをしてさらに3分焼いたら完成です!",
   category_id: 5,
   time_required_id: 2,
   created_at: Wed, 03 Mar 2021 13:54:36 UTC +00:00,
   updated_at: Wed, 03 Mar 2021 13:54:36 UTC +00:00>>

投稿した画像を表示しよう

コントローラーの編集

app/controllers/recipes_controller.rb
class RecipesController < ApplicationController

#中略

  def show
    @recipe = Recipe.find(params[:id])
  end

  def create
    @recipe = Recipe.new(recipe_params)
    if @recipe.save
      redirect_to recipe_path(@recipe) #リダイレクト先をshowに変更
    else
      render :new
    end
  end

#以下略

end

ビューファイルの作成

保存した画像を表示するにはimage_tagメソッドを使います。Railsドキュメント
基本的な使い方は以下のとおりです。

<%= image_tag 画像ファイルのパス %>

#app/assets/imagesにcooking.jpgを配置している場合
<%= image_tag 'cooking.jpg' %>

#モデルから画像ファイルを呼び出す場合
<%= image_tag @recipe.image %>

それではビューファイルを作成していきましょう。

ターミナル
touch app/views/recipes/show.html.erb

show.html.erbを以下のように編集します。

app/views/recipes/show.html.erb
<div class="text-center">
  <div class="card recipe-card ">
    <%= image_tag @recipe.image, class: "card-img-top" %>
    <div class="card-body">
      <div class="recipe-name">
        <%= @recipe.title %>
      </div>
      <div class="recipe-content">
        カテゴリー: <span class="recipe-category"><%= @recipe.category.name %></span>
        所要時間: <span class="recipe-time"><%= @recipe.time_required.name %></span>
      </div>
      <hr>
      <p class="card-text">
        <div class="recipe-title">作り方</div>
        <div class="recipe-text d-flex justify-content-start">
          <%= safe_join(@recipe.text.split("\n"),tag(:br)) %>
        </div>
      </p>
    </div>
  </div>
</div>

しかし、このままでは画像が存在しない場合でも、画像を表示する記述が読み込まれて、エラーを引き起こしてしまいます。
この問題を解決するためにはattached?メソッドを使います。

attached?メソッドとは
ファイルが添付されていればtrue、添付されていなければfalseを返してくれるメソッドです。Railsガイド

それでは、ビューファイルの修正をしましょう。

app/views/recipes/show.html.erb
#中略
<div class="card recipe-card ">
  <%= image_tag @recipe.image, class: "card-img-top" if @recipe.image.attached?%>
  <div class="card-body">
#以下略

以上で完成になります。
実際に投稿が表示されるか確認してみましょう。
35752e8fc8e7033eb1122f524ce04b35.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rspecエラー】expected #<ActiveModel::DeprecationHandlingMessageArray([])> to include ~ の解決

Rspecでテストコード記入中に発生したエラーです。

spec/models/quiz_spec.rbに以下の様にテストを書いていました。

require 'rails_helper'
RSpec.describe Quiz, type: :model do
  include ActionDispatch::TestProcess::FixtureFile


  let(:quiz) { create(:quiz) }

  ~

  describe '投稿画像の拡張子の検証' do 
    it '正しい拡張子の画像ファイルが問題画像として有効であること' do

  ~

    it '不正な拡張子の画像ファイルが正解画像として無効であること' do
        quiz.answer_image = fixture_file_upload('/e5cb899367efb53d53e5047185f273d8.gif')
        expect(quiz.errors[:answer_image]).to include('の拡張子が間違っています')
    end

  end

end

これでRspecを走らせると以下の様なエラーが発生しました。

Failures:

  1) Quiz 投稿画像の拡張子の検証 不正な拡張子の画像ファイルが正解画像として無効であること
     Failure/Error: expect(quiz.errors[:answer_image]).to include('の拡張子が間違っています')
       expected #<ActiveModel::DeprecationHandlingMessageArray([])> to include "の拡張子が間違っています"

~

エラー文を読んでもよくわかりませんでしたが、対象テストを以下の様に書き換えると通りました。

    it '不正な拡張子の画像ファイルが正解画像として無効であること' do
      quiz.answer_image = fixture_file_upload('/e5cb899367efb53d53e5047185f273d8.gif')
      quiz.valid?
      expect(quiz.errors[:answer_image]).to include('の拡張子が間違っています')
    end

quiz.valid?を追加しています。一度バリデーションをかけないとerrorsに何も値が代入されません。
今回のエラーはバリデーションにかけていなかったためにerrorsに何も値が代入されていなかったために発生していた様です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【未解決事件】バルクインサートでユニーク制約に引っ掛かるとIDが飛び飛びになる問題

本記事を読むその前に

本記事は事象を分かりやすくお伝えするため、サンプルアプリを題材として執筆しております。
実際は大規模システムにおいて大量の初期データを準備する必要があり、
その際に直面した問題から本記事を書くに至りました。
@jnchitoさん、ご指摘いただきありがとうございます?‍♂️)

結論

ユニーク制約のあるテーブルにバルクインサートを繰り返すと
ID(主キー)が飛び飛びになることがある。

スクリーンショット 2021-03-04 16.42.31.png

⬆︎ mahjan_piesテーブルのIDが飛び飛びに、、、。
⬇︎ こんな結果を期待してたのに。

スクリーンショット 2021-03-04 18.25.46.png

何をしたいのか

麻雀の座席をランダムに決めるための簡単なプログラムを考える。

麻雀では【東南西北】の中から1つを選び、引いた牌通りの場所に座る。
東南西北】は下図のように席に座り麻雀を打つことが出来るが、は見学となる。
(1半荘休み)

そのため、序盤はが嫌悪されるが、後半は誰もがを懇願することとなる。
(徹麻による体力の限界)

スクリーンショット 2021-03-04 14.51.43.png

ちなみにusersテーブルに登録する名前は俺の大学時代の友達の名前だ。
麻雀に明け暮れた結果、4人/5人が留年する結果となった?‍♂️?️?

やってみる

前提

・Rails6系でアプリを作ってある
・バルクインサートにはinsert_allメソッドを使う
activerecord-importというgemを導入してある

簡単なテーブル構造

今回登場するテーブルは2つ。
usersテーブル?‍♂️とmahjan_piesテーブル?️だ。

mahjan_piesテーブルはuser_idを外部キーとして持っている。
また、nameカラムとuser_idカラムはどちらもユニーク制約が設定されている。

usersテーブル
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint       | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime(6)  | NO   |     | NULL    |                |
| updated_at | datetime(6)  | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+


mahjan_piesテーブル
+------------+--------------+------+-----+---------+----------------+
| id         | bigint       | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  | UNI | NULL    |                |
| user_id    | bigint       | YES  | UNI | NULL    |                |
| created_at | datetime(6)  | NO   |     | NULL    |                |
| updated_at | datetime(6)  | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

Seedファイルにロジックを書く

席決めのプログラムはseedファイルに記述し、初期データとして登録させる。

これは説明するより見た方が早い。
コードは以下の通り。

db/seeds.rb
def pies
  %w(東 南 西 北 中) - MahjanPie.pluck(:name)
end

def user_ids
  [*1..5] - MahjanPie.pluck(:user_id)
end

def just_now
  Time.zone.now
end

def seat_position
  { name:       pies.sample,
    user_id:    user_ids.sample,
    created_at: just_now,
    updated_at: just_now }
end

User.insert_all([
  { name: 'わたなべ', created_at: just_now, updated_at: just_now },
  { name: 'さかい',   created_at: just_now, updated_at: just_now },
  { name: 'とし',    created_at: just_now, updated_at: just_now },
  { name: 'つとむ',   created_at: just_now, updated_at: just_now },
  { name: 'じゅーり', created_at: just_now, updated_at: just_now }
])

while MahjanPie.count < 5 do
  MahjanPie.insert_all(
    (5 - MahjanPie.count).times.map { seat_position }
  )

  puts "====== バルクインサート終了... ======"
  puts "座席、もしくは見学が決まっているのは#{MahjanPie.count}人だ"

  return puts "さて、今夜飛ぶのは誰かな?" if MahjanPie.count >= 5

  puts "残る牌は【#{pies.join(' ')}】のみ!"

  sleep(1.5) && puts('...')
  sleep(1.5) && puts('..')
  sleep(1.5) && puts('.')
end

いざ実行

seedファイルを実行するとこのような実行結果が得られた。

terminal
$ bundle exec rails db:seed
terminal
====== バルクインサート終了... ======
座席、もしくは見学が決まっているのは2人だ
残る牌は【東 南 中】のみ!
...
..
.
====== バルクインサート終了... ======
座席、もしくは見学が決まっているのは3人だ
残る牌は【東 南】のみ!
...
..
.
====== バルクインサート終了... ======
座席、もしくは見学が決まっているのは4人だ
残る牌は【東】のみ!
...
..
.
====== バルクインサート終了... ======
座席、もしくは見学が決まっているのは5人だ
さて、今夜飛ぶのは誰かな?

そしてテーブルの中身は以下の通り

terminal
usersテーブル
+----+--------------+----------------------------+----------------------------+
| id | name         | created_at                 | updated_at                 |
+----+--------------+----------------------------+----------------------------+
|  1 | わたなべ       | 2021-03-04 07:37:56.005154 | 2021-03-04 07:37:56.005172 |
|  2 | さかい         | 2021-03-04 07:37:56.005176 | 2021-03-04 07:37:56.005179 |
|  3 | とし           | 2021-03-04 07:37:56.005181 | 2021-03-04 07:37:56.005183 |
|  4 | つとむ         | 2021-03-04 07:37:56.005186 | 2021-03-04 07:37:56.005187 |
|  5 | じゅーり        | 2021-03-04 07:37:56.005190 | 2021-03-04 07:37:56.005191 |
+----+--------------+----------------------------+----------------------------+


mahjan_piesテーブル
+----+------+---------+----------------------------+----------------------------+
| id | name | user_id | created_at                 | updated_at                 |
+----+------+---------+----------------------------+----------------------------+
|  1 | 北   |       2 | 2021-03-04 07:37:56.018787 | 2021-03-04 07:37:56.018797 |
|  2 | 西   |       1 | 2021-03-04 07:37:56.019929 | 2021-03-04 07:37:56.019937 |
|  6 | 中   |       5 | 2021-03-04 07:38:00.541059 | 2021-03-04 07:38:00.541073 |
|  9 | 南   |       4 | 2021-03-04 07:38:05.092544 | 2021-03-04 07:38:05.092582 |
| 11 | 東   |       3 | 2021-03-04 07:38:09.618758 | 2021-03-04 07:38:09.618771 |
+----+------+---------+----------------------------+----------------------------+

つまりこのような席順になった。
(じゅーり、ドンマイ?)

スクリーンショット 2021-03-04 16.45.37.png

これでいよいよ麻雀を打てるわけだが、ちょっと待ってほしい。

テーブルに登録されたレコードをもう一度よく見てると、
バルクインサートをしたmahjan_piesテーブルのidが連続していない?
これは一体、、、??????

発行されたクエリを確認する(SQL)

Railsの開発環境でのログは log/development.log に記載されているので確認する。

log/development.log
INSERT INTO
  `users` (`name`, `created_at`, `updated_at`)
VALUES
  ('わたなべ', '2021-03-04 07:36:05.037216', '2021-03-04 07:36:05.037305'),
  ('さかい', '2021-03-04 07:36:05.037323', '2021-03-04 07:36:05.037334'),
  ('とし', '2021-03-04 07:36:05.037343', '2021-03-04 07:36:05.037351'),
  ('つとむ', '2021-03-04 07:36:05.037359', '2021-03-04 07:36:05.037366'),
  ('じゅーり', '2021-03-04 07:36:05.037374', '2021-03-04 07:36:05.037382')
ON DUPLICATE KEY UPDATE
  `name` = `name`

これはusersテーブルにデータを登録する際のクエリだ。
ちゃんと登録もされている。

次は問題のmahjan_piesテーブルに対するクエリを確認する。

log/development.log
INSERT INTO
  `mahjan_pies` (`name`, `user_id`, `created_at`, `updated_at`)
VALUES
  ('北', 2, '2021-03-04 07:37:56.018787', '2021-03-04 07:37:56.018797'),
  ('西', 1, '2021-03-04 07:37:56.019929', '2021-03-04 07:37:56.019937'),
  ('北', 1, '2021-03-04 07:37:56.020953', '2021-03-04 07:37:56.020958'),
  ('南', 1, '2021-03-04 07:37:56.021981', '2021-03-04 07:37:56.021987'),
  ('東', 2, '2021-03-04 07:37:56.023049', '2021-03-04 07:37:56.023053') 
ON DUPLICATE KEY UPDATE
  `name` = `name`


INTO
  `mahjan_pies` (`name`, `user_id`, `created_at`, `updated_at`)
VALUES
  ('中', 5, '2021-03-04 07:38:00.541059', '2021-03-04 07:38:00.541073'),
  ('中', 5, '2021-03-04 07:38:00.543138', '2021-03-04 07:38:00.543150'),
  ('南', 5, '2021-03-04 07:38:00.546174', '2021-03-04 07:38:00.546210')
ON DUPLICATE KEY UPDATE
  `name` = `name`


INSERT INTO
  `mahjan_pies` (`name`, `user_id`, `created_at`, `updated_at`)
VALUES
  ('南', 4, '2021-03-04 07:38:05.092544', '2021-03-04 07:38:05.092582'),
  ('南', 3, '2021-03-04 07:38:05.094148', '2021-03-04 07:38:05.094162')
ON DUPLICATE KEY UPDATE
  `name` = `name`


INSERT INTO
  `mahjan_pies` (`name`, `user_id`, `created_at`, `updated_at`)
VALUES
  ('東', 3, '2021-03-04 07:38:09.618758', '2021-03-04 07:38:09.618771')
ON DUPLICATE KEY UPDATE
  `name` = `name`

合計で4回のクエリが発行されていることが分かる。
ここでも結論を先に述べてしまうが、 ON DUPLICATE KEY UPDATE によりIDに抜けが出来てしまっているのだ。

ON DUPLICATE KEY UPDATE構文(SQL)

MySQLの公式ドキュメントにしっかりと記載されていた。
ユニーク制約に引っ掛かるデータは登録されないが、IDは増やしますぜ旦那、と。
※詳しくはリンク先に飛んで読んでほしいが、該当箇所は以下の通り。

スクリーンショット_2021-03-04_17_39_46.png

自動インクリメントカラム、つまりID(主キー)だ。。

ON DUPLICATE KEY UPDATE 構文を使えばユニーク制約が破られることはないが、
ユニーク制約に引っかかったデータの分だけIDが加算されてしまうらしい。。
何てこったい。

つまりこういうこと

各クエリごとに切り分けて見ていく。
最初のクエリでは5つのレコードを登録しようとしている。

ただし、nameカラムとuser_idカラムにユニーク制約がかかっているため、
登録出来たのは最初の2つのレコードだけ。ID 1, 2が振られる。

残り3つのレコードは登録出来ず、かつIDも加算されてしまっている。
そのためID 3, 4, 5は使えなくなった。
スクリーンショット_2021-03-04_17_24_21.png

次のクエリは3つのレコードを登録しようとしている。
最初のレコードはユニーク制約を受けず登録出来た。これがID 6になる。
残り2つのレコードは登録出来ず、かつID 7, 8も使えなくなった。
スクリーンショット_2021-03-04_17_24_35.png

次のクエリは2つのレコードを登録しようとしている。
最初のレコードは登録できた。これがID 9
残り1つのレコードは登録出来ず、ID 10も使えなくなった。
スクリーンショット_2021-03-04_17_24_47.png

そして最後に登録されたレコードがID 11
先ほども示したが、その結果がこれになるわけだ。なるほど理解できた。

terminal
mahjan_piesテーブル
+----+------+---------+----------------------------+----------------------------+
| id | name | user_id | created_at                 | updated_at                 |
+----+------+---------+----------------------------+----------------------------+
|  1 | 北   |       2 | 2021-03-04 07:37:56.018787 | 2021-03-04 07:37:56.018797 |
|  2 | 西   |       1 | 2021-03-04 07:37:56.019929 | 2021-03-04 07:37:56.019937 |
|  6 | 中   |       5 | 2021-03-04 07:38:00.541059 | 2021-03-04 07:38:00.541073 |
|  9 | 南   |       4 | 2021-03-04 07:38:05.092544 | 2021-03-04 07:38:05.092582 |
| 11 | 東   |       3 | 2021-03-04 07:38:09.618758 | 2021-03-04 07:38:09.618771 |
+----+------+---------+----------------------------+----------------------------+

activerecord-import も同様

Rails5系で主流だったgem、activerecord-import でも同様の事象が発生してしまう。
参考までに activerecord-import を使う場合、コードはこのようになる。

db/seeds.rb
while MahjanPie.count < 5 do
- MahjanPie.insert_all(
-     (5 - MahjanPie.count).times.map { seat_position }
+ MahjanPie.import(     
+     (5 - MahjanPie.count).times.map { seat_position },
+     on_duplicate_key_ignore: true
  )

さいごに

そもそもユニーク制約に引っ掛かるようなクエリ発行するのがアカンやろ!というご指摘はもっともです。
が!!IDが飛び飛びにならずにバルクインサートできる方法を知っている方!
もしいたらぜひ教えてください?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6.1ハンズオン(3)~ユーザー機能編

はじめに

Rails6.1ハンズオン(2)の続きです。
環境は前回を参考にしてください。

いつRails6.1固有の機能にたどり着くのか...

今回はメールアドレスとパスワードを使った、
ユーザーログイン・ログアウトの機能を追加します。
コードはGithubにあげています。章ごとにコミットしてますので、参考にしていただければ幸いです。

やること

Railsでユーザー関連の機能をつけるときは、Railsチュートリアルのように自前で全部用意することも可能ですが、今回は楽をするためにGemを使います。

定番は「devise」でしょうか?仕事でもよく使いますが、せっかくなので、「sorcery」を使ってみようと思います。

最近(21/2/17くらい)v0.16.0になり、Rails6に対応したようです。ぴったりですね!

実装

参考にさせていただくもの:
Sorcery/sorcery
docker-compose下でrails newして Rails6.1 + Sorcery を試す( Sorcery の仕組み少し解説)
シンプル認証gem sorceryを完全入門するで!! - Qiita

4-1. sorcery gem導入、Userクラスをつくる

Gemfileにて以下を追加:

gem 'sorcery'
gem 'bcrypt'

↓これに準拠して進めます
Sorcery/sorcery

コマンドを実行:

bundle install
bundle exec rails g sorcery:install

File unchanged! The supplied flag value not found! app/models/user.rb

と赤字で出た。なんだろう... コマンドオプションを設定するとでないのかな?
とりあえずヨシ!

gコマンドで生成されたmigrationファイルでテーブル名がUserになっているのをusersに直す。(Userのまま進めたらエラーになりました)

db/migrate/(数字)_sorcery_core.rb

class SorceryCore < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email,            null: false
      t.string :crypted_password
      t.string :salt

      t.timestamps                null: false
    end

    add_index :users, :email, unique: true
  end
end

Userモデルのバリデーション設定を追加する。

app/models/user.rb

class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :email, uniqueness: true, presence: true

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
end

チュートリアルではemailのバリデーションにpresence: trueが入っていなかったので追加。

4-2. ユーザー作成・編集機能

rails g controller Users new create edit update

app/views/layouts/_header.html.haml(セッション機能は後で作る)

%nav.navbar.navbar-expand-lg.navbar-dark.bg-dark
  .container-fluid
    = link_to root_path, class: 'navbar-brand' do
      Rails6.1掲示板
    %button.navbar-toggler{"aria-controls" => "navbarSupportedContent", "aria-expanded" => "false", "aria-label" => "Toggle navigation", "data-bs-target" => "#navbarSupportedContent", "data-bs-toggle" => "collapse", :type => "button"}
      %span.navbar-toggler-icon
    #navbarSupportedContent.collapse.navbar-collapse
      %ul.navbar-nav.me-auto.mb-2.mb-lg-0
        %li.nav-item
          = link_to root_path, class: 'nav-link' do
            トップ
        %li.nav-item
          = link_to new_community_path, class: 'nav-link' do
            コミュニティを作る
      .d-flex
        -if current_user
          .nav-item.dropdown
            %a.nav-link.text-white.dropdown-toggle#navbarDropdownMenuLink{href: "#", role: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false"}
              = current_user.email
            %ul.dropdown-menu{"aria-labelledby": "navbarDropdownMenuLink"}
              %li
                = link_to t('header.to_user_edit'), edit_user_path(current_user.id), class: 'dropdown-item'
              %li
                = link_to t('header.to_logout'), :logout, method: :post, class: 'dropdown-item'
        - else
          = link_to t('header.to_register'), new_user_path, class: 'btn btn-primary me-2'
          = link_to t('header.to_login'), :login, class: 'btn btn-secondary'

app/controllers/users_controller.rb(require_loginメソッドは後で作ります)

class UsersController < ApplicationController
  skip_before_action :require_login, only: %i[new create]
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to root_path, notice: 'ユーザーを新規作成しました。'
    else
      render :new
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to root_path, notice: 'ユーザー情報を変更しました。'
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

app/views/users/new.html.haml

%h1= t '.title'

= form_with model: @user do |f|
  = render 'shared/error_messages', object: f.object

  = f.label :email, class: 'form-label'
  = f.text_field :email, class: 'form-control mb-2'

  = render 'form', form: f
  = f.submit t('.submit'), class: 'btn btn-primary'

app/views/users/edit.html.haml

%h1= t '.title'

= form_with model: @user do |f|
  = render 'shared/error_messages', object: f.object
  = render 'form', form: f
  = f.submit t('.submit'), class: 'btn btn-primary'

app/views/users/_form.html.haml

= form.label :password, class: 'form-label'
= form.password_field :password, class: 'form-control mb-2'

= form.label :password_confirmation, class: 'form-label'
= form.password_field :password_confirmation, class: 'form-control mb-2'

app/views/shared/_error_messages.html.haml

- if object.errors.present?
  .card.mb-2.border-danger
    .card-header.bg-danger.text-white
      エラーがあります
    .card-body
      %ul
        - object.errors.each do |e|
          %li= e.full_message

config/locales/model.ja.yml
config/locales/views/users/ja.yml
config/routes.rb
をいじる(コードは省略、リポジトリをみてください)

4-3. セッション機能

rails g controller UserSessions new create destroy

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action :require_login

  private

  def not_authenticated
    redirect_to login_path, alert: 'ログインしてください。'
  end
end

require_loginはsorceryにもともと入っているメソッドらしい。
ログインしていなかったらnot_authenticatedメソッドを呼ぶという挙動。
not_authenticatedも忘れずに定義しておく。

app/controllers/user_sessions_controller.rb

class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[new create]
  def new; end

  def create
    @user = login(params[:email], params[:password])

    if @user
      redirect_back_or_to(root_path, notice: 'ログインしました。')
    else
      flash.now[:alert] = 'ログインできませんでした。'
      render :new
    end
  end

  def destroy
    logout
    redirect_to(login_path, notice: 'ログアウトしました。')
  end
end

login(メアド, パスワード)でログインできる。
redirect_back_or_toで#not_authenticatedでリダイレクトする前のページに戻せる。
logout()でログアウト。

app/views/user_sessions/new.html.haml

%h1= t '.title'
= form_with url: login_path, method: :post do |f|
  = f.label t('.email'), class: 'form-label'
  = f.text_field :email, class: 'form-control mb-2'

  = f.label t('.password'), class: 'form-label'
  = f.password_field :password, class: 'form-control mb-4'

  = f.submit t('.submit'), class: 'btn btn-primary'

config/routes.rb

Rails.application.routes.draw do
  root to: 'communities#index'

  get 'login' => 'user_sessions#new', as: :login
  post 'login' => 'user_sessions#create'
  post 'logout' => 'user_sessions#destroy', as: :logout
  resources :users

  resources :communities, only: %i[index new create show] do
    resources :comments, only: %i[new create]
  end
end

4-4. おまけ

flashメッセージを表示する。

app/views/layouts/application.html.haml

!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title Rails61HandsOn
    %meta{:content => "width=device-width,initial-scale=1", :name => "viewport"}/
    = csrf_meta_tags
    = csp_meta_tag
    = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
    = stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
  %body
    = render '/layouts/header'
    - if flash[:notice]
      .alert.alert-info.m-2= flash[:notice]
    - if flash[:alert]
      .alert.alert-warning.m-2= flash[:alert]
    .container-fluid.pt-4.px-4
      = yield
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【テストコードで使用するFakerについて】

テストコードを実行する際、FactryBot内で活躍するFakerについて、
ちょっとした学びがあったので備忘録として残す。

Fakerは無作為に半角数字、半角英語の組み合わせを作ってくれるもの。
パスワードのバリデーションとして、「英数字を含む」という設定が多々見受けられると思う。
基本的にはFakerをそのまま使えば問題ないが、稀に数字のみ、もしくは英語のみという組み合わせになることもある。
そうなってしまうと、バリデーションではじかれてしまうことになる。

そこでFakerに"1a"などの文字列をくっつけてやる。
そうすることで必ず"1a"という数字、英語を含む組み合わせを作ることができる。

{'1a' + Faker::Internet.password(min_length: 6)}

このような形とする。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

fixtureをrails_helperでまとめて読み込む

はじめに

rspecでfixtureを使用する際、各ファイルでfixtures :allするのが面倒なとき、rails_helperでまとめて読み込むことができます。

結論

spec/rails_helper.rb
RSpec.configure do |config|
  config.global_fixtures = :all
end

すでに存在するRSpec.configure do |config|ブロック内に、config.global_fixtures = :allを格納します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

結合テストコードの書き方(Rails)

今回はRailsでの結合テストコードの記述の仕方を記事にしたいと思います。
開発したオリジナルアプリのコードを元に説明したいと思います。

なお、FactoryBotとFakerを導入済みです

はじめに

単体テストコードでは、RspecというGemを導入しました。
結合テストコードでは、System Specという技術とCapybaraというGem
が必要です。
しかし、これらはデフォルトで導入済みです。

Gemfile

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

Gemfileを確認すると、Capybaraが導入されていることがわかります。

Gemが導入されていることがわかったので、次はターミナルよりファイルを作成します。

% rails g rspec:system users

Image from Gyazo

system/user_spec.rbが作成されていることが確認できます。

今回はユーザー新規登録ができる時のテストコードを書いていきたいと思います。
まずは、example洗い出します。

system/user_spec.rb

describe "新規登録できる時" do
      it "正しい情報が登録できれば新規登録ができてトップページに遷移する" do
        # トップページに移動する
        # トップページに新規登録へ遷移のボタンがあることを確認する        
        # 新規登録ページへ移動する
        # ユーザー情報を入力する
        # 新規登録ボタンをクリックするとユーザーカウントが1上がる
        # トップページへ遷移したことを確認する
        # トップページに新規登録ボタンやログインボタンがないことを確認する
      end
    end

ユーザー目線で、どのように操作するか考えながらexampleを洗い出します。

コードの記述

一項目ずつ説明したいと思います。

# トップページに移動する
   visit root_path

・visit
visitを使用しpathを指定するとページに移動します。

# トップページに新規登録へ遷移のボタンがあることを確認する
  expect(page).to have_content("新規登録")

これは、ページの中に新規登録の文字列が含まれているかを判断します。
文字れるの確認には、have_contentのマッチャを使用します。
ちなみに、画像の場合はhave_selectorを使用します。

# 新規登録ページへ移動する
  visit new_user_registration_path
# ユーザー情報を入力する
  fill_in "メールアドレス", with: @user.email
  fill_in "パスワード(半角英数混合6文字以上)", with: @user.password, match: :first
  fill_in "パスワード再入力", with: @user.password_confirmation, match: :first
  fill_in "nickname", with: @user.nickname
  fill_in "プロフィール(自分を一言で表現してください)", with: @user.profile

「ユーザー情報入力」の部分、フォームへの入力はfill_inをを使用します。

fill_in 'フォームの名前', with: '入力する文字列'

のように記述することで、フォームへの入力を行うことができます。
フォームの名前の入力には検証ツールを使用します。

Image from Gyazo

label要素のforに指定されているIDと、inputのidの値が同一になっています。
各フォームがlabelで紐付いていることがわかります。
フォームの名前が「メールアドレス」ということがわかるので、抜き出しました。
この要領で抜き出す部分を探します。

入力する文字列は、@userの中にはFactoryBotで設定した情報が入っていて、
.emailとつけることによってemailの情報を入力しています。

# 新規登録ボタンをクリックするとユーザーカウントが1上がる
  expect{
  find('input[name="commit"]').click
  }.to change { User.count }.by(1)

新規登録が完了すし、ユーザーが1人増えることを確認しています。

・find().click
find('クリックしたい要素').clickと記述することで、実際にクリックができます。
つまりフォームを入力した後に、登録完了ボタンを押すということです。
これも検証ツールで確認することができます。

Image from Gyazo
この部分のどこかしらを指定してあげれば大丈夫です。

・change
expect{ 何かしらの動作 }.to change { モデル名.count }.by(1)と記述することによって、モデルのレコードの数がいくつ変動するのかを確認できます。changeマッチャでモデルのカウントをする場合のみ、expect()ではなくexpect{}となります。

クリックして、ユーザーが1人増えたことを確認しているわけですね。
changeの時だけ、expect{}になるは忘れそうなので要チャックです。

# トップページへ遷移したことを確認する
  expect(current_path).to eq(root_path)
# トップページに新規登録ボタンやログインボタンがないことを確認する
  expect(page).to have_no_content("ログイン")
  expect(page).to have_no_content("新規登録")

新規登録が完了した後は、トップページに移動するように処理を記述しています。
なので、移動した現在のページ(current_path)がトップページ(root_path)で
あることを確認しています。

そして、ログイン状態のトップページには、「ログイン」「新規登録ボタン」がない
ことを確認することにより、新規登録が完了しログイン状態になっていることが確認できます。
have_no_contentはhave_contentとは逆でないことを確認します。

以上が一部ですが、結合テストコードの書き方です。
色々書きましたが、これらは調べれば出てくるので
暗記する必要はなく大事なのは、意味を理解することだと学びました。

もし何か間違った記述や気になることありましたらコメント残して頂ければ幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】コピペされたソースコードから英単語のみを抽出し、初出の単語のみ保存するプログラムを組んでみた

初めに

 この記事は、上記記事で紹介したポートフォリオで使用した技術を切り出した記事になります。もし宜しかったらこちらもご覧ください。

イメージ図

Image from Gyazo

Image from Gyazo

 上記画像のように、コピペしたソースコードから英単語を抽出し、初出の英単語のみ保存するプログラムを組んでみました。ポートフォリオのタイピングアプリにおいて必須になる機能です。

開発環境

  • Ruby 2.6.5
  • Rails 6.0.3

実際に組んだプログラム

models/form.rb
def save
  ActiveRecord::Base.transaction do
    book = Book.create(title: title, color: color, user_id: user_id)
    strings = code.split(/[\W|\d|\s]+/).uniq.select { |str| str.length != 1 }
    strings.each do |string|
      word = Word.where(word: string).first_or_create
      BookWord.create(book_id: book.id, word_id: word.id)
    end
    language = Language.where(name: name).first_or_create
    BookLanguage.create(book_id: book.id, language_id: language.id)
    score = Score.new
    score.typing_score = '0'
    score.typing_time = '0'
    score.book_id = book.id
    score.save
  end
end

 上記ソースコードが実際に英単語問題の作成を行うソースコードです。フォームオブジェクトパターンで組んでいるので他のモデルの記述も入っていますが、実際に英単語の抽出を行っているのは、

models/form.rb
 strings = code.split(/[\W|\d|\s]+/).uniq.select { |str| str.length != 1 }
 strings.each do |string|
   word = Word.where(word: string).first_or_create
   BookWord.create(book_id: book.id, word_id: word.id)
 end

この部分になります。中間テーブルの記述が混じってますが…

解説

 順を追って説明していきます。

models/form.rb
  strings = code.split(/[\W|\d|\s]+/).uniq.select { |str| str.length != 1 }

 まず、このフォームオブジェクトではコピペしたソースコードの内容を変数codeに格納しています。サーバーのログを見てみると、
Image from Gyazo
上記画像の赤い印をつけた部分のようにデータが飛んできていることが分かります。それでは、ここから半角英単語のみ抽出していきましょう。この場合はstringクラスのsplitメソッドがとても便利です。

stringクラスのsplitメソッド

 splitメソッドは、簡単に説明すると「stringクラスのオブジェクトを、引数に指定した方法を用いて分割し、それらを配列に格納する」メソッドになります。具体的に見ていきましょう。
 code.split(/[\W|\d|\s]+/)の部分において、変数codeにはコピペしたソースコードが格納されていますが、半角英語だけでなく数値や全角文字、記号や空白文字・改行文字など、今回は不要なオブジェクトが満載です。splitメソッドが行う処理のイメージは、正規表現などを用いて引数に指定したオブジェクトを「区切り」として、対象から取り除きながら、区切られた文字列を配列に格納するという感じです。今回の記述でいうと、\W 非単語構成文字 [^a-zA-Z0-9_]\d 10進数字 [0-9]\s 空白文字 [ \t\r\n\f\v]1回以上使われているのを検知した場合、その文字を「区切り」として分割した文字列の集合を配列に格納しています。
 つまりこんな処理を行っています。

hoge.rb
  string = "hoge1huga.ho@ga ho ge"
  array = string.split(/[\W|\d|\s]+/)

 上記のような記述の場合、string ="hoge[区切]huga[区切]ho[区切]ga[区切]ho[区切]ge"のように、記号や数値・空白文字を[区切]に変換して文字列が格納された変数stringを分割します。その結果、

hoge.rb
  p array
  => ["hoge", "huga", "ho", "ga", "ho", "ge"]

のように配列に格納されていきます。

arrayクラスのuniqメソッド

 さて、stringクラスのsplitメソッドによって、対象となるオブジェクトのクラスがstringクラスからarrayクラスに変わりましたので、ここからは配列を処理するメソッドを使用していきます。

 まだ半角英語のみ配列に格納した状態なので、何度も登場する単語が重複してしまっている状態です。この配列から、重複した要素を取り除いた新しい配列を作成するのがarrayクラスのuniqメソッドです。

arrayクラスのselectメソッド

 まだ完成ではありません。今のままでは1文字だけの英単語(eachメソッドを使用した際のブロック変数"i"とか)が残ってしまいますので、今回はarrayクラスのselectメソッドを採用しました。(本当はstringクラスのsplitメソッドの正規表現で処理したかったのですが、上手くして指定することが出来ませんでした…。)

models/forms.rb
  strings = array.select { |str| str.length != 1 }

 現状、イメージとしては上記のように英単語が格納された配列がある状態です。これに対してarrayクラスのselectメソッドはどのような処理を行うのでしょうか。後ろに波括弧が付いてますね、中身を確認してみましょう。
 { |str| str.length != 1 }一見複雑そうに見えますが、やっていることはとても単純です。arrayクラスのselectメソッドが行う処理は、配列内の要素1つ1つに対して条件式を適用し、返り値がtrueになった要素だけ抽出して新しい配列を作成するというものです。つまり、波括弧内の|str|はarrayクラスのeachメソッドにも登場するブロック変数であり、配列に格納されている英単語1つ1つに対して条件式str.length != 1を実行しているだけのメソッドになります。今回の場合は、「文字数が1文字ではない英単語」のみ抽出して新しい配列に格納しています。
 これにて、目当ての英単語のみ格納された配列が完成しました。

初出の単語のみ保存する

 ゴールまであと一歩です。最後に、初出の単語のみ保存する処理を記述していきます。これを記述しなければ同じ単語が何度も保存されてしまうためデータベースを圧迫してしまいます。英単語は文字列のため数値などと比べて容量が大きいので、既に保存されている単語が送信されてきた際は登録されている単語のレコードだけ返す処理の方が効率的です。これを可能にするのがfirst_or_createメソッドもしくはfirst_or_initializeメソッドとsaveメソッドになります。

models/form.rb
 strings.each do |string|
   word = Word.where(word: string).first_or_create
   BookWord.create(book_id: book.id, word_id: word.id)
 end

first_or_createはとても便利なActiveRecordメソッドで、上記のようにwhere句と組み合わせて使用することで、「特定のカラムに特定のバリューは存在するか」を検索した後、存在しないなら新しくレコードを作成し、レコードの情報を変数に格納することができ、存在するならそのレコードを変数に格納することが出来ます。
 もしレコードを参照するタイミングと保存するタイミングを分けたいならばfirst_or_initializeメソッドsaveメソッドを使いましょう。

まとめ

 データを思うように成形するには「それは何クラスのオブジェクトか」というのをまず考えるようにしたことで、目当てのメソッドを見つけるまでの時間が格段に短くなっていきました。まだまだ使い方を知らないメソッドだらけですが、1つ1つ着実に習得していきたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】モデルの単体テストコードについてまとめ(RSpec)

初学者です。
テストコードがなぜか大好きです。

今回はモデルの単体テストコードについてまとめます。
私はRSpecを利用しています。

モデルのテストコードを書く方針は
インスタンスを生成し、そのインスタンスがモデルに規定したどおりの挙動になるか(バリデーションが正しく働くか等)を確かめる
ことです。

前提条件

  • pry-railsを導入済みである
  • FactoryBotを導入済みである

上記については以下の記事にまとめています。

【Ruby on Rails】デバッグツール(pry-rails)
【Ruby on Rails】FactoryBotとFakerについてまとめ

RSpec

RSpecは、Railsのテストコードを書くために用いられるGemです。
RSpec公式Github

テストコードの種類

  • 単体テストコード

    モデルやコントローラーなど機能ごとに問題がないか確認します。
    例えばバリデーションがきちんと動作しているかなどです。

  • 結合テストコード

    ユーザーがブラウザで操作する一連の流れを再現して問題がないか確かめます。
    例えば、ユーザー登録で「名前とメールアドレスとパスワードを入力するとトップページに遷移して表示がユーザーの名前に変わっている」などの一連の動作を確認します。

  • 正常系

    ユーザーが開発者の意図する操作を行った時の挙動を確認するテストコードです。
    例えば、ユーザー登録で問題なく全てのデータが入力された場合などです。

  • 異常系

    ユーザーが開発者の意図しない操作を行った時の挙動を確認するテストコードです。
    例えば、ユーザー登録で正しい値が入っていないと登録できないかどうかなどです。

RSpec導入

下記のようにGemfilegroup :development, :test dogem 'rspec-rails', '~> 4.0.0'と記述します。

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]

# 以下を追加
  gem 'rspec-rails', '~> 4.0.0'
end

ターミナルで下記のコマンドを実行しGemを導入します。

ターミナル
bundle install

ターミナルで下記のコマンドを実行しアプリケーション内でRSpecを使用するための設定を行います。実行すると関連するファイルが生成されます。

ターミナル
rails g rspec:install

生成された.rspecに下記のように記述します。
テストコードの結果をターミナルで可視化するために必要な記述です。

.rspec
--require spec_helper

# 以下を追加
--format documentation

ターミナルで下記のコマンドを実行しテストコードを記述するファイルを生成します。

ターミナル
rails g rspec:model モデル名

# 以下はuserモデルのファイル生成の例
rails g rspec:model user

テストコードを記述するファイルは手動で生成してもいいのですが、require 'rails_helper'の記述がないと読み込めないファイルがあり、きちんとテストできないのでコマンドで生成した方がいいみたいです。

もし、このコマンドで生成した際に既にFactoryBotが導入済みであればFactoryBotのファイルも一緒に生成してくれますし楽だと思います。

単体テストコードの準備

本題です。
まだ業務経験もないペーペーなのであしからず。

ユーザー登録についての単体テストコードを例にします。

最初に何を検証すべきなのかを洗い出していきます。

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do

# FactoryBotでデータをbuild
  before do
    @user = FactoryBot.build(:user)
  end

  describe 'ユーザー新規登録' do

# 正常系
    context '新規登録できる時' do
      it '全ての項目が存在すれば登録できる' do
      end
      it 'nameが6文字以下であれば登録できる' do
      end
      it 'passwordとpassword_confirmationが6文字以上であれば登録できる' do
      end
    end

# 異常系
    context '新規登録できない時' do
      it 'nameが空だと登録できない' do
      end
      it 'emailが空だと登録できない' do
      end
      it 'passwordが空だと登録できない' do
      end
      it 'passwordが存在してもpassword_confirmationが空だと登録できない' do
      end
      it 'nameが7文字以上では登録できない' do
      end
      it '重複したemailが存在する場合登録できない' do
      end
      it 'passwordが5文字以下では登録できない' do
      end
    end
  end
end

解説していきます。

まず以下のようにbefore doでFactoryBotのデータをbuildします。
インスタンス変数にする必要があるので@userになります。

before do
    @user = FactoryBot.build(:user)
end

次に以下のようにdescribeどの機能についてのテストを行うかを記述します。
グループ分けのようなものです。

describe 'ユーザー新規登録' do

end

次に以下のようにcontextでさらに分けていきます。
contextではどんな状況を確認したいのかで分けるので、私は正常系と異常系に分けて考えます。

# 正常系
context '新規登録できる時' do

end

# 異常系
context '新規登録できない時' do

end

次に以下のようにitでさらに細かい機能に分けます。
itに書いた確認したい状況をテストしていきます。

it '全ての項目が存在すれば登録できる' do

end

正常系

正常系を書いていきます。

spec/models/user_spec.rb
context '新規登録できる時' do

  it '全ての項目が存在すれば登録できる' do
    expect(@user).to be_valid
  end

  it 'nameが6文字以下であれば登録できる' do
    @user.name = 'abcde'
    expect(@user).to be_valid
  end

  it 'passwordとpassword_confirmationが6文字以上であれば登録できる' do
    @user.password = '123456'
    @user.password_confirmation = '123456'
    expect(@user).to be_valid
  end
end

解説します。

まず「全ての項目が存在すれば登録できる」についてです。
expect(@user)@userbe_validで正しいかどうかを判断しています。

expect(@user).to be_valid

次は「nameが6文字以下であれば登録できる」についてです。
@user.nameabcde(つまり6文字以下の文字列)を代入して、そのデータが入った@userbe_validで正しいかどうかを判断しています。

@user.name = 'abcde'
@expect(@user).to be_valid

次に「passwordとpassword_confirmationが6文字以上であれば登録できる」についてです。
nameと同じように@user.password123456を代入し、そのデータが入った@userbe_validで正しいかどうかを判断しています。

@user.password = '123456'
@user.password_confirmation = '123456'
expect(@user).to be_valid

これで成功するかどうかターミナルに以下を入力します。

ターミナル
bundle exec rspec spec/models/user_spec.rb

成功したら緑色の文字で結果が出力されます。

異常系

1つを例に解説します。
「nameが空だと登録できない」についてを例にします。

it 'nameが空だと登録できない' do

  # 下記を追加
  @user.name = ''
  @user.valid?
  binding.pry

end

まずFactoryBotでデータを入れてある@userのnameに「''」のように空を代入します。
次にvalid?で正しいか?(trueかfalseを返す)を確認しています。

そしてbinding.pryで処理を止める記述をしました。
この状態でターミナルに以下を入力すると処理が止まります。

ターミナル
bundle exec rspec spec/models/user_spec.rb

処理が止まりコンソールに入力できるようになるのでuser.errors.full_messagesと入力するとメッセージが出てきます。
そのメッセージを踏まえてテストコードを変更していきます。

it 'nameが空だと登録できない' do

  @user.name = ''
  @user.valid?

# 以下を変更
  expect(@user.errors.full_messages).to include("name can't be blank")

end

上記の記述はexpectの引数にincludeの引数が含まれるという意味です。
先ほどuser.errors.full_messagesで出てきたメッセージをincludeの引数に与えることで一致する(含まれる)ということになります。

こんな感じで記述していきます。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyのincludeを理解したい②

書くこと

先日書いた、こちらの記事の続きとなります。

前回の記事の概要をまとめると

Rubyのクラスとインスタンスの関係から、メソッドはどのように扱われているのか?

ということをまとめました。

今回の記事は、前回の内容を元に、Rubyのモジュールから本題のincludeについて、説明していきます。

includeとは?

早速ですが、Rubyのincludeについて少し触れていきましょう。
実際に見てもらったほうが早いので、まずはこちらをご覧ください。

example.rb
class QuestionAnswer
  include ActiveModel::Model   #ActiveModel::Modelというモジュールをincludeしています。
end

このように、QuestionAnswerというクラスに、ActiveModel::Modelというモジュールをincludeしています。こうすることで、

クラスにinclude モジュール名と記述することで、そのモジュールのインスタンスメソッドをクラスで使用出来るようになります。

これが、includeの大枠の役割となります。

includeとメソッドの旅

以上のincludeの役割と踏まえると、このクラスから生成されたインスタンスは、includeされたmoduleのところまでメソッドを探しに行けるというということが言えそうです。

この話は、前回の記事の最大のテーマ

インスタンスはメソッドを持つのではなく、生成されたクラスに参照しに行く性質を持つ

ということにつながってきます。このことを、もう少し端的な例で見てみましょう。

example.rb
Module Alpha
  def module_a_method
    @a_method
  end
end

class Beta
  include Alpha  #モジュールをincludeしました。
  def class_b_method
    @class_b_method
  end
end

Ceta = B.new
Ceta.module_a_method

ここで、Betaクラスから生成されたインスタンスがCetaに代入されています。
そして、CetaModule Alphaで定義されたインスタンスメソッドmodule_a_methodを利用しています。

通常、スコープの性質上、あるクラスから生成されたインスタンスはそのクラス内に記述されたインスタンスメソッドのみを用いることができます。しかし、今回はモジュールをincludeしているので、そこで定義されているmodule_a_methodを利用することができます。

これは、以下の図で理解してもらうと良いかもしれません。

アウトプットメモ-338.jpg

このように、生成されたインスタンスは、メソッドを参照しに行く性質があるからこそ、includeの役割が果たされるのだと理解することができました。

まとめ

  1. includeは、記述したクラスにそのモジュールのインスタンスメソッドを利用させることを許可(?)している
  2. Rubyのメソッドは、インスタンスには属さず、クラスに属している。その代わり、インスタンスはメソッドを探しに行くという性質を持つ。
  3. 2より、includeが成り立つのはrubyのメソッドの性質が寄与していることがわかる

おまけ

Githubを確認したところ、

model.rb
module Model
    extend ActiveSupport::Concern
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations  #ActiveModelから、Validationsをincludeしている
    include ActiveModel::Conversion
(中略)
end

このことから、バリデーションを設定することも可能になります。

example.rb
class QuestionAnswer
  include ActiveModel::Model   

  validates :answer, presence: true

ActiveModel::Modelも、上位のモジュールをincludeしているのですねぇ。

感想

このincludeが理解できたことで、今まで形式的に覚えていたことが芯から理解できるようになる気がします。こういう根本を知っていくって、楽しいですよね。

最後に

最後まで読んでいただき、ありがとうございます。
ソースコード、記事の書き方について「もっとこうしたほうがいいよ!」というご意見、「そこどうなっているの?」というご質問など、お待ちしております。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue + Rails】422 (Unprocessable Entity) 新規ユーザーの作成ができない原因と解決方法について

原因

  • CSRF保護がオフになっていたため

CSRF(Cross-Site Request Forgery)

悪意のあるユーザーがサーバーへのリクエストを捏造して正当なものに見せかけ、認証済みユーザーを装うという攻撃手法

このエラーが怒った原因として、Vue側(フロント)から送られてきたPOSTリクエストの中に含まれるCSRFトークン。そして、Railsはページのトークンとcookie内のトークンが同一のものではないと判断されたがために起こってしまったエラーと考えられます。

Vue + Railsを使用して作成したがため外部からのPOSTによってエラーが出たのではないかと思います。

解決(結論)

結論から申しますと、僕の場合はcotrollers/user_controller.rb
CSRF(Cross-Site Request Forgery)の対策用のコードを1行足すだけでした。

cotrollers/user_controller.rb
class UsersController < ApplicationController
  protect_from_forgery # 追記

  
  ・(略)
  
end

解決方法

解決に至った手順についてです。

①ディベロッパーツールにて、422 (Unprocessable Entity)を確認

スクリーンショット 2021-03-04 11.44.34.png

②ディベロッパーツールのNetworkタブ→JSでフロント側の確認

スクリーンショット 2021-03-04 11.47.10.png

スクリーンショット 2021-03-04 11.48.20.png

フロント側は大丈夫そう

③続いて、XHRタブ

スクリーンショット 2021-03-04 11.48.58.png

XHRタブは、非同期通信でリクエストを送っているリソースを確認できるみたいです。
スクリーンショット 2021-03-04 11.50.41.png

ここに422エラーがありました。

④色々、触ってみる。(ここでは)usersをタップ

スクリーンショット 2021-03-04 11.52.37.png

するとこんな画面が。

Previewタブをみてみると・・・

⑤ActionController::InvalidAuthenticityToken

スクリーンショット 2021-03-04 11.53.38.png

おお!見慣れたRailsのエラーが!!!!

ここで、「なるほど、CSRF保護がオフになっているのか」と解決まで辿り着きました。

最後に。今回学んだこと

ログや、ディベロッパーツールを舐めてはいけないこと思い知らされました。

恥ずかしながら、エラー発生時、ネットワークタブなどこれまで見向きもしていませんでした。
しかし今回、色々ディベロッパーツールをあさりながら解決に至ることが出来たのも事実です。

Networkタブ。活用していきます。

参考

【Rails API】ActionController::InvalidAuthenticityTokenの解決方法

RailsのCSRF保護を詳しく調べてみた(翻訳)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】flashとflash.nowの使い分けと理屈について【Ruby】

Railsチュートリアルで使われている2種類のメッセージ表示メソッドflash[]とflash.now[]。

どちらもメッセージを表示するという結果は変わらないですが、処理によってうまく動作しなかったりするので、それぞれの使い分けついて書いていきます。

flash[]とflash.now[]

結論から言うと、下記になります。

flash[]:
 redirect_to 使用時(データの追加、更新、削除を行いたいとき)

flash.now[]:
 render 使用時(データの取得を行いたいとき)

なぜこのような使い分けをするかというと、次の画面を表示するためのrenderとredirect_toによって処理の仕方が異なり、その処理の違いに関連してflash[]が表示され続けてしまったり、表示されなかったりします。

なぜrenderとredirect_toをそもそもなぜ使い分けるかを、ざっくり書いていきます。

renderとredirect_to

render:
・そのままviewを出力するメソッド。再度リクエストを投げない。
・route→controller(render)→view

redirect_to:
・再度リクエストを投げなおし、ルーティングからもう一回処理をし直す。
・route→controller(redirect_to)→route→controller→view

どちらも結果としては画面遷移のためのメソッドですが、redirect_toは再度リクエストを投げる処理をしています。

これは別の画面を呼び出したいときに、再度リクエストを投げてその画面のアクションを呼び出しなおすことで、処理の無駄をなくさせるためです。

renderは別画面を描画するだけで、別画面のアクション内の処理を実行しません。
そのため、renderで別画面の処理を正常に表示させたい場合、別画面のアクションの処理をrender前で処理したうえで、renderを実行する必要があります。

そういった、別の画面を呼び出す際に、わざわざrednderの前にその画面用の処理を書くより、その別画面用のアクションをredirect_toで呼び出してしまえば、処理を2重に書かずに済み、ラクチンです。

しかし、redirect_toでルーティングをしなおした際に、保持していたインスタンス変数がリセットされてしまいます。

そのため、
・フォームで送信失敗時に情報を残しておきたい時はrenderでフォームに値を入れる
・データ更新をして、別画面に遷移させる際はredirect_toを使う
といった使い分けになります。

このredirect_toとrenderによって、再度リクエストされてアクションが呼びだされるかが、ここでは重要です。

flashとflash.now[]の使い分け

renderとredirect_toの説明が長くなりましたが、flash[]とflash.now[]に立ち戻ります。

各メソッドのflashメッセージの消失トリガーは、
flash[]
 次次アクション終了時に削除

flash.now[]
 次アクション終了時に削除

よって、renderやredirect_toと組み合わせた場合、(redirect_toとrender, flashとflash.nowの違いから引用)

flash[]とredirect_to => 次のページにリダイレクトした時点でflashは消える
flash[]とrender => renderはリクエストを送信しないため、次のページに移動してもflashは残る
flash.now[]とredirect_to => redirect_toの時点でflashが消えるため、flash自体表示されない
flash.now[]とrender => 次のページへリダイレクトした時点でflashは消える

となります。

したがって、遷移した一回のみ表示させたい際の適切な使い分けは、

flash[]:
 redirect_to 使用時(データの追加、更新、削除を行いたいとき)

flash.now[]:
 render 使用時(データの取得を行いたいとき)



nowがついているほうは「今リクエスト」だけ表示すると認識すると覚えやすいですね。

間違っていたらコメントいただけると幸いです。

参考:
Railsドキュメント 簡単なメッセージを画面に表示
renderとredirect_toの違いを整理 【Day 1/30 2nd】
redirect_toとrender, flashとflash.nowの違い
Railsガイド 2.3 redirect_toを使用する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む