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

【初心者向け】rails 投稿機能解説!form_withやストロングパラメータを図で理解!

はじめに form_withをつかった投稿機能は、Rails 初学者がつまづきやすいポイントかと思います! 本記事では form_withを使った入力フォームの作成 、ストロングパラメータ、データの保存までを解説していきます。 図を織り交ぜていますので、イメージをつかみつつ理解を深めていきましょう 動作環境 Ruby 2.6.7 Rails 5.2.7 (動作としてはrails6系でも同様です) 実装する機能・仕様 フォームでタイトルと内容を入力し、データベースに保存する。 Blogモデルにtitle、bodyカラムを準備。 form_withを用いて入力データを送信する new.html.erbにてフォームを以下のように作成していきます。 blogs_controller.rb def new @blog = Blog.new end new.html.erb <h1>Blogs#new</h1> <%= form_with model: @blog, local: true do |f| %>  <!--タイトル入力フォーム生成--> <%= f.label :title %><br> <%= f.text_field :title %><br>  <!--内容入力フォーム生成--> <%= f.label :body %><br> <%= f.text_area :body %><br>  <!--投稿ボタン生成--> <%= f.submit %> <% end %> 実行画面 入力フォームのイメージ Blog.newの記述がありますが、これはBlogの新しい箱を作るイメージになります。このBlogの箱の中には空のtitle、bodyの箱を持っています。この空の箱をインスタンス変数@blogに格納して、フォームヘルパー(form_with)に渡していきます。 フォームヘルパーとは フォームヘルパーは入力フォームを簡単に作成できるように、あらかじめメソッド(処理)を用意してくれているものです。 f.text_fieldやf.text_area、f.submitなどがその例です。フォームをヘルパーなしで書こうとするとなかなか面倒になるので、とてもありがたい存在です 文字や値を入力したのちにCreate Blogのボタンを押すと、フォームヘルパーは入力値を該当する箱に入れていきます。 この作業を経てパラメータの一つが生成され、次にcreateアクションへ送信していきます。イメージとしては荷物パッキングをして次のアクションまで配送するイメージです。 パラメータとは ここで、パラメータについて少し触れます。 パラメータとは基本的にはアクション間で受け渡しされるデータのことを指します。 どのような情報が渡されているのかはターミナルで確認することができます。titleに"hoge"、bodyに"fuga"を入力してターミナルを確認すると以下のようになります。(一部抜粋) terminal Started POST "/blogs" for ...... : Processing by BlogsController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"..", "blog"=>{"title"=>"hoge", "body"=>"fuga"}, "commit"=>"Create Blog"} : : 今回はこの中でも Parameters: {"utf8"=>"✓", "authenticity_token"=>"..", "blog"=>{"title"=>"hoge", "body"=>"fuga"}, "commit"=>"Create Blog"} がパラメータになります。 『"〇〇"=>"××"』を一つのまとまりと考えます。上記を箱のイメージであらわすと となります。箱の中にそれぞれ矢印の右辺の情報が入っているイメージです。 このように、先ほど入力した情報がBlogの箱の中のtitleとbodyに格納されて、送信されていることが確認できます。 フォームの送信後createアクションでの挙動 ここからはcreateアクション内での処理を説明していきます。 blogs_controller.rb def create # ストロングパラメータで入力された値を取得 @blog = Blog.new(blog_params) # 保存 @blog.save # 保存後詳細ページに遷移 redirect_to blog_path(@blog) end private # ストロングパラメータ def blog_params params.require(:blog).permit(:title, :body) end 送られてきた箱をそのまま保存したい!!となりますが、railsではアクションごとに新しい箱を作成して移し替える作業を行う必要があります Blog.new(blog_params)のBlog.new()関しては前章の考え方と同様に、空の箱を作るイメージです。 かっこの中身は空の箱の中に、値をはじめから入れたいときに入力します。 ストロングパラメータ 今回はフォームで入力した値を空の箱に入れていきたいので、ここでストロングパラメータのblog_paramsを呼び出しています。 params.require(:blog).permit(:title, :body)は3つに分割して考えていきます。 params パラメータ全体を呼び出します。 .require(:blog) パラメータの中で、どの要素(大きな箱)を選択するかを宣言する。今回の場合、4つの箱(utf8, authenticity_token, blog, commit)の中からblogを選択するという意味になります。 .permit(:title, :body) 選択した要素(大きな箱)内に含まれるパラメータの中でどの情報の保存を許可するのかを選択します。今回の場合、blogの箱の中にあるtitleとbodyについて保存するという意味になります。 ストロングパラメータはなぜ必要? ストロングパラメータを設定することで、不要な情報が保存されてしますことを防ぎます。 仮に悪意のあるユーザーがtitle、body以外の箱を自分で作って送信してきたとします。ストロングパラメータを設定していなかった場合、データベースにはそのカラムは存在していないので、その情報を保存するときにエラーを吐いてしまいます。ストロングパラメータで、必要な情報のみを選別しているといことになります。システムの根幹にもなるので、他の場所から呼び出して書き換えができないようにprivate下に書くことが一般的です。 これらをまとめると、Blog.new(blog_params) は『送られてきたパラメータの中から、blogの中のtitleとbodyを取り出して、新しい箱に入れる』ということになります。したがって、createアクションのイメージを描くと以下のようになります。 これでフォームの入力~データの保存が完了です! おわりに 今回はフォームの入力から保存までの流れを図メインでまとめてみました! イメージはつかめましたでしょうか? 少しでも参考になればと思います
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

api/v1をエンドポイントにしたルーティング

名前空間を使ったルーティング 最初から下の様にバージョンで名前空間を作成しておくことで今後のAPIのバージョン管理が簡単になる。 routes.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :articles # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end end end rails routrs |grep api/v1で確認 api_v1_articles GET /api/v1/articles(.:format) api/v1/articles#index POST /api/v1/articles(.:format) api/v1/articles#create api_v1_article GET /api/v1/articles/:id(.:format) api/v1/articles#show PATCH /api/v1/articles/:id(.:format) api/v1/articles#update PUT /api/v1/articles/:id(.:format) api/v1/articles#update DELETE /api/v1/articles/:id(.:format) api/v1/articles#destroy
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

binding.pryを使えるようになりたい!2

はじめに 本記事は、昨日投稿した続きで、 「binding.pry」の使い方をまとめたものになります。 前回の投稿もご参照ください。 「binding.pry」の目的 たとえ、「binding.pry」が万能だとしても、 確認できることは限られています。 目的(私が思いつくものです。他にもあると思います。) ①paramsの中身の確認 ②「binding.pry」の後に、if文があれば、true or falseの指示が出せているかどうか ③エラー内容の確認 ④インスタンス変数の中身の確認 結論としては、 何かの中身や結果が期待するものかどうかを調べたい時 エラーの原因はどこにあるのかを探したい時 が目的となります。 何ができる? ここのコードが間違えているため、このように修正しなさい!! という直接的にエラーを解決できるものではありません。 ここではないかという目星をつけ、 「ここは間違えていない!ここも間違えていない!ここは間違っている!」 という風に、選択肢を潰していく このようなことができます。 指定した場所でエラーが起きるかどうかを確認するものです。 どこで使用できる? コントローラーでもビューでもモデルでも使用できます。 特にモデルでは、自作のメソッドが役割を果たせているかを確認する際に、 「binding.pry」は多く使用されます。 また、試したいコードの部分の手前に「binding.pry」を置きましょう。 エラー内容の確認 class OrdersController < ApplicationController def index @order = Order.new end def create @order = Order.new(order_params) binding.pry if @order.valid? @order.save return redirect_to root_path else render 'index' end end private def order_params params.require(:order).permit(:price) end end 前回投稿したものを引用しますと、 if @order.valid? から「binding.pry」はエラーを調べることができます。 エラーがあれば「false」、なければ「true」ということは、前回に投稿しました。 「false」だった場合で、エラーの内容を確認したい時は、 errors.full_messagesを記述しましょう。 [1] pry(#<OrdersController>)> @order.valid? => false [2] pry(#<OrdersController>)> errors.full_messages => Price can't be blank エラーが実際に画面に表示されたときも、 「Price can't be blank」のような文章が現れるはずです。 errors.full_messages についてはよく使用するので、ここは暗記でも良いのでは? と思い、私は暗記しました。 終わり 昨日に引き続き「binding.pry」の深堀を行いました。 何一つわからなかった昨日とは大きく成長したように思えます。 今回のように理解していなく、不安な要素は、 理解した上で、深堀することで、 苦手から得意になると私はそう思います。 どんどん得意を増やします。 引き続きがんばりましょう!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの環境構築

はじめに 環境構築はPCを購入した後、一回やれば何度も毎回やる必要のない作業です。 ですので全く作業工程を覚えられる気がしないので、私が初めて行ったRailsで開発するための環境構築を忘れないうちに、ざっと書いておきます。 ちなみにMacでiOSはCatalina以降のやり方になります。 よろしくお願いします。 Command Line Toolsをインストール まず、Command Line Toolsを入れる前に下記のコマンドをターミナルで実行します。 ターミナル # zshをデフォルトに設定 % chsh -s /bin/zsh # ログインシェルを表示 % echo $SHELL # 以下のように表示されれば成功 /bin/zsh もしパスワードを求められたら、PCのパスワードを入力します。 さあ、Command Line Toolsをインストールします。Command Line ToolsとはWebアプリケーション開発に必要なソフトウェアをダウンロードするために必要となる機能です。 下記のコマンドをターミナルに打ち込みます。 ターミナル % xcode-select --install するとホップアップが出てくるのでインストールをクリック →仕様許諾契約が出てきたら「同意する」をクリック するとCommand Line Toolsのダウンロードが開始します。 完了したら次に進みます。 Homebrewをインストール ただこのHomebrewのインストールの前にMacのチップがM1チップの場合下記の設定を行っておくことをおすすめします。 M1チップの場合 ①Finderを開けて「ターミナル」を検索 ②ターミナルアプリを2本指でタップ、もしくはマウスの場合は右クリックでメニューを表示させる。 ③「情報を見る」をクリック ④「Rosettaを使用して開く」にチェックを入れる ⑤一度ターミナルを終了して、もう一度起動させる ここまでできたら、あとはIntelチップの方と同じように進めていきます。 それではHomebrewをインストールしていきます。(Intelチップの人はここから作業する) 下記のコマンドを一つずつ実行していきます。 ターミナル % cd # ホームディレクトリに移動 % pwd # ホームディレクトリにいるかどうか確認 % /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" # コマンドを実行 処理に時価がかかる可能性があるので慌てず待ちます。 パスワードの入力を求められたらPCのパスワードを入力します。 入力しても文字が入力されていないように見えるかもしれませんが、入力はされています。 入力できたらエンターキーを押します。 「Press RETURN to continue or any other key to abort」という文章が出てきたらエンターキーを押します。 プロンプトが現れ入力待ち状態になれば成功です。 下記コマンドでインストールできているか確認します。 ターミナル % brew -v Homebrew 2.5.1 # 数字は異なる場合があります。 下記コマンドでバージョンを最新にします。 ターミナル % brew update 下記のコマンドで権限を変更します。 ターミナル % sudo chown -R `whoami`:admin /usr/local/bin 最新バージョンのRubyをインストール まずはRubyの土台となる、rbenvとruby-buildをHomebrewを用いてインストールします。 ターミナル % brew install rbenv ruby-build rbenvをどこからでも使用できるように下記コマンドを実行します。 ターミナル % echo 'eval "$(rbenv init -)"' >> ~/.zshrc このコマンドでzshrcという設定ファイルを変更しました。 このファイルの修正内容を反映させるために、下記コマンドで再度読み込みをさせます。 ターミナル % source ~/.zshrc 続いてターミナルのirbで日本語入力を可能にする設定を行うために下記コマンドで「readline」をインストールします。 ターミナル % brew install readline readlineもどこからでも使用できるようにします。 ターミナル % brew link readline --force rbenvを使ってRubyをインストールします。 今回は2.6.5のバージョンのRubyをインストールします。 ターミナル % RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)" % rbenv install 2.6.5 このコマンドは10分程度かかる場合があるコマンドなので気長に待ちましょう。 私は心配になって変に触って失敗しました。 インストールしたRuby 2.6.5を使用するために下記コマンドを実行します。 ターミナル % rbenv global 2.6.5 これでバージョンの指定ができたのでrbenvを読み込んで、変更を反映させます。 ターミナル % rbenv rehash Rubyのバージョンを下記コマンドで確認します。 ターミナル % ruby -v 2.6.5が表示されればOKです。 MySQLをインストール 下記コマンドでMySQLを導入します。 ターミナル % brew install mysql@5.6 このコマンドも時間がかかります。 完了したら次は自動起動させる設定をします。 というのもPCを再起動するたびにMySQLは起動し直す必要があるためです。面倒なので自動で起動してくれるようにします。 ターミナル % mkdir ~/Library/LaunchAgents % ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents % launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plist 続いてmysqlをどこからでも実行できるコマンドmysqlを実行できるようにします。 下記コマンドを実行します。 ターミナル % echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.zshrc # mysqlのコマンドを実行できるようにする設定 % source ~/.zshrc # 設定を読み込むコマンド % which mysql # mysqlのコマンドが打てるか確認する # 以下のように表示されれば成功 /usr/local/opt/mysql@5.6/bin/mysql 下記コマンドでMySQLが起動しているか確認します。 ターミナル % mysql.server status # MySQLの状態を確認するコマンド # 以下のように表示されれば成功 SUCCESS! MySQL running shared-mime-infoをインストール 下記コマンドでshared-mime-infoをインストールします。 ターミナル % brew install shared-mime-info Railsをインストール まずはgemを管理するためのbundlerをインストールする必要があります。 下記コマンドを実行します。 ターミナル % gem install bundler --version='2.1.4' 続いて下記コマンドでRailsをインストールします。 ターミナル % gem install rails --version='6.0.0' このコマンドも時間がかかります。 下記コマンドでrbenvを読み込んで変更の反映をさせておきます。 ターミナル % rbenv rehash Railsが正常にインストールされているか下記コマンドで確認します。 ターミナル % rails -v Rails 6.0.0 # 「Rails」のあとに続く数字は変わる可能性があります Node.jsをインストールする 下記コマンドでNode.jsをインストールします。 Railsを動かすためにNode.jsは必要です。 ターミナル % brew install node@14 さらに下記コマンドでNode.jsへのパスを設定します。 ターミナル % echo 'export PATH="/usr/local/opt/node@14/bin:$PATH"' >> ~/.zshrc % source ~/.zshrc 下記コマンドでNode.jsがインストールされているか確認できます。 ターミナル % node -v v14.15.3 # 数値は異なる場合があります yarnをインストール yarnはプログラム同士の関係性を管理する役割で用いられるものです。 下記コマンドでyarnをインストールします。 ターミナル % brew install yarn 下記コマンドでインストールできているか確認します。 ターミナル % yarn -v これで完了です。 最後に あくまでこれは私が初めて環境構築をしたときにやった工程を記したものです。 次にPCを買い換える時まで使うことはないかなと思いますし、そのときでは少しやり方が違ってたりするかもしれませんね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby on rails]タグ付け機能② タグをリンクにしてタグで検索(複数タグ)

やりたいこと ①投稿に対して複数タグを付けれるようにする(別記事で実装済) ②タグをリンクにして、検索できるようにする。リンクになってるタグを押すと、 そのタグを付与してる記事一覧が表示される 複数タグの投稿 投稿一覧画面でタグの一覧と、記事数の表示 タグを押すと、検索ができ、そのタグがついてる投稿を検索できる 投稿詳細画面 ①の複数のタグをつけて記事を投稿する機能については以下の記事で行っています。 今回はその続きです。 ルーティング記述 route.rb # タグの検索で使用する get "search_tag"=>"posts#search_tag" コントローラー記述 posts_controller.rb def search_tag #検索結果画面でもタグ一覧表示 @tag_list=Tag.all        #検索されたタグを受け取る @tag=Tag.find(params[:tag_id])         #検索されたタグに紐づく投稿を表示 @posts=@tag.posts.page(params[:page]).per(10) end 表示してるタグをリンクにする index.html.erb <!--タグリスト--> <% @tag_list.each do |list|%> <%=link_to list.name,search_tag_path(tag_id: list.id) %> <%="(#{list.posts.count})" %><% end %> show.html.erb <!--タグ--> <% @post_tags.each do |tag| %> <%= link_to tag.name,search_tag_path(tag_id: tag.id)%><%="(#{tag.posts.count})" %><% end %> view(検索結果の表示 <div class="container"> <h5>タグが<%=@tag.name%>の投稿一覧</h5> <!--タグリスト--> <% @tag_list.each do |list|%> <%=link_to list.name,search_tag_path(tag_id: list.id) %> <%="(#{list.posts.count})" %><% end %> <% @posts.each do |post| %> <!--投稿はカードで囲む--> <div class="card"> <div class="card-body"> <!--タイトル表示される文字数は25文字までにしてます--> <h6 class="card-title"> <%=link_to post_path(post.id) do %><%= post.title.truncate(25) %> <% end %> </h6> <!--お気に入り、コメント数、投稿者は部分テンプレ--> <div id="<%=post.id %>"><%= render 'favorites/favorite', post: post %></div> </div> </div> <% end %> <div class="pagination pagination-sm justify-content-center"> <%= paginate @posts %></div> </div> <style> .card{ padding:0; margin:0; border:none; } h6 { /*線の種類(実線) 太さ 色*/ border-bottom: solid 3px black; } </style> 終わり 以上で完了です。 とても参考にさせていただきました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby on rails]タグ付け機能① 複数のタグ付け

やりたいこと ①投稿に対して複数タグを付けれるようにする ②タグをリンクにして、検索できるようにする。リンクになってるタグを押すと、 そのタグを付与してる記事一覧が表示される 複数タグの投稿 投稿一覧画面でタグの一覧と、記事数の表示 タグを押すと、検索ができ、そのタグがついてる投稿を検索できる 投稿詳細画面 タグを一個だけ付与するのは、別記事で投稿しました。 複数タグの実装はかなり難しかったのでまとめていきます。 ①テーブル定義 一応、以下の感じです。 タグ:投稿=1:N(複数) 投稿:タグ=1:N(複数) の多 対 多 になるので、 中間テーブルであるPostTagを用意して、それぞれの外部キーを持たせてます。 ①モデル/マイグレーションファイル作成 モデル作成 rails g model Tag name:string rails g model PostTag post:references tag:references マイグレーションファイル tagのマイグレーションファイル class CreateTags < ActiveRecord::Migration[5.2] def change create_table :tags do |t| t.string :name,null: false t.timestamps end end end post_tagsのマイグレーションファイル class CreatePostTags < ActiveRecord::Migration[5.2] def change create_table :post_tags do |t| t.references :post, foreign_key: true t.references :tag, foreign_key: true t.timestamps end # 同じタグを2回保存するのは出来ないようになる add_index :post_tags, [:post_id, :tag_id], unique: true end end add_index :post_tags, [:post_id, :tag_id], unique: true これは、複合キーインデックスで、この記述で同じタグを2回保存できないようにしています。 ここで、rails db:migrateして、続いてモデルファイルを記載します。 モデルファイル tag.rb class Tag < ApplicationRecord has_many :post_tags,dependent: :destroy, foreign_key: 'tag_id' # タグは複数の投稿を持つ それは、post_tagsを通じて参照できる has_many :posts,through: :post_tags validates :name, uniqueness: true, presence: true end 初めてthrough〜を見たときはなんのこっちゃでしたが、、だいぶ理解できるようになってきました! 中間テーブルは、”仲介役”という風に考えるようにしてから、何だかわかるようになってきた気がします。 post_tag.rb class PostTag < ApplicationRecord belongs_to :post belongs_to :tag validates :post_id, presence: true validates :tag_id, presence: true end validates〜_のところはPostとTagの関係を構築する際、2つの外部キーが存在することは絶対なので、 バリデーションを貼るそうですが、無くても問題ない気がしています(by初心者) これまでの中間テーブルでもvalidationつけてないので。 post.rb # タグのリレーションのみ記載 has_many :post_tags,dependent: :destroy has_many :tags,through: :post_tags ここからが難しかった・・ 何書いてあるかわからないので1行ずつ見ていきます。 ①コントローラー記述 post_controller.rb def create @post = Post.new(post_params) @post.user_id=current_user.id # 受け取った値を,で区切って配列にする tag_list=params[:post][:name].split(',') if @post.save @post.save_tag(tag_list) redirect_to posts_path(@post),notice:'投稿完了しました:)' else render:new end tag_list=params[:post][:name].split(',') なんかpaizaで勉強した記憶があるsplit..確か分割して配列を作ってくれるようやつだったように思います。 「splitとは」 1番目の引数に指定したパターンに従って文字列を分割し、分割された各部分文字列を要素とする配列を取得します。 @post.save_tag(tag_list) save_tagなんてそんなメソッドがあるのか・・・と思っていましたが違います!!!!! @post.saveの後に書いてあるから、紛らわしいですね・・ save_tagの働きについては、モデルファイルで定義してます。 タグで入力した値を配列に収めてる(tag_list)を引数としてモデルファイルに渡します。 ①問題のモデルファイル ここがすごく難しい。。。 def save_tag(sent_tags) # タグが存在していれば、タグの名前を配列として全て取得 current_tags = self.tags.pluck(:name) unless self.tags.nil? # 現在取得したタグから送られてきたタグを除いてoldtagとする old_tags = current_tags - sent_tags # 送信されてきたタグから現在存在するタグを除いたタグをnewとする new_tags = sent_tags - current_tags # 古いタグを消す old_tags.each do |old| self.tags.delete Tag.find_by(name: old) end # 新しいタグを保存 new_tags.each do |new| new_post_tag = Tag.find_or_create_by(name: new) self.tags << new_post_tag end end current_tags = self.tags.pluck(:name) unless self.tags.nil? unless~は「タグが存在してるか?」を確認しています。 ※投稿フォームでタグが入力されたか?を聞いてるわけではないので注意です。(ややこしいですが) タグが存在している場合には、current_tagsに、「タグの名前を配列として」全て取得します。 「plunk」とは?   簡単にいうとカラムの中身を展開してくれる https://qiita.com/k-o-u/items/31e4a2f9f5d2a3c7867f old_tags = current_tags - sent_tagsここでは、「今あるタグ」から「新たに送られてきたタグ」を引いて、 「old_tag」に代入しています。 例えば、既に 「ネズミ」 「お花」 「コアラ」 というタグが存在していて、新たに、 「お花」 「ユニコーン」 というタグが登録されたら old_tagには、「ネズミ」「コアラ」が入ります。 new_tags = sent_tags - current_tags ここで、new_tagsに「ユニコーン」が入ります。 old_tags.each do |old|〜で古いタグを消します。 今回の投稿記事付与したタグは 「お花」、「ユニコーン」です。 old_tagsに入ってる 「ネズミ」「コアラ」には用がありませんのでサヨナラします。 new_tags.each do |new|〜で新しいユニコーンというタグを保管します。 (以下は自分なりの解釈ですので気にしないでください。) モデルファイルを通過した時点で、手元に残ったタグは、 「お花」、「ユニコーン」です。 さらに、「ユニコーン」については「これは新しいタグ!」と判断されています。 モデルファイルは、なんだか気高い?裁判所のような?イメージです。 ①タグの表示・viewとコントローラー タグを表示していきます。(リンクは未実装です) 投稿一覧画面 postのコントローラー def index @posts = Post.page(params[:page]).per(10) @tag_list=Tag.all end app/views/posts/index.html.erb <!--タグリスト--> <% @tag_list.each do |list|%> <%=list.name %> <%="(#{list.posts.count})" %><% end %> 投稿一覧画面で投稿に紐づくタグを表示 <% @posts.each do |post| %> <i class="fas fa-tag"><%= post.tags.map(&:name).join(', ') %></i> <%= post.tags.map(&:name).join(', ') %>の部分、 最初、<%= post.tags.name %>にしてたのですが、エラーになってしまいました。 確かに、タグは複数ついてるので、<%= post.tags.name %>では取り出せないんだろうな。とは理解しました。 mapとは??&:とは??joinとは?? <%= post.tags.map(&:name).join(', ') %> 現状、postにタグは複数ついていて、配列になってます。['お花','ゴリラ','カエル']みたいな。 さて、これを取り出して、表示させたい。 map(&:name)は、配列の要素1つ1つを:nameの型にする というような意味のようです。 例を見るとわかりやすいです。 例:['a' ,'b'].map(&:upcase) #=> ["A", "B"] なので今回は、map(&:name)の部分で["お花","ゴリラ","カエル"]と文字列にされてるのだと思います。:nameはstring型にしてるので。 そしてjoin(', ')指定した文字で区切って連結してくれます。 "お花,ゴリラ,カエル"となってくれるわけですね 詳細画面での表示 posts_controller.rb def show @post = Post.find(params[:id]) @post_comment=PostComment.new @post_tags = @post.tags end posts/show.html.erb <% @post_tags.each do |tag| %> <%=tag.name%><%="(#{tag.posts.count})" %><% end %> ①編集機能 忘れてました.. <div class="field"> <%= f.label"タグ (,で区切ると複数タグ登録できます)" %> <%= f.text_field :name,value: @tag_list,class:"form-control"%> </div>  <div class="actions" style="margin:20px 0;"> <%= f.submit "投稿",class:"btn btn-outline-primary btn-block" %> </div> <% end %> value: @tag_listにしてあげないと、フォームに何も入ってない状態になってしまいます。 posts_controller.rb def edit @post = Post.find(params[:id]) # pluckはmapと同じ意味です!! @tag_list=@post.tags.pluck(:name).join(',') end def update @post = Post.find(params[:id]) tag_list=params[:post][:name].split(',') if @post.update(post_params) @post.save_tag(tag_list) redirect_to post_path(@post.id),notice:'投稿完了しました:)' else render:edit end end これで、編集機能もOKです!!! ひとまず終わり タグをリンクにして、投稿を取り出す手順については②の記事で説明します! とても参考にさせていただきました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】コントローラーのリファクタリング

はじめに ポートフォリオで、コントローラーのリファクタリングをやったので、備忘録として記事を書きます。 環境 ・Ruby 2.6.3 ・Ruby on Rails 5.2.5 リファクタリング1つ目 concemsを使う 共通記述を複数のコントローラーで使う時にやる app/controllers/concernsにファイルを追加し、必要箇所で読み込ませる controllers/concerns/search.rb module Search extend ActiveSupport::Concern def search @keyword = params[:word] redirect_to request.referer, notice: "検索文字を入れて下さい" if @keyword.blank? searcher = Searcher.new(params[:word].split(/[[:blank:]]+/)) @users = Kaminari.paginate_array(searcher.users).page(params[:page]).per(12) @groups = Kaminari.paginate_array(searcher.groups).page(params[:page]).per(12) @posts = Kaminari.paginate_array(searcher.posts).page(params[:page]).per(12) end end controllers/user/searches_controller.rb class User::SearchesController < ApplicationController include Search before_action :search end controllers/admin/searches_controller.rb class Admin::SearchesController < ApplicationController include Search before_action :search end これで同じ記述の箇所はスッキリ〜〜 リファクタリング2つ目 application_controller.rbにかいて継承 複数のコントローラに同じ処理が記述されている場合(継承)に役立つ controllers/user/●●●●●_controller.rb class ●●●●●Controller < ApplicationController before_action :authorize_owner private def authorize_owner redirect_to root_path unless current_user.owner? end end authorize_ownerメソッドが定義されていて、このメソッドを複数のコントローラーで使うよ!って時に application_controller.rbに記載して継承すれば、記述量少なくなります。 ※今回はauthorize_ownerメソッド使ってるけど、例として使っているのでどんなメソッドでもいい controllers/user/application_controller.rb class ApplicationController < ApplicationController private def authorize_owner redirect_to root_path unless current_user.owner? end end ↓継承したいコントローラーに。 controllers/user/●●●●●_controller.rb class ●●●●●Controller < ApplicationController before_action :authorize_owner end リファクタリング3つ目 callbackを利用して共通化 定義したメソッドをbefore_actionなどで共通化させちゃう方法 複数のアクションに同じ処理が記述されている場合に役立つ controllers/user/●●●●●_controller.rb class User::PostsController < ApplicationController before_action :set_group, only: %i[new index show create update destroy] before_action :set_post, only: %i[show update destroy] def new @post = Post.new end def index @posts = @group.posts.includes(:user).order(updated_at: :desc).page(params[:page]).per(12) end def show @comment = Comment.new respond_to do |format| format.html do end format.csv do send_data render_to_string, filename: "投稿詳細.csv", type: :csv end end end 一部アクション割愛 private #ここで記述が重なるコードをメソッド化 def set_group @group = Group.find(params[:group_id]) end def set_post @post = Post.find(params[:id]) end #ここまで #これをbefore_actionで呼び出しちゃえばおk end リファクタリング4つ目 王道モデルにメソッド定義 長ったらしいコードをモデルに定義してコントローラーをすっきりしようよ作戦 controllers/user/homes_controller.rb class User::HomesController < ApplicationController def top @group_ranks = Group.find(GroupUser.group(:group_id).order("count(group_id) desc").limit(6).pluck(:group_id)) @admins = Admin.first(6) end end #この部分をメソッド化してスッキリさせる# Group.find(GroupUser.group(:group_id).order("count(group_id) desc").limit(6).pluck(:group_id)) モデルに定義 models/group.rb # ランキング用メソッド def self.all_group_ranks find(GroupUser.group(:group_id).order("count(group_id) desc").limit(6).pluck(:group_id)) end コントローラーに呼び出す controllers/user/homes_controller.rb class User::HomesController < ApplicationController def top @group_ranks = Group.all_group_ranks # Group.rbでメソッド定義 @admins = Admin.first(6) end end 以上自分が実際にやったリファクタリングでした〜〜
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerコンテナ内でmysqlコマンドが使えなかったときの話

はじめに Docker環境で開発中に、コンテナ内で「rails g model ~」や「rails db:migrate」により、テーブルを作成したので、作成したテーブルなどを確認するために、MySQLを起動させようとしたところ、以下のとおり、起動しませんでした。 $ mysql -u root -p bash: mysql: command not found あれ、MySQL入っているはずなのに、なぜだろう? 開発環境 ・Ruby on Rails 6.1.4 ・MySQL 8.0.25 ・Docker 20.10.7 ・docker-compose 1.29.2 原因と対応 私はrailsコマンドを使用するために、Dockerをバックグラウンドで起動させた後、次のコマンドによりコンテナに入って作業をしていました。 $ docker-compose exec web bash そうです、webコンテナ(=railsのコンテナ)の中で、mysqlコマンドを実行しようとしていたのです。 気を取り直して、webコンテナからexitし、以下のコマンドを実行します。 $ docker-compose exec db bash これでdbコンテナ(MySQLのコンテナ)に入ることができ、無事、mysqlコマンドが実行できるようになりました。 もちろん、コンテナに入らずに直接、次のコマンドを入力してもOKです。 (これなら、コマンドにコンテナ名が入っているので、最初から気がついていたような気もしますが…) $ docker-compose exec db mysql -u root -p おわりに 原因がわかれば何とも単純な話ですが、エラーが出るとあれこれ調べて、よくわからない方向へ行ってしまい、いつの間にか時間ばかりが過ぎているというのは、初心者あるあるかと思います。 同じことをやらかしてしまった人が、この記事にたどり着いてくれれば、嬉しく思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]N+1問題を対処する

N+1問題って聞いたことはありますか? N+1問題はかなり怖い問題で、ぼくはポートフォリオ作成のときに、N+1問題に直面しました。 そのときに、調べた内容をぼくの理解の範囲で、アウトプットしていきます。 N+1問題とは そもそもN+1問題とはどんな問題のことをいうのでしょうか? データベースからデータベースを取り出すときに、大量のSQLが発行されて、動作が重くなる問題のことです。 たとえば、usersテーブルとpostsテーブルで、下記のようなアソシエーションがあったとします。 models/user.rb class User < ActiveRecord::Base has_many :posts end models/post.rb class Post < ActiveRecord::Base belongs_to :user end で、投稿の一覧を表示していきます。 Postモデルから全データ取得して、それをeach文で回していきます。 controllers/posts_controller.rb class PostsController < ApplicationController def index @posts = Post.all end end views/posts/index.html.erb <%= @posts.each do |post| %> <%= post.content %> <%= post.user.name %> <% end %> これで、このビューにアクセスし、ログを確認してみましょう。 User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] : このようにPostの数だけ、post.userを探すため、SQLが大量に発行されます。 勉強段階の開発環境等では、データ数もそこまで多くないので、そこまで問題になりませんが、実際に運用していくとなると、投稿数も増えていくことが想定されます。 データ数が、1000になると考えると、毎回1000件のデータ全てにSQLを発行していたら、遅くなるのは明白ですね。 なにも対策をとらないとデータ量が増えるにつれて、動作が遅くなる問題が、N+1問題です。 対策 では、N+1問題がどんなものか分かったところで、対策方法をみていきましょう! includesメソッドを使っていきます。 includesメソッドは、`アソシエーションの関連付けを事前に取得してくれます。 事前に取得ってよくわかりませんよね。 例をあげて考えてみましょう。 基本構文はこうです。 基本構文 # 関連名はアソシエーションの関連先 モデル名.inculudes(:関連名) controllers/posts_controller.rb class PostsController < ApplicationController def index # .allは省略できます @posts = Post.includes(:user) end end これで、ログを見てみましょう! User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 4]] このように、post.userに関するSQLは1行で完了しています。 つまりどういうことかというと、 Post.allの場合は、Postのデータを全て取得して、ビューのeach文で表示するときに、postのuser_idとどのユーザーが一致するか全てのユーザーの中から検証していました。 しかし、Post.includes(:user)の場合は、postsテーブルからデータを取得するときに、関連するusersテーブルのデータも取得しているので、each文で表示する時に、その都度SQLを発行する必要はなくなったのです。 まとめ N+1問題の対策としては、includesメソッドを使う! ポートフォリオ作成時に、N+1問題のことはなにも考えずにコーディングしていて、動作確認のときに「あれ?なんか重たいな」となったのが気づいたきっかけでした。 早いうちにN+1問題に触れられてよかったです。 ただ今回は自分なりに調べてみての結論で、他の記事の中では、includesメソッドを使う方法が推奨されていなかったりした記事がありました。 あとしょうがないのかもしれませんが、includesメソッドを使うことで、SQLの発行は改善されたのですが、コードの可読性がかなり下がるなと思いました。 まだまだ理解しきれていないことも多いので、これからもっとデータベースの知識を深めていきたいと思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

~part2~【FC版】React + Rails API + axios + react-router-domでCRUDを実装する

こんにちは!スージーです。 前回書いたこちらの続きです やりたい事 モデルを1対1で関連付け CRUDを実装 React × Rails APIで関連付けをしたモデルのCRUDを実装するにはこんな感じでやるんだなーって感じで見ていただけると幸いです 開発環境 Ruby 2.7.1 Rails 6.0.4 MySQL node.js 14.8.0 React 17.0.2 参考 【Rails】モデルの関連付けで用いられるbuildメソッドまとめ やらないこと rails トランザクションの説明 リレーションの説明 エラーハンドリング まずgem foremanを入れる 毎回ローカルサーバを立ち上げる時に、rails sとyarn startをするのが面倒くさいので backend $ gem install foreman touch Procfile backendディレクトリにProcfileを作成します // Procfile web: bundle exec rails s npm: cd ../frontend && yarn start group :development do gem 'listen', '~> 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' gem 'foreman' end gem foremanはデフォルトだとrails側が5000番ポート、client側が5100番ポートをlistenするのでcors.rbとclient.jsを修正します # cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:5100' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end // client.js const client = applyCaseMiddleware( axios.create({ baseURL: 'http://localhost:5000/api/v1', }), options ); これで設定完了です backend $ foreman start 07:06:36 web.1 | started with pid 6341 07:06:36 npm.1 | started with pid 6342 07:06:37 npm.1 | yarn run v1.22.10 07:06:37 npm.1 | $ react-scripts start 07:06:38 web.1 | => Booting Puma 07:06:38 web.1 | => Rails 6.0.4 application starting in development 07:06:38 web.1 | => Run `rails server --help` for more startup options 07:06:39 web.1 | Puma starting in single mode... 07:06:39 web.1 | * Version 4.3.8 (ruby 2.7.1-p83), codename: Mysterious Traveller 07:06:39 web.1 | * Min threads: 5, max threads: 5 07:06:39 web.1 | * Environment: development 07:06:39 web.1 | * Listening on tcp://127.0.0.1:5000 07:06:39 web.1 | * Listening on tcp://[::1]:5000 07:06:39 web.1 | Use Ctrl-C to stop 07:06:45 npm.1 | ℹ 「wds」: Project is running at http://172.20.10.5/ 07:06:45 npm.1 | ℹ 「wds」: webpack output is served from 07:06:45 npm.1 | ℹ 「wds」: Content not from webpack is served from /Users/sugawarakouhei/project/local-project/react-form-sample/frontend/public 07:06:45 npm.1 | ℹ 「wds」: 404s will fallback to / 07:06:45 npm.1 | Starting the development server... web => rails npm => react foreman startコマンド1つで両方のプロセスが実行される事が確認できればOK modelを作成 postモデルと1対1(has_one)で関連付けるdetailInfoモデルを作成します backend $ rails g model detailInfo # 日付_create_detail_infos.rb class CreateDetailInfos < ActiveRecord::Migration[6.0] def change create_table :detail_infos do |t| t.references :post, null: false t.string :favorite_food, limit: 100 t.string :favorite_toy, limit: 100 t.timestamps end end end 外部キーと他に2つのカラムを持たせます。好きな食べ物と好きなおもちゃです。全くテーブルを分ける必要がないデータ構造ですが、練習なので悪しからず... # post.rb class Post < ApplicationRecord has_one :detail_info, dependent: :destroy end dependent: :destroyは親モデルが物理削除された時に一緒に子モデルも物理削除します。 # detail_iinfo.rb class DetailInfo < ApplicationRecord belongs_to :post end これで1対1の関係になりました コントローラーの修正 class Api::V1::PostsController < ApplicationController def index render json: Post.all.to_json(include: :detail_info) end def show render json: Post.find(params[:id]).to_json(include: :detail_info) end def create ActiveRecord::Base.transaction do post = Post.new(post_params) detail_info = post.build_detail_info(detail_info_params) if post.save && detail_info.save render json: post.to_json(include: :detail_info) else ender json: { post: post.errors, detail_info: detail_info.errors }, status: 422 end end end def update ActiveRecord::Base.transaction do post = Post.find(params[:id]) detail_info = DetailInfo.find_by(post_id: params[:id]) if post.update(post_params) && detail_info.update(detail_info_params) render json: post.to_json(include: :detail_info) else render json: { post: post.errors, detail_info: detail_info.errors }, status: 422 end end end def destroy post = Post.find(params[:id]) post.destroy render json: post.to_json(include: :detail_info) end private def post_params params.require(:post).permit(:name, :neko_type) end def detail_info_params params.require(:detail_info).permit(:favorite_food, :favorite_toy) end end render末尾に.to_json(include: :detail_info)を追加しました。リレーションを設定したデータを紐付けてjsonを返してくれるようになります。後ほどcurlコマンドを叩いて確認します createアクションはbuildメソッドを使って保存します。1対1のときはbuild_モデル名で保存します。 あとトランザクションを使い、失敗した時にはrollbackするように処理を囲みます。 複数テーブルへの保存処理にはaccepts_nested_attributes_for(非推奨)やFormオブジェクトを使う方法など存在します。ただこれらをうまく動作させる事ができなかった(筆者の知識が乏しく)のでtransactionとbuildメソッドを使い、2つのモデルに登録する処理をcontrollerに書きました(fatコントローラーになってしまいました) updateアクションもcreateアクション同様にtrunsactionを使い処理を囲みます。findメソッドでpostモデルから該当レコードを1件取得します。find_byメソッドを使い、親モデルと同じid(post_id)を持つ該当レコードをdetail_infoモデルから1件取得します。 seeds.rbを修正 post1 = Post.create!(name: 'ニャア', neko_type: 'アメリカンショートヘア') DetailInfo.create!(post: post1, favorite_food: '魚', favorite_toy: '猫じゃらし') post2 = Post.create!(name: 'まる', neko_type: 'スコッティシュフォールド') DetailInfo.create!(post: post2, favorite_food: '野菜', favorite_toy: 'まりたん') post3 = Post.create!(name: 'むぎ', neko_type: 'スコッティシュフォールド') DetailInfo.create!(post: post3, favorite_food: '肉', favorite_toy: 'ダンボール') 以前の記事で作成したseeds.rbを修正しました。post: post1のように記載して、外部キーを持っている子モデルに対してcreateしています curlコマンドを叩いて確認してみます backend $ rails db:migrate:reset rails db:seed // 一覧 curl http://localhost:5000/api/v1/posts [{"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","created_at":"2021-07-30T23:22:17.084Z","updated_at":"2021-07-30T23:22:17.084Z","detail_info":{"id":1,"post_id":1,"favorite_food":"魚","favorite_toy":"猫じゃらし","created_at":"2021-07-30T23:22:17.106Z","updated_at":"2021-07-30T23:22:17.106Z"}},{"id":2,"name":"まる","neko_type":"スコッティシュフォールド","created_at":"2021-07-30T23:22:17.111Z","updated_at":"2021-07-30T23:22:17.111Z","detail_info":{"id":2,"post_id":2,"favorite_food":"野菜","favorite_toy":"まりたん","created_at":"2021-07-30T23:22:17.116Z","updated_at":"2021-07-30T23:22:17.116Z"}},{"id":3,"name":"むぎ","neko_type":"スコッティシュフォールド","created_at":"2021-07-30T23:22:17.121Z","updated_at":"2021-07-30T23:22:17.121Z","detail_info":{"id":3,"post_id":3,"favorite_food":"肉","favorite_toy":"ダンボール","created_at":"2021-07-30T23:22:17.126Z","updated_at":"2021-07-30T23:22:17.126Z"}}] // 詳細 curl http://localhost:5000/api/v1/posts/1 {"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","created_at":"2021-07-30T23:22:17.084Z","updated_at":"2021-07-30T23:22:17.084Z","detail_info":{"id":1,"post_id":1,"favorite_food":"魚","favorite_toy":"猫じゃらし","created_at":"2021-07-30T23:22:17.106Z","updated_at":"2021-07-30T23:22:17.106Z"}} // 新規作成 curl -X POST "http://localhost:5000/api/v1/posts" -H "Accept: application/json" -H "Content-type: application/json" -d '{"post": {"name": "test","neko_type":"test"},"detail_info":{"favorite_food": "test","favorite_toy":"test"}}' {"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"test","favorite_toy":"test","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:33:18.496Z"}}% // 更新 curl -X PATCH "http://localhost:5000/api/v1/posts/4" -H "Accept: application/json" -H "Content-type: application/json" -d '{"post": {"name": "test","neko_type":"test"},"detail_info":{"favorite_food": "update3","favorite_toy":"update3"}}' {"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"update3","favorite_toy":"update3","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:34:19.279Z"}}% // 削除 curl -X DELETE http://localhost:5000/api/v1/posts/4 {"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"update3","favorite_toy":"update3","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:34:19.279Z"}} [{"id": 1 ~~ detailInfo: {"id": 1, "post_id": 1 ~~} }] オブジェクトの中にdetailInfoというキーでオブジェクトがネストされたデータ構造になっています。想定通りにデータが入っています またpost / patchのリクエストを受けてcreateアクション / updateアクションが正常に動いている事が確認できました deleteのリクエストを受けて親モデルのレコードが削除された時にはモデルに追記したdependent: :destroyによって子モデルのレコードも削除されている事が確認できました 一覧画面の修正 以前の記事でapiコールの実装は完了しているので、取得したデータの中にあるdetailInfo: {"id": 1, "post_id": 1 ~~}のオブジェクト内から必要なプロパティを展開し表示します // List.jsx import React, { useEffect, useState } from 'react'; // deletePostを追加 import { getList, deletePost } from '../lib/api/post'; import { useHistory, Link } from 'react-router-dom'; const List = () => { const [dataList, setDataList] = useState([]); useEffect(() => { handleGetList(); }, []); const handleGetList = async () => { try { const res = await getList(); console.log(res.data); setDataList(res.data); } catch (e) { console.log(e); } }; const history = useHistory(); const handleDelete = async (item) => { console.log('click', item.id); try { const res = await deletePost(item.id); console.log(res.data); handleGetList(); } catch (e) { console.log(e); } }; return ( <> <h1>HOME</h1> <button onClick={() => history.push('/new')}>新規作成</button> <table> <thead> <tr> <th>名前</th> <th>猫種</th> {/* 追加 */} <th>好きな食べ物</th> {/* 追加 */} <th>好きなおもちゃ</th> <th colSpan='1'></th> <th colSpan='1'></th> <th colSpan='1'></th> </tr> </thead> {dataList.map((item, index) => ( <tbody key={index}> <tr> <td>{item.name}</td> <td>{item.nekoType}</td> {/* 追加 */} <td>{item.detailInfo.favoriteFood}</td> {/* 追加 */} <td>{item.detailInfo.favoriteToy}</td> <td> <Link to={`/edit/${item.id}`}>更新</Link> </td> <td> <Link to={`/post/${item.id}`}>詳細へ</Link> </td> <td> <button onClick={() => handleDelete(item)}>削除</button> </td> </tr> </tbody> ))} </table> </> ); }; export default List; 一覧に好きな食べ物と好きなおもちゃが表示されていればOK 詳細画面の修正 同じ要領で詳細画面の修正をしていきます。詳細画面ではquery.idの取得タイミングがrenderingより遅くなってしまうので{data.detailInfo.favoriteFood}がTypeError: Cannot read property 'favoriteFood' of undefinedと怒られてしまいます。なのでstateに初期値を設定します // Detail.jsx import React, { useEffect, useState } from 'react'; import { getDetail } from '../lib/api/post'; import { useHistory, useParams } from 'react-router-dom'; const Detail = () => { // 修正 const [data, setData] = useState({ name: '', neko_type: '', detailInfo: { favorite_food: '', favorite_toy: '', }, }); const query = useParams(); console.log(query.id); const history = useHistory(); useEffect(() => { handleGetDetail(query); }, [query]); const handleGetDetail = async (query) => { try { const res = await getDetail(query.id); console.log(res.data); setData(res.data); } catch (e) { console.log(e); } }; return ( <> <h1>DETAIL</h1> <div>ID:{data.id}</div> <div>名前:{data.name}</div> <div>猫種:{data.nekoType}</div> {/* 追加 */} <div>好きな食べ物:{data.detailInfo.favoriteFood}</div> {/* 追加 */} <div>好きなおもちゃ:{data.detailInfo.favoriteToy}</div> <button onClick={() => history.push('/')}>戻る</button> </> ); }; export default Detail; 好きな食べ物と好きなおもちゃが表示されていればOK 削除機能(追加無し) 削除機能は変更なしで削除できることが確認できればOK 新規作成の修正 新規作成を修正します テキストフィールをForm.jsxに追加 Submitイベントでapiに送るパラメータのオブジェクト構造を加工する // Form.jsx import React from 'react'; const Form = (props) => { const { handleChange, handleSubmit, value, buttonType } = props; return ( <> <form> <div> <label htmlFor='name'>猫の名前:</label> <input type='text' name='name' id='name' onChange={(e) => handleChange(e)} value={value.name} /> </div> <div> <label htmlFor='nekoType'>猫種</label> <input type='text' name='nekoType' id='nekoType' onChange={(e) => handleChange(e)} value={value.nekoType} /> </div> {/* 追加 */} <div> <label htmlFor='nekoType'>好きな食べ物</label> <input type='text' name='favoriteFood' id='favoriteFood' onChange={(e) => handleChange(e)} value={value.favoriteFood} /> </div> <div> <label htmlFor='nekoType'>好きなおもちゃ</label> <input type='text' name='favoriteToy' id='favoriteToy' onChange={(e) => handleChange(e)} value={value.favoriteToy} /> </div> {/* ここまで */} <input type='submit' value={buttonType} onClick={(e) => handleSubmit(e)} /> </form> </> ); }; export default Form; // New.jsx import React, { useState } from 'react'; import FormBody from './Form'; import { createPost } from '../lib/api/post'; import { useHistory } from 'react-router-dom'; const New = () => { const [value, setValue] = useState({}); const history = useHistory(); const handleChange = (e) => { setValue({ ...value, [e.target.name]: e.target.value, }); }; const handleSubmit = async (e) => { e.preventDefault(); console.log(value); // 追加 const params = generateParams(); try { const res = await createPost(params); console.log(res); history.push('/'); } catch (e) { console.log(e); } }; // パラメータのオブジェクト構造を加工 const generateParams = () => { const params = { name: value.name, nekoType: value.nekoType, // detailInfoというキーでオブジェクトをネストする detailInfo: { favoriteFood: value.favoriteFood, favoriteToy: value.favoriteToy, }, }; return params; }; return ( <> <h1>NEW</h1> <FormBody handleChange={handleChange} handleSubmit={handleSubmit} value={value} buttonType='登録' /> </> ); }; export default New; paramsの中身を見てみるとこのようなオブジェクトになっています 登録ボタンを押下してデータが登録できればOK railsのログを見てみると、パラメータを受け取って2つのinsert処理が走っている事がわかります。BEGINとCOMMITに挟まれ処理が実行されているのでtrunsactionが効いている事も確認できます 10:11:26 web.1 | Started POST "/api/v1/posts" for ::1 at 2021-08-02 10:11:26 +0900 10:11:26 web.1 | Processing by Api::V1::PostsController#create as HTML 10:11:26 web.1 | Parameters: {"name"=>"test", "neko_type"=>"test", "detail_info"=>{"favorite_food"=>"test", "favorite_toy"=>"test"}, "post"=>{"name"=>"test", "neko_type"=>"test"}} 10:11:26 web.1 | (54.6ms) BEGIN 10:11:26 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create' 10:11:26 web.1 | Post Create (14.8ms) INSERT INTO `posts` (`name`, `neko_type`, `created_at`, `updated_at`) VALUES ('test', 'test', '2021-08-02 01:11:26.894098', '2021-08-02 01:11:26.894098') 10:11:26 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create' 10:11:27 web.1 | DetailInfo Create (26.6ms) INSERT INTO `detail_infos` (`post_id`, `favorite_food`, `favorite_toy`, `created_at`, `updated_at`) VALUES (4, 'test', 'test', '2021-08-02 01:11:26.977189', '2021-08-02 01:11:26.977189') 10:11:27 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create' 10:11:27 web.1 | (6.9ms) COMMIT 10:11:27 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:12:in `create' 10:11:27 web.1 | Completed 200 OK in 142ms (Views: 0.4ms | ActiveRecord: 102.9ms | Allocations: 4071) 更新画面を修正 最後に更新処理を修正します stateの初期値を修正する Submitイベントでapiに送るパラメータのオブジェクト構造を加工する // Edit.jsx import React, { useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { updatePost, getDetail } from '../lib/api/post'; import FormBody from './Form'; const Edit = () => { const [value, setValue] = useState({ name: '', nekoType: '', // 追加 favoriteFood: '', // 追加 favoriteToy: '', }); const query = useParams(); const history = useHistory(); useEffect(() => { handleGetData(query); }, [query]); const handleGetData = async (query) => { try { const res = await getDetail(query.id); console.log(res.data); setValue({ name: res.data.name, nekoType: res.data.nekoType, // 追加 favoriteFood: res.data.detailInfo.favoriteFood, // 追加 favoriteToy: res.data.detailInfo.favoriteToy, }); } catch (e) { console.log(e); } }; const handleChange = (e) => { setValue({ ...value, [e.target.name]: e.target.value, }); }; const handleSubmit = async (e) => { e.preventDefault(); // 追加 const params = generateParams(); try { const res = await updatePost(query.id, params); console.log(res); history.push('/'); } catch (e) { console.log(e); } }; // パラメータのオブジェクト構造を加工 const generateParams = () => { const params = { name: value.name, nekoType: value.nekoType, detailInfo: { favoriteFood: value.favoriteFood, favoriteToy: value.favoriteToy, }, }; return params; }; return ( <> <h1>Edit</h1> <FormBody handleChange={handleChange} handleSubmit={handleSubmit} value={value} buttonType='更新' /> </> ); }; export default Edit; 修正箇所はNew.jsxとほとんど同じです 更新ボタンを押下してデータが更新できればOK railsのログを見てみると、パラメータを受け取って2つのselectと2つのupdate処理が走っている事がわかります。id=4のレコードをpostテーブルから取得して更新処理をしています。post_id=4のレコードをdetailInfoテーブルから取得して更新処理をしています。BEGINとCOMMITに挟まれ処理が実行されているのでtrunsactionが効いている事も確認できます 10:16:04 web.1 | Started PATCH "/api/v1/posts/4" for ::1 at 2021-08-02 10:16:04 +0900 10:16:04 web.1 | Processing by Api::V1::PostsController#update as HTML 10:16:04 web.1 | Parameters: {"name"=>"update", "neko_type"=>"update", "detail_info"=>{"favorite_food"=>"udpate", "favorite_toy"=>"update"}, "id"=>"4", "post"=>{"name"=>"update", "neko_type"=>"update"}} 10:16:04 web.1 | (0.2ms) BEGIN 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:25:in `block in update' 10:16:04 web.1 | Post Load (0.6ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 4 LIMIT 1 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:25:in `block in update' 10:16:04 web.1 | DetailInfo Load (21.6ms) SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 4 LIMIT 1 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:26:in `block in update' 10:16:04 web.1 | Post Update (12.4ms) UPDATE `posts` SET `posts`.`name` = 'update', `posts`.`neko_type` = 'update', `posts`.`updated_at` = '2021-08-02 01:16:04.763790' WHERE `posts`.`id` = 4 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update' 10:16:04 web.1 | Post Load (14.5ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 4 LIMIT 1 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update' 10:16:04 web.1 | DetailInfo Update (1.0ms) UPDATE `detail_infos` SET `detail_infos`.`favorite_food` = 'udpate', `detail_infos`.`favorite_toy` = 'update', `detail_infos`.`updated_at` = '2021-08-02 01:16:04.821784' WHERE `detail_infos`.`id` = 4 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update' 10:16:04 web.1 | DetailInfo Load (21.8ms) SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 4 LIMIT 1 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:28:in `block in update' 10:16:04 web.1 | (7.4ms) COMMIT 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:24:in `update' 10:16:04 web.1 | Completed 200 OK in 184ms (Views: 0.2ms | ActiveRecord: 79.4ms | Allocations: 6798) 最後に コントローラがfatになっているので、もっと良い書き方があればご教示ください! おわり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】サンプルユーザーを増やす!Fakerの導入でサクラを作る方法

対象者 作成したサイトにテストユーザーを大量に導入したい方 目的 Gem「Faker」を使用してデータを増やすこと 実際の手順と実例 1.Fakerとは Fakerとは実際にいそうなユーザー名(ユーザー以外も行ける??)を作成するGemです。 これの導入によって手作業で1人ずつユーザー登録をしていくという作業や手間をカットできます。 2.Faker導入方法 1.gemに導入 : : gem 'rails', '6.0.3' gem 'bcrypt', '3.1.13' gem 'faker', '2.1.2' gem 'will_paginate', '3.1.8' : この後bundle installを実行します 2.サンプルユーザー作成 seedファイルに作成していきます。 まずメインとなるユーザーを1人作成します db/seed.rb User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar") 続いて、追加で偽物を何人作成するのか決めていきます。 db/seed.rb #上記と同じ User.create!(name: "Example User", email: "example@gmail.com", password: "foobar", password_confirmation: "foobar") #ここから追記 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@gmail.com" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) end これで完了です。ここでは99人のユーザーを作成しました。 この後、rails db:seedを実行すれば完了です。 ⚠先に何かデータが入っていいる場合、rails db:migrate:resetするといいかもしれません!また、サーバー起動中はrails db:migrate:resetがうまくできないこともあるそうなので、再起動して実行がおすすめです! 参照 Rails tutorial 10.3.2サンプルのユーザー 投稿者コメント Rails tutolialを進めてたら何やら面白そうなGemがあるなあとおもって記事にしました。 これからPF作成もあるので、使ってみようかなと思っています! My Profile プログラミング学習歴3ヶ月目のアカウントです! プログラミングスクールで学んだ内容や自分が躓いた箇所等のアウトプットの為に発信しています。 また、プログラミング初学者の方にわかりやすく、簡潔にまとめて情報共有できればと考えています。 もし、投稿した記事の中に誤り等ございましたら、コメント欄でご教授いただけると幸いです。 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails deviseの導入方法と仕様・仕組み

deviseとは ユーザー管理機能の実装を補助してくれるRubyのgem(ライブラリ)です。 新規登録・ログイン・ログアウトやメール認証・SNS認証等の機能を実装でき、セキュリティ的にも安全なものが作れます。 devise導入 Railsのアプリケーションディレクトリ直下にあるGemfileの末尾に以下を追記します。 gem 'devise' ターミナルでRailsのアプリケーションディレクトリに移動し、以下コマンドを実行してインストールします。 bundle install # gemインストール rails g devise:install # deviseの設定ファイル生成 認証用のモデルを作成 認証用のモデルを作成します。以下ではuserモデルを作成していますがcustomerやmail_user、admin等好きなモデル名でOKです。 rails g devise user migrationファイルやmodelファイルが生成されます。 rails g model userとの違いは以下3点です。 - migrationファイルにemailカラムやencrypted_passwordカラム等の記述が最初からされているものが生成される - modelファイルにdevise :database_authenticatable, ~ ::validatableの記述が最初からされているものが生成される - routes.rbにdevise_for :モデル名の記述が最初からされているものが生成される migrationファイルの編集・実行 migrationファイルを編集します。 必要に応じてnameカラム等追加したいカラムを追加します。 また以下機能のカラムを必要に応じてコメントアウト・アンコメントします。 機能 概要 デフォルト Recoverable パスワードリセット機能 有効 Rememberable ログイン状態を保持する機能 有効 Trackable ログイン回数や最終ログイン日時等の追跡をする機能 無効 Confirmable メール認証機能 無効 Lockable アカウントロック機能(指定回数連続で認証失敗するとアカウントをロックする機能) 無効 rails g db:migrateを実行してDBのテーブルを生成します。 コントローラーの作成 deviseのデフォルトの動作を改変する場合はrails g devise:controllers usersでコントローラーを作成します。 app/models/users/ディレクトリ以下に複数のコントローラーが生成されるので、必要なもの(改変したいコントローラー)のみroutes.rbに以下のように記述して読み込ませます。 routes.rb devise_for :users, controllers: { sessions: 'users/sessions' } ビューの生成 deviseのデフォルトのビューを改変する場合はrails g devise:views usersでビューを作成します。 app/views/users/ディレクトリ以下にビューファイルが生成されるので、改変したい箇所を編集します。 モデルの設定 modelファイルに記述されているdevise :database_authenticatable, ~ ::validatableを必要に応じて削除・追記します。以下各説明です。 これらの設定はdeviseのmodel作成コマンドではなく通常のrails g model ~コマンドで生成したモデルにも設定できます。 :database_authenticatable メールとパスワードを使った認証をしたい場合に設定します。 パスワード保存時に自動でハッシュ化してくれたり、ログイン・ログアウトのセッション管理もしてくれます。 これを設定したモデルにemailとencrypted_passwordカラムが無いとエラーになります。 routes.rbでdevise_forにこれを設定したモデルを指定すると、ログイン・ログアウト用のルーティングが設定されます。 - session#new - session#create - session#destroy モデルにクラスメソッドやインスタンスメソッドを追加します。詳細は以下のドキュメントに記載されています。 https://www.rubydoc.info/github/plataformatec/devise/Devise/Models/DatabaseAuthenticatable :authenticatable メールとパスワードを使った認証以外の認証をしたい場合に設定します。 SNS認証用のモデルにはこちらを設定するのが良さそうです。 :database_authenticatableと異なり、ログイン・ログアウト用のルーティングは追加されません。 モデルにクラスメソッドやインスタンスメソッドを追加します。詳細は以下のドキュメントに記載されています。 https://www.rubydoc.info/github/plataformatec/devise/Devise/Models/Authenticatable :registerable 新規登録機能を実装したい場合に設定します。 SNS認証用のモデルには必要ありません。 routes.rbでdevise_forにこれを設定したモデルを指定すると、新規登録用のルーティングが設定されます。 - registrations#cancel - registrations#new - registrations#edit - registrations#update - registrations#update - registrations#destroy - registrations#create :validatable メールとパスワードのバリデーションを追加します。 自分でバリデーションを設定する場合は必要ありません。 追加されるバリデーションは以下の通りです。 - メールアドレスの入力が必須であり、有効な形式であり、一意であること(登録済みでないこと) - 有効な形式は正規表現で/\A[^@\s]+@[^@\s]+\z/とされています。 - @が1個含まれで、かつ、その前後に@とスペース以外の文字が1文字以上必要って意味...だと思います。 - パスワードの入力が必須であり、文字数が6~128文字の範囲であり、パスワードと確認用パスワードが一致していること https://www.rubydoc.info/github/plataformatec/devise/Devise/Models/Validatable https://github.com/heartcombo/devise/blob/master/lib/devise.rb#L116 :confirmable メール認証機能を実装する場合に設定します。 routes.rbでdevise_forにこれを設定したモデルを指定すると、新規登録用のルーティングが設定されます。 - confirmations#new - confirmations#show - confirmations#create アカウント登録時に認証用トークンを生成し、認証用トークンを付与した本登録用URLを記載したメールをユーザーに送信してくれます。 (confirmed_atが空の状態で保存されると、メールが送信されます。) これを利用する時はRailsの送信用メールサーバーの設定をする必要があります。 https://www.rubydoc.info/github/plataformatec/devise/Devise/Models/Confirmable https://github.com/heartcombo/devise/blob/master/lib/devise/models/confirmable.rb :omniauthable OAuth認証(SNS認証等)を実装する場合に設定します。 別途gemをインストールする必要があります。 routes.rbでdevise_forにこれを設定したモデルを指定すると、OAuth認証のコールバックURLのルーティングが設定されます。 :recoverable パスワードリセット機能を実装する場合に設定します。 :rememberable ログイン状態を保持する機能を実装する場合に設定します。 :lockable アカウントロック機能を実装する場合に設定します。 指定回数ログインに失敗するとアカウントをロックします。 アカウントロックの解除方法はメールでの解除・時間経過での解除の2つ用意されており、両方を設定することも可能です。 https://www.rubydoc.info/github/plataformatec/devise/Devise/Models/Lockable :timeoutable ログインセッションの有効期限機能を実装する場合に設定します。 設定された有効期限を過ぎるとログインページへリダイレクトされます。 https://www.rubydoc.info/github/plataformatec/devise/Devise/Models/Timeoutable :trackable ログイン回数やログイン日時・IPアドレスを記録する機能を実装する場合に設定します。 - sign_in_count : ログイン回数 - current_sign_in_at : 最終ログイン日時 - last_sign_in_at : その前のログイン日時 - current_sign_in_ip : 最終ログイン時のIPアドレス - last_sign_in_ip : その前のログイン時のIPアドレス https://www.rubydoc.info/github/plataformatec/devise/Devise/Models/Trackable ルーティングの設定 devise_forやdevise_scopeでdeviseのルーティングを追加します。 複数の認証用モデルが存在する場合はそれぞれの認証用モデルに設定することが可能です。 devise_for :モデル名(複数形)ではそのモデルにdeviseメソッドで設定されてる内容を読み取ってそれに対応したルーティングを追加します。 deviseのデフォルトのコントローラーを改変する場合はコントローラーを指定します。 以下はセッション管理コントローラーと新規登録用コントローラーとメール認証のコントローラーを設定している例です。 routes.rb devise_for :users, controllers: { sessions: 'users/sessions', registrations: 'users/registrations', confirmations: 'users/confirmations' } 独自のルーティングを追加する場合はdevise_scopeを利用します。 こちらはdevise_scope :モデル名(単数形)にします。以下は例です。 routes.rb devise_scope :user do patch 'users/confirmation', to: 'users/confirmations#confirm' end deviseのヘルパーメソッド deviseは以下のような便利なヘルパーメソッドを用意してくれています。 以下userモデルの例です。user以外の名前のモデル名を付けている場合はメソッド名もモデル名に対応して変わります。 (adminモデルならadmin_sign_in?等) - user_sign_in?: ユーザーがログインしているかtrue/falseで返します。 - current_user: 現在ログイン中のユーザーのモデルインスタンスを返します。ログインしていなければnilを返します。 - before_action :authenticate_user!: ログインしていなければログインページへ飛ばします。 - ApplicationControllerまたは個別のコントローラーに設定します。 - 特定のページに設定したい場合はonlyやexceptオプションを利用します。 user_sign_in?メソッドは全てのコントローラー・ビュー内で利用できます。 current_userメソッドはdevise_forが呼び出された際に指定したモデル名に合わせて、そのdevise_forで設定されるコントローラーのアクション・及びそのビューでのみ利用できます。 ストロングパラメーター :database_authenticatableを設定しているモデルにカラムを追加した場合は以下のようにストロングパラメーターを設定します。 以下の例ではlast_nameカラムとfirst_nameカラムを設定しています。 user_details等の別のモデルに追加カラムを分離している場合は以下の設定は不要です。 application_controller.rb before_action :configure_permitted_parameters, if: :devise_controller? private def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:last_name, :first_name]) end 参考 【Rails】deviseを導入してみる https://qiita.com/Hal_mai/items/350c400e8763ce0487a3 Rails deviseで使えるようになるヘルパーメソッド一覧s https://qiita.com/tobita0000/items/866de191635e6d74e392 【Rails】 deviseの使い方をマスターしてログイン認証機能を実装しよう! https://pikawaka.com/rails/devise Deviseのモヤモヤを解消して快適なRailsライフを送ろう! https://zenn.dev/kitabatake/articles/start-to-like-the-devise Document Module: Devise::Models https://www.rubydoc.info/github/plataformatec/devise/Devise/Models 最後に 間違い等あれば指摘いただけると幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]パンくずリストの実装方法

はじめに パンくずリストを一ページごとに挿入するのは面倒で管理がしにくいです。webサービスに関しても遷移が多くなるので、パンくずリストを実装していないと希望のページに飛びにくくなってUI的に下がります。そこで、パンくずリストを簡単に導入できるGemを紹介します。是非つかっていただけたら記事を書いた甲斐があって嬉しいです。 紹介するGem gretel パンくずリストのためにつくられたGemで、スター数は少ないものの簡易的にパンくずリストを実装できます。 多少使いにくい部分も出てきましたので改善の余地があります。最新アップデートが一か月前なので、まだ期待はできるかなと。(自分でプルリク出すのはニッチすぎる部分なので控えます?) 使い方 1.Gemをインストール Gemfile gem "gretel" bundle install 2.Configファイルを作成 rails generate gretel:install 上記のコマンドを実行すると、config/breadcrumbs.rbが作成されます。このConfigファイルでパンくずリストのバックエンド部分を設定します。 3.Configファイルを編集 config/breadcrumbs.rb # rootの設定 crumb :root do link "Home", root_path end # 投稿一覧の設定 crumb :posts do link "投稿一覧", posts_path end # 投稿詳細の設定 crumb :post do |post| link post.title, post parent :posts # 投稿詳細の親元を投稿一覧に設定する end コードの説明 link: パンくずリストの実際に表示されるリンクの名前とpathを設定しています。投稿詳細の部分は、投稿のタイトルを表示しています。 crumb :example : くずを作成。exampleには変数名をつけています。view側で呼び出すときに、わかりやすい名前にしましょう。(基本的には、path名から取ってきます) parent: 親元のくず名を指定します。 4. Viewファイルに出力 layout/applcation.rb <%= breadcrumbs pretext: "You are here: ", separator: " &rsaquo; " %> <%= yield %> まず、表示する場所に挿入します。一定の場所に表示したいので例ではlayout/application.rbの<%= yield %>上に設置しています。 pretext : 現在位置のくずに追加でテキストを指定できます。 separator: 例えば、[Home > Posts > Post]の">"の部分を変数として決めれます。 views/posts/index.html.erb <% breadcrumb :posts %> 最後に作成した「くず」を呼び出してあげます。 これで完成です。 まとめ デメリットとしてはページごとに「くず」を呼び出さなければいけない部分であり、Configファイルでpathと連携することができれば各ページに挿入しなくて済むかなと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

部分テンプレートで初歩的なエラー

ActionView::MissingTemplate エラーが起きました! ポートフォリオが完成してきたので、可読性の向上、見た目を整えようと思ってheaderを部分テンプレートにしようと思い、取り組もうとコードを書き始めましたが・・・・。 結論 フォルダの階層を間違えておりました! お手本にしているのでは、sharedファイルは、viewファイルの階層にあったのですが、deviseを使っていた為、deviseファイルの階層(1階層下)に潜り込んでいたが故に怒ったエラーだったんですね。 反省点 エラーが出てググってみるも、同じエラーでも原因の種類もたくさんありました。 とりあえず今回は数分で気づくことができたので、自分の中ではよしとしましょう・・・。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails チュートリアル第11章

メール機能を使ってsignupを改善する ブランチを変える git checkout -b account-activation AccountActivationsのフォルダを作成 rails generate controller AccountActivations 新しくマイグレーションファイルを作成 またカラム名とデータ型を指定する rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime editアクションへの名前付きルートが必要になるので 名前付きルートを扱えるようにルーティングにアカウント有効化用のresources行を追加 編集だけにする Rails.application.routes.draw do root 'static_pages#home' . . . resources :account_activations, only: [:edit] #名前付きルートを扱えるようにするため #ルーティングにアカウント有効化用のresources行を追加 # 編集だけ end 指定したために作成したファイルのコードに名前とデータ型が書かれてある。 booleanではtrue,false,nilの3つがある。のでnilが出ないようにデフォルトで指定しておく。 class AddActivationToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :activation_digest, :string add_column :users, :activated, :boolean default: false add_column :users, :activated_at, :datetime end end migrateはmigrationファイルの内容をDBに反映する行為 rails db:migrate Userモデルにアカウント有効化のコードを追加する class User < ApplicationRecord attr_accessor :remember_token, :activation_token before_save :downcase_mail before_create :create_activation_digest . . . private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end 開発環境のサンプルユーザーを最初から有効にしておく User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true, activated: true, activated_at: Time.zone.now) # admin: trueにすることで管理者にすることができる # 追加のユーザーをまとめて生成する 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password, activated: true, activated_at: Time.zone.now) end テスト環境のユーザーも同じように書く。 データベースを初期化して、サンプルデータを再度生成し直し、変更を反映 $ rails db:migrate:reset $ rails db:seed 送信メールのテンプレート メイラーは、モデルやコントローラと同様にrails generateで生成 アカウント有効化メールの送信に必要なコードを追加のため account_activation,password_resetはアクション名 UserMailerはコントローラ名という感じ rails generate mailer UserMailer account_activation password_reset 生成されたApplicationメイラーのデフォルト値を更新した。 class ApplicationMailer < ActionMailer::Base default from: "noreply@example.com" # アドレスのデフォルト値を更新 layout 'mailer' # layout コントローラーごとに各ページ共通の #ビューファイルを指定できるメソッド # mailerのページを指定する end Userメイラーを生成し、アカウント有効化リンクをメール送信する様にする class UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end def password_reset @greeting = "Hi" mail to: "to@example.org" end end # 内容はわからん 送信メールのプレビュー テンプレートの実際の表示を簡単に確認するためメールプレビューを使う。 Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができる。 これを利用するには、アプリケーションのdevelopment環境の設定に手を加える必要がある。 development環境のメール設定 Rails.application.configure do . . . # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false # '/account_activation/:token/edit?email=foobar@bar.com' host = '開発環境のurl' # example.comにはsample_appは無いから  # ここをコピペすると失敗します。 # 自分の環境のホストに変えてください。 # クラウドIDEの場合は以下をお使いください config.action_mailer.default_url_options = { host: host, protocol: 'https' } . . . end アカウント有効化のプレビューメソッド # Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at /rails/mailers/user_mailer/account_activation def account_activation user = User.first user.activation_token = User.new_token # activation_tokenは仮の属性でしかない # データベースのユーザーはこの値を実際には持っていません UserMailer.account_activation(user) # UserMailerクラスのメソッド # 引数はメールオブジェクト(メールのプレビュー) # 開発用データベースの最初のユーザーに定義して #UserMailer.account_activationの引数として渡す end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end # だいたいわからん 現在のメールの実装をテストする require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end #だいたいわからん end users.rb def authenticated?(attribute, token) digest = send("#{attribute}_digest") # attributeがremember,activation,resetだったりする return false if digest.nil? # remember_digestが空ならばfalseを返す # 先にreturnをすることでエラーを起こさないようにする # remember_tokenが空(nil)だから BCrypt::Password.new(remember_digest).is_password?(remember_token) # データベースのremember_digestとトークンのremember_token #を照らし合わせる end テストを行うとエラーが起こる Error: SessionsHelperTest#test_current_user_returns_right_user_when_session_is_nil: ArgumentError: wrong number of arguments (given 1, expected 2)                                                                                     expected 引数は2つを期待しているが given 引数は1個しかない app/models/user.rb:49:in `authenticated?' app/helpers/sessions_helper.rb:20:in `current_user' test/helpers/sessions_helper_test.rb:15:in `block in <class:SessionsHelperTest>' user_test.rb test "authenticated? should return false for a user with nil digest" do # 認証する? ダイジェストが空(nil)のユーザーにはfalseを返す必要があります assert_not @user.authenticated?(:remember, '') # 空のデータベースは認証されないよね? end Failure: SessionsHelperTest#test_current_user_returns_right_user_when_session_is_nil [/home/ubuntu/environment/sample_app/test/helpers/sessions_helper_test.rb:15]: --- expected +++ actual @@ -1 +1 @@ -#<User id: 762146111, name: "Michael Example", email: "michael@example.com", created_at: "2021-07-31 15:33:09", updated_at: "2021-07-31 15:33:13", password_digest: [FILTERED], remember_digest: "$2a$04$iDPLGzGFNJBBoRUPTNCKL.mdoxo7NQTKSPy3G8YKU/I...", admin: true, activation_digest: nil, activated: true, activated_at: "2021-07-31 15:33:09"> +nil -#<User....の情報が来るべきだが +nil その情報が来ない というエラーらしい editアクションを書く準備ができた paramsハッシュで渡されたメールアドレスに対応するユーザーを認証 既に有効になっているユーザーを誤って再度有効化しないために必要 このコードがなければ、攻撃者がユーザーの有効化リンクを後から盗みだしてクリックするだけで、本当のユーザーとしてログインできてしまいます。 class AccountActivationsController < ApplicationController def edit ef edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) # 既に有効になっているユーザーを誤って再度有効化しないために必要 # このコードがなければ、攻撃者がユーザーの有効化リンクを後から盗みだしてクリックするだけで、本当のユーザーとしてログインできてしまう user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) # 標準時間を表示させる log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end ここで使用していたインスタンスが消えてしまった。 また1から頑張りたい。 困ったこと rails generate migration add_activation_to_usersしか入力せずにmigrationファイルを作成してしまった。 rails generate migration add_activation_to_users このファイルを削除するために入力した。 rails destroy migration add_activation_to_users 見事に成功。 また新しくファイルを作成した。 rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime これも成功した。 Error: UsersSignupTest#test_valid_signup_information: AbstractController::DoubleRenderError: Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return". app/controllers/users_controller.rb:54:in `create' test/integration/users_signup_test.rb:18:in `block (2 levels) in <class:UsersSignupTest>' test/integration/users_signup_test.rb:17:in `block in <class:UsersSignupTest>'</font> rails test test/integration/users_signup_test.rb:15 なんと言っているわからないが、 user_controllerの54行目を確認してみたら log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user を削除していなかった。 動画通りになった。 鈍臭い...。 また困りごとが解決したら書きたいと思う。 努力したい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【第9章】Railsチュートリアルでbcryptとfixtureについて忘れていたので復習した

Railsチュートリアル第9章(第6版)でbcryptとfixtureを忘れていたので、復習した。 ①bcrypt(ビー・クリプト)とは ・6.3.1 ハッシュ化されたパスワード にて登場 ・パスワードをハッシュ化する為の、最先端のハッシュ関数。 ・ハッシュ化すると元のデータを特定するのは、非常に難しい(ほぼ無理)。 ・bcryptアルゴリズムではハッシュ化する前にソルト化されたハッシュを追加している。 これにより、辞書攻撃やレインボーテーブル攻撃を防ぐことができる。 ②fixture(フィクスチャ)とは ・言葉自体は2.2 Usersリソース から登場している。 ・本格的に扱ったのは 8.2.4 レイアウトの変更をテストする ・テスト時に使用。 ・データベースに追加するテスト用データをfixtureで作成できる。 ・fixtureではERb(埋め込みRuby)を利用できる。 -fixtureの例- ---test/fixtures/users.yml--- abblepai: name: Abblepai Example email: applepai@example.com 以下のように参照できる。 user = users(:abblepai)  users=fixutreのファイル名、:abblepaiというシンボルはユーザーを参照するためのキー。 ③まとめ ①~②で忘れていた箇所を思い出せた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails マイグレーション

マイグレーション マイグレーション・・データベースの設計図 作成した・編集したマイグレーションファイルを読み込み、データベースに反映 $ rails db:migrate def change create_table :posts do |t|//postsテーブルの作成 t.text :content//データがtextであるcontentという名前のカラムを作成している end def change add_column :users, :image_name,:string //テーブル名    カラム名            データ型 end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby] pryで、矢印キーを押すと`^[[A`などと出てしまう現象の解決法

環境 macOS 10.13.6 Ruby 2.5.7 Rails 6.1.3.1 Docker 20.10.7 Docker Compose v2.0.0-beta.6 現象 pryで矢印キーを押すと、次のような記号が現れてしまうようになってしまう。 例えば、矢印キーの↑を押すと、下記のような^[[Aという文字が表示されてしまいます。 その他、矢印キーの→を押すと^[[C、矢印キーの←を押すと^[[Bが表示されます。 原因 最近私はDockerのバージョンを20.10.7にUpgradeしたのだが、それに伴ってDocker Composeのバージョンがbeta版になってしまっていた。 つまりDocker Compose V2がデフォルトで有効になっていた。 これが、今回起きた矢印キーのバグの原因だった。 対処法 Docker Compose V2を無効にすると解決します。 docker docsに記載の通り、下記のように無効にします。 DockerDesktopを使用してDockerCompose V2を無効にするには: ① Dockerメニューから、[Preferences]> [experimental features]をクリックします。 ② [Use Docker Compose V2]チェックボックスをオフにします。 CLIを使用してDockerCompose V2を無効にするには、次のコマンドを実行します。 $ docker-compose disable-v2 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む