- 投稿日:2021-03-04T23:01:06+09:00
renderするとURLが変わり、リロード問題に直面するやつ
はい!僕です!!
railsでポートフォリオを作成しています。
表題の問題にぶち当たり半日消耗したので、記録しておく。
環境
ruby2.7
rails6.0問題の詳細
例えばblogs_controllerのcreateアクションを実装するとして、バリデーションが失敗するとrenderで新規投稿画面をrenderするコードを書きます。
blogs_controller.rbdef 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.rbdef 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.jshistory.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
- 投稿日:2021-03-04T22:47:28+09:00
bundler: not executable: bin/railsエラーがwheneverでパッチ処理しようとして出た時の対処法
wheneverを用いたパッチ処理の練習中のエラー。
log/cron.logbundler: not executable: bin/rails bundler: not executable: bin/rails bundler: not executable: bin/rails ・・・続くと同じエラーが続き、超初心者につきよくわからず悩んで途方に暮れていた時。。。
解決方法を見つけました!!!
ターミナル$ bundle exec rake app:update:binをしてひたすらエンターエンターエンター
で解決しました!!!やった!!!!
- 投稿日:2021-03-04T21:24:45+09:00
[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画面作りたくてこの書き方をしました。
あまり良くない書き方の場合は、ご指摘いただけますと幸いです。
こちらの記事を参照させていただきました。
- 投稿日:2021-03-04T21:22:42+09:00
RSpecにて、引数が必要なsubjectの使い方
はじめに
RSpecで、同じメソッドに対して同様のテストを複数回書くシーンでは、subjectでDRYなコードを書くことができます。
ここでは引数が必要なメソッドのテストをsubjectを用いて書きます。実装
ユーザー名(user_name)、自己紹介(pr)カラムがあるユーザーを、フリーワードで検索するuser_search(free_word)メソッドをテストするとします。以下のように繰り返し同じ処理が出てきます。
spec/models/user_spec.rbdescribe "#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.rbdescribe "#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を入れます。
Gemfilegroup :development, :test do #省略 gem 'rspec-its' end$ bundle installこれで、上記のコードが動作します。
- 投稿日:2021-03-04T20:56:04+09:00
【超かんたん】Active Storageで画像機能を実装しよう!
Active Storageを利用して画像を機能を実装します。
今回も初心者向けにレシピ投稿アプリを例に作成していきます。完成イメージ
Active Storageとは
ファイルアップロード機能を簡単に実装できるGem。Railsガイド
事前準備
ImageMagickの導入
ImageMagickとは
ImageMagickとは画像処理ライブラリです。
コマンドラインから画像に処理を加えることができるツールです。
Homebrewからインストールするので一度構築すれば次回以降この作業は不要になります。ターミナルbrew install imagemagick必要なGemのインストール
MiniMagickとImageProcessingをインストールします。
MiniMagickとは
mageMagickの機能をRubyで扱えるようにするためのGem。公式ドキュメント
ImageProcessingとは
画像サイズを調整するためのGem。公式ドキュメント
Gemfilegem 'mini_magick' gem 'image_processing'ターミナルbundle installActive 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.rbclass Recipe < ApplicationRecord has_one_attached :image endコントローラーの編集
画像の保存を許可するストロングパラメーターにしましょう。
app/controllers/recipes_controller.rbclass 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 %>保存されているか確認してみましょう。
ターミナル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.rbclass 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.erbshow.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"> #以下略
- 投稿日:2021-03-04T20:56:04+09:00
【超かんたん】Active Storageで画像投稿機能を実装しよう!
Active Storageを利用して画像を機能を実装します。
今回も初心者向けにレシピ投稿アプリを例に作成していきます。完成イメージ
Active Storageとは
ファイルアップロード機能を簡単に実装できるGem。Railsガイド
事前準備
ImageMagickの導入
ImageMagickとは
ImageMagickとは画像処理ライブラリです。
コマンドラインから画像に処理を加えることができるツールです。
Homebrewからインストールするので一度構築すれば次回以降この作業は不要になります。ターミナルbrew install imagemagick必要なGemのインストール
MiniMagickとImageProcessingをインストールします。
MiniMagickとは
mageMagickの機能をRubyで扱えるようにするためのGem。公式ドキュメント
ImageProcessingとは
画像サイズを調整するためのGem。公式ドキュメント
Gemfilegem 'mini_magick' gem 'image_processing'ターミナルbundle installActive 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.rbclass Recipe < ApplicationRecord has_one_attached :image endコントローラーの編集
画像の保存を許可するストロングパラメーターにしましょう。
app/controllers/recipes_controller.rbclass 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 %>保存されているか確認してみましょう。
ターミナル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.rbclass 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.erbshow.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"> #以下略
- 投稿日:2021-03-04T20:37:44+09:00
【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に何も値が代入されていなかったために発生していた様です。
- 投稿日:2021-03-04T18:25:08+09:00
【未解決事件】バルクインサートでユニーク制約に引っ掛かるとIDが飛び飛びになる問題
本記事を読むその前に
本記事は事象を分かりやすくお伝えするため、サンプルアプリを題材として執筆しております。
実際は大規模システムにおいて大量の初期データを準備する必要があり、
その際に直面した問題から本記事を書くに至りました。
(@jnchitoさん、ご指摘いただきありがとうございます?♂️)結論
ユニーク制約のあるテーブルにバルクインサートを繰り返すと
ID(主キー)が飛び飛びになることがある。⬆︎
mahjan_pies
テーブルのIDが飛び飛びに、、、。
⬇︎ こんな結果を期待してたのに。何をしたいのか
麻雀の座席をランダムに決めるための簡単なプログラムを考える。
麻雀では【東南西北中】の中から1つを選び、引いた牌通りの場所に座る。
【東南西北】は下図のように席に座り麻雀を打つことが出来るが、中は見学となる。
(1半荘休み)そのため、序盤は中が嫌悪されるが、後半は誰もが中を懇願することとなる。
(徹麻による体力の限界)ちなみに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.rbdef 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:seedterminal====== バルクインサート終了... ====== 座席、もしくは見学が決まっているのは2人だ 残る牌は【東 南 中】のみ! ... .. . ====== バルクインサート終了... ====== 座席、もしくは見学が決まっているのは3人だ 残る牌は【東 南】のみ! ... .. . ====== バルクインサート終了... ====== 座席、もしくは見学が決まっているのは4人だ 残る牌は【東】のみ! ... .. . ====== バルクインサート終了... ====== 座席、もしくは見学が決まっているのは5人だ さて、今夜飛ぶのは誰かな?そしてテーブルの中身は以下の通り
terminalusersテーブル +----+--------------+----------------------------+----------------------------+ | 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 | +----+------+---------+----------------------------+----------------------------+
つまりこのような席順になった。
(じゅーり、ドンマイ?)これでいよいよ麻雀を打てるわけだが、ちょっと待ってほしい。
テーブルに登録されたレコードをもう一度よく見てると、
バルクインサートをしたmahjan_pies
テーブルのid
が連続していない?
これは一体、、、??????発行されたクエリを確認する(SQL)
Railsの開発環境でのログは
log/development.log
に記載されているので確認する。log/development.logINSERT 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.logINSERT 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は増やしますぜ旦那、と。
※詳しくはリンク先に飛んで読んでほしいが、該当箇所は以下の通り。自動インクリメントカラム、つまりID(主キー)だ。。
ON DUPLICATE KEY UPDATE 構文を使えばユニーク制約が破られることはないが、
ユニーク制約に引っかかったデータの分だけIDが加算されてしまうらしい。。
何てこったい。つまりこういうこと
各クエリごとに切り分けて見ていく。
最初のクエリでは5つのレコードを登録しようとしている。ただし、
name
カラムとuser_id
カラムにユニーク制約がかかっているため、
登録出来たのは最初の2つのレコードだけ。ID 1, 2が振られる。残り3つのレコードは登録出来ず、かつIDも加算されてしまっている。
そのためID 3, 4, 5は使えなくなった。
次のクエリは3つのレコードを登録しようとしている。
最初のレコードはユニーク制約を受けず登録出来た。これがID 6になる。
残り2つのレコードは登録出来ず、かつID 7, 8も使えなくなった。
次のクエリは2つのレコードを登録しようとしている。
最初のレコードは登録できた。これがID 9。
残り1つのレコードは登録出来ず、ID 10も使えなくなった。
そして最後に登録されたレコードがID 11。
先ほども示したが、その結果がこれになるわけだ。なるほど理解できた。terminalmahjan_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.rbwhile 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が飛び飛びにならずにバルクインサートできる方法を知っている方!
もしいたらぜひ教えてください?
- 投稿日:2021-03-04T16:37:22+09:00
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を完全入門するで!! - Qiita4-1. sorcery gem導入、Userクラスをつくる
Gemfileにて以下を追加:
gem 'sorcery' gem 'bcrypt'↓これに準拠して進めます
Sorcery/sorceryコマンドを実行:
bundle install bundle exec rails g sorcery:installFile 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 endUserモデルのバリデーション設定を追加する。
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 updateapp/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 endapp/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_messageconfig/locales/model.ja.yml
config/locales/views/users/ja.yml
config/routes.rb
をいじる(コードは省略、リポジトリをみてください)4-3. セッション機能
rails g controller UserSessions new create destroyapp/controllers/application_controller.rb
class ApplicationController < ActionController::Base before_action :require_login private def not_authenticated redirect_to login_path, alert: 'ログインしてください。' end endrequire_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 endlogin(メアド, パスワード)でログインできる。
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 end4-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
- 投稿日:2021-03-04T16:22:55+09:00
【テストコードで使用するFakerについて】
テストコードを実行する際、FactryBot内で活躍するFakerについて、
ちょっとした学びがあったので備忘録として残す。Fakerは無作為に半角数字、半角英語の組み合わせを作ってくれるもの。
パスワードのバリデーションとして、「英数字を含む」という設定が多々見受けられると思う。
基本的にはFakerをそのまま使えば問題ないが、稀に数字のみ、もしくは英語のみという組み合わせになることもある。
そうなってしまうと、バリデーションではじかれてしまうことになる。そこでFakerに"1a"などの文字列をくっつけてやる。
そうすることで必ず"1a"という数字、英語を含む組み合わせを作ることができる。{'1a' + Faker::Internet.password(min_length: 6)}
このような形とする。
- 投稿日:2021-03-04T15:45:23+09:00
fixtureをrails_helperでまとめて読み込む
- 投稿日:2021-03-04T14:37:49+09:00
結合テストコードの書き方(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' endGemfileを確認すると、Capybaraが導入されていることがわかります。
Gemが導入されていることがわかったので、次はターミナルよりファイルを作成します。
% rails g rspec:system userssystem/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: '入力する文字列'のように記述することで、フォームへの入力を行うことができます。
フォームの名前の入力には検証ツールを使用します。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と記述することで、実際にクリックができます。
つまりフォームを入力した後に、登録完了ボタンを押すということです。
これも検証ツールで確認することができます。・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とは逆でないことを確認します。以上が一部ですが、結合テストコードの書き方です。
色々書きましたが、これらは調べれば出てくるので
暗記する必要はなく大事なのは、意味を理解することだと学びました。もし何か間違った記述や気になることありましたらコメント残して頂ければ幸いです。
- 投稿日:2021-03-04T14:21:50+09:00
【Rails】コピペされたソースコードから英単語のみを抽出し、初出の単語のみ保存するプログラムを組んでみた
初めに
この記事は、上記記事で紹介したポートフォリオで使用した技術を切り出した記事になります。もし宜しかったらこちらもご覧ください。
イメージ図
上記画像のように、コピペしたソースコードから英単語を抽出し、初出の英単語のみ保存するプログラムを組んでみました。ポートフォリオのタイピングアプリにおいて必須になる機能です。
開発環境
- Ruby 2.6.5
- Rails 6.0.3
実際に組んだプログラム
models/form.rbdef 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.rbstrings = 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.rbstrings = code.split(/[\W|\d|\s]+/).uniq.select { |str| str.length != 1 }まず、このフォームオブジェクトではコピペしたソースコードの内容を変数
code
に格納しています。サーバーのログを見てみると、
上記画像の赤い印をつけた部分のようにデータが飛んできていることが分かります。それでは、ここから半角英単語のみ抽出していきましょう。この場合は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.rbstring = "hoge1huga.ho@ga ho ge" array = string.split(/[\W|\d|\s]+/)上記のような記述の場合、
string ="hoge[区切]huga[区切]ho[区切]ga[区切]ho[区切]ge"
のように、記号や数値・空白文字を[区切]
に変換して文字列が格納された変数string
を分割します。その結果、hoge.rbp 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.rbstrings = 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.rbstrings.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つ着実に習得していきたいです。
- 投稿日:2021-03-04T13:01:08+09:00
【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導入
下記のように
Gemfile
のgroup :development, :test do
にgem 'rspec-rails', '~> 4.0.0'
と記述します。Gemfilegroup :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.rbrequire '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.rbcontext '新規登録できる時' 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)
で@user
がbe_valid
で正しいかどうかを判断しています。expect(@user).to be_valid次は「nameが6文字以下であれば登録できる」についてです。
@user.name
にabcde(つまり6文字以下の文字列)
を代入して、そのデータが入った@userをbe_valid
で正しいかどうかを判断しています。@user.name = 'abcde' @expect(@user).to be_valid次に「passwordとpassword_confirmationが6文字以上であれば登録できる」についてです。
nameと同じように@user.password
に123456
を代入し、そのデータが入った@userをbe_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の引数に与えることで一致する(含まれる)ということになります。こんな感じで記述していきます。
以上です。
- 投稿日:2021-03-04T12:04:13+09:00
Rubyのincludeを理解したい②
書くこと
先日書いた、こちらの記事の続きとなります。
前回の記事の概要をまとめると
Rubyのクラスとインスタンスの関係から、メソッドはどのように扱われているのか?
ということをまとめました。
今回の記事は、前回の内容を元に、Rubyのモジュールから本題のincludeについて、説明していきます。
includeとは?
早速ですが、Rubyのincludeについて少し触れていきましょう。
実際に見てもらったほうが早いので、まずはこちらをご覧ください。example.rbclass QuestionAnswer include ActiveModel::Model #ActiveModel::Modelというモジュールをincludeしています。 endこのように、
QuestionAnswer
というクラスに、ActiveModel::Model
というモジュールをincludeしています。こうすることで、クラスにinclude モジュール名と記述することで、そのモジュールのインスタンスメソッドをクラスで使用出来るようになります。
これが、includeの大枠の役割となります。
includeとメソッドの旅
以上のincludeの役割と踏まえると、このクラスから生成されたインスタンスは、includeされたmoduleのところまでメソッドを探しに行けるというということが言えそうです。
この話は、前回の記事の最大のテーマ
インスタンスはメソッドを持つのではなく、生成されたクラスに参照しに行く性質を持つ
ということにつながってきます。このことを、もう少し端的な例で見てみましょう。
example.rbModule 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
に代入されています。
そして、Ceta
はModule Alpha
で定義されたインスタンスメソッドmodule_a_method
を利用しています。通常、スコープの性質上、あるクラスから生成されたインスタンスはそのクラス内に記述されたインスタンスメソッドのみを用いることができます。しかし、今回はモジュールをincludeしているので、そこで定義されている
module_a_method
を利用することができます。これは、以下の図で理解してもらうと良いかもしれません。
このように、生成されたインスタンスは、メソッドを参照しに行く性質があるからこそ、includeの役割が果たされるのだと理解することができました。
まとめ
- includeは、記述したクラスにそのモジュールのインスタンスメソッドを利用させることを許可(?)している
- Rubyのメソッドは、インスタンスには属さず、クラスに属している。その代わり、インスタンスはメソッドを探しに行くという性質を持つ。
- 2より、includeが成り立つのはrubyのメソッドの性質が寄与していることがわかる
おまけ
Githubを確認したところ、
model.rbmodule Model extend ActiveSupport::Concern include ActiveModel::AttributeAssignment include ActiveModel::Validations #ActiveModelから、Validationsをincludeしている include ActiveModel::Conversion (中略) endこのことから、バリデーションを設定することも可能になります。
example.rbclass QuestionAnswer include ActiveModel::Model validates :answer, presence: true
ActiveModel::Model
も、上位のモジュールをincludeしているのですねぇ。感想
このincludeが理解できたことで、今まで形式的に覚えていたことが芯から理解できるようになる気がします。こういう根本を知っていくって、楽しいですよね。
最後に
最後まで読んでいただき、ありがとうございます。
ソースコード、記事の書き方について「もっとこうしたほうがいいよ!」というご意見、「そこどうなっているの?」というご質問など、お待ちしております。参考文献
- 投稿日:2021-03-04T11:59:24+09:00
【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.rbclass UsersController < ApplicationController protect_from_forgery # 追記 ・ ・(略) ・ end解決方法
解決に至った手順についてです。
①ディベロッパーツールにて、422 (Unprocessable Entity)を確認
②ディベロッパーツールのNetworkタブ→JSでフロント側の確認
フロント側は大丈夫そう
③続いて、XHRタブ
XHRタブは、非同期通信でリクエストを送っているリソースを確認できるみたいです。
ここに422エラーがありました。
④色々、触ってみる。(ここでは)usersをタップ
するとこんな画面が。
Previewタブをみてみると・・・
⑤ActionController::InvalidAuthenticityToken
おお!見慣れたRailsのエラーが!!!!
ここで、「なるほど、CSRF保護がオフになっているのか」と解決まで辿り着きました。
最後に。今回学んだこと
ログや、ディベロッパーツールを舐めてはいけないこと思い知らされました。
恥ずかしながら、エラー発生時、ネットワークタブなどこれまで見向きもしていませんでした。
しかし今回、色々ディベロッパーツールをあさりながら解決に至ることが出来たのも事実です。Networkタブ。活用していきます。
参考
- 投稿日:2021-03-04T11:50:48+09:00
【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)→viewredirect_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を使用する