- 投稿日:2019-06-22T23:00:13+09:00
2019年にRails + Ajaxを整理してみる(サンプルアプリ&コード付き)
Rails上でAjaxを動かす、という良くありそうな話。
ただ調べてみるとやり方が色々あって、
Rails歴半年ちょいの私には何が正しいのかさっぱり分からなかった。という訳で整理してみる。同じ境遇にある人の助けになれば嬉しい。
jQueryとかを用いた、古式ゆかしい(らしい)やり方ですので、
Vue/React等をお使いの方々はおかえりください(涙)間違ってる部分とかあったら、コメントいただければ幸いです。
(特に図の部分)この記事がよく刺さりそうな人
- Railsの基礎はわかる
- Ajaxの雰囲気はわかる
- JavaScript & jQueryも本気出せばちょっと書ける
(決してチョットデキルではない)- RailsでAjaxはあまりやった事がない
もしくは「良く分からんけどまぁ動いてるからヨシ!」で乗り切ったとりあえず結論
Rails + Ajax の実現方法は、
ざっくり以下の3パターン&その組み合わせっぽい。1. Rails推奨方式
2. フロントはJavaScriptだけでやる方式
3. AjaxのリクエストはRails & レスポンス以降はJavaScriptでやる方式方式別サンプルアプリ&コード
作ったのは、フォームに文字を入力&ボタンを押すと、Ajaxを使って文字が書き換わるアプリ。
(アプリを名乗るのはおこがましいかもしれない)これを上の3方式のそれぞれでやってみた。
Ajaxでやる意味ある?というツッコミは無しで...コードは本当に必要最低限なので、色々細かいツッコミはご容赦くださいm(_ _)m
逆に手元で再現する分にはやりやすいはず。。。あとjQueryを使ってます。入れ方は以下参照。
Rails 5.2 jQuery 動かし方 - Qiita共通処理
サーバ(Rails)側の処理は、3つの方式でほぼ共通。
routes.rbRails.application.routes.draw do get 'static/top' post 'static/ajax_update', to: 'static#ajax_update' post 'static/ajax_update2', to: 'static#ajax_update2' endstatic_controller.rbclass StaticController < ApplicationController def top end # 1. Rails推奨方式 で使用 def ajax_update @text = params[:data] render end # 2. フロント側はJavaScriptだけでやる方式 # 3. AjaxのリクエストはRails & レスポンス以降はJavaScriptでやる方式 # で使用 def ajax_update2 @text = params[:data] render plain: @text end end1. Rails推奨方式
Railsガイドに書かれたやり方
Rails で JavaScript を使用する - Rails ガイドajax_update.js.erbvar user = '<%= "#{ @text }" %>' $('#ajax-test1').text(user);top.html.slimh1 Static#top p Find me in app/views/static/top.html.slim #ajax-test1 Ajax: Rails依存 #ajax-request1 = form_with url: static_ajax_update_path do |f| = f.text_field :data = f.submit 'Post Ajax'図に表すと多分以下の感じ。
もうガッツリRailsに乗っかっている状態。肝は以下2点
- xxx.js.erbからJavaScriptをレンダリングしてフロントに返す
- フロント側で受け取ったJavaScriptを実行
個人的にはRails側で、JavaScriptをレンダリングしている辺り、
少し気持ち悪い。。。ただコード量は必要最低限で済むし、
Rails推奨であることからトラブルも起きにくそう。
基本はこれでいいのではないだろうか。なお細かい処理がしたい場合には不便になることもある様子で、
何だかんだ使わないと言う話もあるらしい。
参考:https://qiita.com/ka215/items/dfa602f1ccc652cf28882. フロント側はJavaScriptだけでやる方式
Railsにあえて叛逆していくやり方。
top.html.slim#ajax-test4 Ajax: ほぼJS(jQuery) = text_field_tag 'static[ajax_data2]' = button_tag 'Post Ajax', id: 'btn2'ajax_request_response.js$(document).ready( () => { $('#btn2').on('click', (e) => { e.preventDefault(); const param = $('#static_ajax_data2').val(); // CSRFトークンを取得&セット $.ajaxPrefilter( (options, originalOptions, jqXHR) => { if (!options.crossDomain) { const token = $('meta[name="csrf-token"]').attr('content'); if (token) { return jqXHR.setRequestHeader('X-CSRF-Token', token); } } }); $.ajax({ url: `/static/ajax_update2`, type: 'POST', data: { data: param } }) .done( (data, textStatus, jqXHR) => { var result = $('#ajax-test4'); result.text(data); }); }); });図に表すと多分以下の感じ。
Ajaxに関しては、Railsには頼らないという強い意思が見える。肝は、
RailsのCSRF対策のために、
CSRFトークンの取得&セットを行なっている所。具体的なやり方は以下の記事を完全リスペクトしましたm(_ _)m
https://qiita.com/a_ishidaaa/items/7c3fa339d3bea25a9ba8ざっくり言うと、RailsではCSRFという脆弱性への対策として、
Postのリクエスト時にトークン(身分証明みたいなもの)を使っている。
ここをカバーしてあげないと、JavaScriptからPostは出来ない。そう、Railsからの叛逆に成功したと思いきや、
実はその呪縛から逃れきれていなかったのだ。
なんかエモい。なお、RailsのCSRFについての詳細は以下の記事等をご参照ください。
外部からPOSTできない?RailsのCSRF対策をまとめてみた - Qiita3. AjaxのリクエストはRails & レスポンス以降はJavaScriptでやる方式
Railsへの依存を減らしつつ、CSRF対策はRailsによろしくできるやり方。
top.html.slim= form_with url: static_ajax_update2_path, id: 'ajax-request-3' do |f| = f.text_field :data = f.submit 'Post Ajax'ajax_response.js// Ajax: form送信はRails、受信以降はJS $( () => { $('#ajax-request-3').on('ajax:success', (e) => { const result = $('#ajax-test3'); result.text(e.detail[0]); }); });図に表すと多分以下の感じ。
折衷案な雰囲気。肝は、
RailsとJavaScriptの間で、
どのようにデータをやり取りされるかの理解が必要な所。適当にやってると変なハマり方をしそう。。。
ただそこさえクリアすれば、
Railsっぽさと自由度をある程度両立できる?気がする(よく分かってない)まとめ
- 基本的には大人しく「1. Rails推奨方式」を使った方がいい気がする。
(特に経験浅めの人)- ただ不便な場合もある(らしい)ので、
その際は「3. AjaxのリクエストはRails...」を採用、
もしくは「1. Rails推奨方式」と組み合わせて使えば良さそう。- Railsで開発するけどなるべく依存したくないというワガママな人は、
「2. フロント側はJavaScriptだけでやる方式」を使えばいい...のか?参考サイト
以下本記事作成に際しお世話になったサイト。見ると理解がすごく深まる。。。
Ruby on RailsのAjax処理のおさらい - Qiita
Rails 5.1+jQueryでajaxを試す (罠にハマる) - Qiita
Rails 5.2 jQuery 動かし方 - Qiita
RailsでのAjax - Qiita
jQuery.ajax()のまとめ: 小粋空間
Rails 5.2 jQuery 動かし方 - Qiita
- 投稿日:2019-06-22T22:24:48+09:00
railsで複数ワードでの検索機能(end)とマイナス検索機能(-)を実装してみる
はじめに
半年前にrailsで複数ワードでの検索機能(or)とマイナス検索機能(-)を実装してみるという記事を投稿させてもらったのですが、沢山の方に見てもらい良質なフィードバックまで頂きました。本当にありがとうございます!
そこで記事の続きという訳では無いですが、複数ワードでの検索時にorでは無くendで検索ができるようなコードをフィードバックを踏まえて書いてみました。また今回も完成品のコードを最後に置いておきます。環境
Ruby 2.3.3
Rails 5.2.3
MySQL 8.0.13前提として
まず前提としてこんな感じの検索フォームからコントローラの方に検索ワードを送る。
index.html.erb<%= form_tag('/items/search', method: :get) do %> <input id="page_name" name='keyword' size="30" type="text" /> <% end %>その後にコントローラで入力された検索ワードに合わせてデータを引っ張ってきて表示する。
search.html.erb<% @items.each do |item| %> <%= item.name %> <% end %>ちなみにDBにはこんなデータが入っているとします。
mysql> select name from items; +------------------+ | name | +------------------+ | 水筒A | | 水筒B | | 水筒C | | 大きい水筒 | | 大きい水筒A | | 小さい水筒 | | 小さい水筒A | +------------------+ 7 rows in set (0.00 sec)前提のコード(orとマイナス検索)
# キーワード分割 keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?) # 普通のキーワードとマイナスのキーワードを分ける negative_keywords, positive_keywords = keywords.partition {|keyword| keyword.start_with?("-") } # 空のモデルオブジェクト作成(何も入っていない空配列のようなもの) @items = Item.none # 検索ワードの数だけor検索を行う positive_keywords.each do |keyword| @items = @items.or(Item.where("name LIKE ?", "%#{keyword}%")) end # -(マイナス)がついた検索ワードの数だけnot検索を行う negative_keywords.each do |keyword| @items.where!("name NOT LIKE ?", "%#{keyword.delete_prefix('-')}%") end前回の記事でkg8mさんから教えて頂いた複数ワードでの検索機能(or)とマイナス検索機能(-)のコードです。短くて素敵。これをorからandに変えていく形で解説を入れながらコードを書いていきます。
実際に書いてみる
必要な作業は
1,送られてきたキーワードを空白で区切る(キーワード分割)
2,普通のキーワードと-(マイナス)のついたキーワードを分ける
3,普通のキーワード群でAND検索を行う
4,-(マイナス)のキーワードでNOT検索を行う早速1から行きます。
1、送られてきたキーワードを空白で区切る(キーワード分割)
検索フォームから送られてきた「水筒 A -小さい」のようなキーワードの文字列を「"水筒","A","-小さい"」と空白で区切って複数のワードに分ける作業です。この時はまだマイナスは気にしません。
items_controller.rbdef search # キーワード分割 keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?) endparams[:keyword]に検索フォームで入力された検索ワードが入っています。
それをsplitメソッドで分割を行います。[:blank:]は簡単に言ったら空白やタブという意味です(この記事を参考にしました)。つまり空白で区切って配列にするぜ!という意味になります。その後にselectメソッドで配列から何も入っていない要素を削除します。このメソッドの使い方はリファレンスを見るとわかりやすい。あ、present?はちょっと説明が難しいのですが何か値があるか?で真と偽を返すようです。
そもそも空白で区切ってなんで配列の要素にそんなのがあんだよ!ってなるんですが、前回の記事のここに理由を書いてあるので気になった方は読んでみてください。2、普通のキーワードと-(マイナス)のついたキーワードを分ける
「"水筒","A","-小さい"」のように配列になったキーワード群を普通のキーワードの配列とマイナスのキーワードの配列に分けます。
items_controller.rbdef search # キーワード分割 keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?) # 普通のキーワードとマイナスのキーワードを分ける negative_keywords, positive_keywords = keywords.partition {|keyword| keyword.start_with?("-") } end
keywords
に配列で検索ワードが入っています。これをpartitionメソッドで普通の検索ワードが入った配列とマイナスの検索ワードが入った配列に分けます。
partitionメソッドは配列の要素1つ1つを調べて真になったら1つめの配列型の変数に(今回の場合はnegative_keywords)、偽だったら2つめの配列型の変数に(今回の場合はpositive_keywords)に入れ直してくれます。
今回は1つ1つのキーワードに対してstart_with?("-")としているので要素の先頭が「-」だったらnegative_keywordsに要素を入れるといった動きになります。3,普通のキーワード群でAND検索を行う
ここが一番大切なポイントです!この部分はor検索の時は以下のようになっていました。
items_controller.rb# 検索ワードの数だけor検索を行う positive_keywords.each do |keyword| @items = @items.or(Item.where("name LIKE ?", "%#{keyword}%")) endキーワードを1つづつwhereで検索をかけてそれをorで繋げる感じ。ただ今回はandなので普通にwhere句を重ねていく。
items_controller.rbdef search # キーワード分割 keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?) # 普通のキーワードとマイナスのキーワードを分ける negative_keywords, positive_keywords = keywords.partition {|keyword| keyword.start_with?("-") } # Itemモデルオブジェクト作成 @items = Item # 検索ワードの数だけand検索を行う positive_keywords.each do |keyword| @items = @items.where("name LIKE ?", "%#{keyword}%") end endこれでいけるはず。
ただし@items = Item
はどうなんでしょうかね?これOKなんですかね?4,-(マイナス)のキーワードでNOT検索を行う
ここは元の所と変える必要が無かった。はずだった。
とりあえず元のコードを解説。items_controller.rb# -(マイナス)がついた検索ワードの数だけnot検索を行う negative_keywords.each do |keyword| @items.where!("name NOT LIKE ?", "%#{keyword.delete_prefix('-')}%") endNOT LIKEでキーワードに引っかかったデータを除外している。delete_prefixメソッドは文字列の先頭に引数の文字があれば削除するというもの。negative_keywordsにはマイナスキーワードが ["-小さい","-コンパクト"] といった感じで入っていますが、これをこのまま「-小さい」で検索しても「小さい」にはヒットしません。
なのでdelete_prefixを使い先頭の-を削除してから検索をしている訳です。で、問題はここから。このdelete_prefixメソッドはRubyのバージョンが2.5で実装されたメソッドなので自分の開発環境の2.3では使えない。このメソッドを教えて頂いた前回の記事ではあろうことか開発環境の項目にrailsとMysqlだけでRubyのバージョンを書いていないというアホみたいな事をやらかしているという・・・すまねぇすまねぇ。
しょうがないのでdelete_prefixを使わない方向で実装する。
items_controller.rbdef search # キーワード分割 keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?) # 普通のキーワードとマイナスのキーワードを分ける negative_keywords, positive_keywords = keywords.partition {|keyword| keyword.start_with?("-") } # Itemモデルオブジェクト作成 @items = Item # 検索ワードの数だけand検索を行う positive_keywords.each do |keyword| @items = @items.where("name LIKE ?", "%#{keyword}%") end # マイナスキーワードの先頭から-を取り除く negative_keywords.each {|word| word.slice!(/^-/) } # マイナスキーワードでnot検索 negative_keywords.each do |keyword| next if keyword.blank? @items = @items.where.not("name LIKE ?", "%#{keyword}%") end endまずslice!メソッドでマイナスキーワードから先頭の-を取り除く。そして後はnot検索を行います。keyword.blank?は「""」みたいな要素が来た時の対策です。詳しくは前回の記事のここを見てみてください。
これで完成です!
改善点
@items = Item
がちょっと気になる。
元々のor検索では@items = Item.none
だったけど、and検索だと常に検索結果が0件になる。推測だけどnoneは空のモデルを取得するって意味らしいが、アクションレコードとしてSQLが発行されて何もヒットしなかった(だから空のモデル)扱いになってるのかな?
だとすると「"水筒","大きい"」で検索した場合条件は
1.none(ヒット無し)
2.水筒
3.大きい
になるから、orだったら1~3のどれかに合致すればデータを引っ張ってこれたけど、andだと全ての条件に合致しなければいけないから何のデータにも引っかからないnoneがあると検索結果が常に0件になるんだと思う。なので@items = Item.all
で取り敢えず全データを入れてたけれど、all無しでも動いたので無しで動かしてる。アクションレコードに関しては勉強不足だなー。完成品(コード)
items_controller.rbdef search keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?) negative_keywords, positive_keywords = keywords.partition {|keyword| keyword.start_with?("-") } @items = Item positive_keywords.each do |keyword| @items = @items.where("name LIKE ?", "%#{keyword}%") end negative_keywords.each {|word| word.slice!(/^-/) } negative_keywords.each do |keyword| next if keyword.blank? @items = @items.where.not("name LIKE ?", "%#{keyword}%") end endあと今回のコードは検索フォームにキーワードが入力されていなかった場合の処理を書いていないので必要に応じて付け足してください。
おわりに
Ruby(Rails)は面白いメソッドが多いと思う。自分でガリガリ書かなくても良いようになっていて便利だと感じています。あと久しぶりに記事を書いたからマトモに書けてるか心配です・・・
- 投稿日:2019-06-22T19:22:12+09:00
現場で使うコントローラーの作成方法
概要
プログラミングスクールなどで習う
rails g controller コントローラー名
というコマンドでのコントローラー作成方法は、実際の現場では使用する頻度が少ないという。では実際の現場で使うコントローラー作成方法をどのようなものなのか。簡単にまとめてみた。なぜ現場では
rails g controller コントローラー名
というコマンドで作成しないのか?結論!!余計なファイルができると管理しづらい!
これに尽きるということ!
では実際はどうやるのか…
現場で使うコントローラー作成方法とは
1.app/controllers/の中に新しいファイルを作成。
app/controllers/ディレクトリの中に
コントローラー名_controller.rb
という新しいファイルを作成する。2.
コントローラー名_controller.rb
に、コントローラークラスを作成していく。class コントローラー名Controller endこれだけだと、クラスを定義しただけなので、コントローラーとして機能しない。
なので、app/controllers/application_controllerの機能を引き継いであげる。(
< ApplicationController
を追加)class コントローラー名Controller < ApplicationController endこれで、コントローラー名Controllerが、コントローラーとしての役割をしてくれるため、
結果 → コントローラー作成!!(ApplicationControllerの機能をすべて引き継いだ)
まとめ
以上、現場で使うコントローラー作成方法をまとめてみた。今回は実際に現場で働いた実績のあるフリーランスエンジニアの方の方法を自分なりに噛み砕いて、アウトプット。初学者ないし、これからエンジニアとして現場に入る方の参考になれば幸いです。
- 投稿日:2019-06-22T19:18:37+09:00
credentials.yml.encでRails環境変数の管理
前にdotenvを使ったRails環境変数の設定方法について記事を書いた
(dotenvを使ったRails環境変数設定)ところ、
Rails5.2以降ならcredentials.yml.encで環境変数管理ができると教えて頂いたので、備忘録を兼ねて書きます。参考記事
Rails5.2から追加された credentials.yml.enc のキホン
credentials.yml.encの編集
config
配下にcredentials.yml.enc
というファイルがアプリを作成した時に作られている。ここに認証情報を定義する。
credentials.yml.enc
を編集するためにはrails credentials:edit
を実行する必要がある。(ここではEDITORが未指定なのでEDITOR="vi"
としている)ルートディレクトリで以下を実行
$ EDITOR="vi" bin/rails credentials:edit認証情報を記載
hoge_keyを追加
credentials.yml.enc# aws: # access_key_id: 123 # secret_access_key: 345 hoge: hoge_key:hogehogehoge # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. secret_key_base: 43f416e170882b7f2d3422e9c8d74c81664d9728851531.......認証情報を取り出す時
hoge_key = Rails.application.credentials[:hoge_key]
credentials.yml.enc
はmaster keyを利用して暗号化・復号され、master keyの値はconfig/master.key
に記載されている。
master.key
はデフォルトで.gitignore
に含まれており、master keyがGitリポジトリに含まれないようになっている。
.gitignore
を確認.gitignore# Ignore master key for decrypting credentials and more. /config/master.keyこれで認証情報を含んだソースをGitに上がるのを防ぐことができた。
- 投稿日:2019-06-22T18:17:30+09:00
【Rails】一覧ページ上部に検索機能を実装する ~ form_with ~
こんにちは!Railsエンジニア歴10ヶ月のshin1rokです!
プログラミング初心者の友人に検索機能の実装方法を伝授したので、その内容を公開します。バージョン
- Ruby 2.6.3
- Rails 5.2.3
- bootstrap 4.3.1(レイアウト調整のため。検索機能実装に必須ではない。)
完成イメージ
コード
# app/views/users/index.html.erb <p id="notice"><%= notice %></p> <h1>Users</h1> <div class="search-form"> <%= form_with(scope: :search, url: users_path, method: :get, local: true) do |f| %> <div class="field"> <%= f.label(:name, User.human_attribute_name(:name)) %> <%= f.text_field :name, value: @search_params[:name] %> </div> <div class="field"> <%= f.label(:gender, User.human_attribute_name(:gender)) %> <%= f.collection_radio_buttons(:gender, User.genders, :first, :first) do |r| %> <%= tag.div(class: 'form-check form-check-inline') do %> <%= r.radio_button(checked: r.value == @search_params[:gender], class: 'form-check-input') + f.label(User.human_attribute_name("gender.#{r.text}"), class: 'form-check-label') %> <% end %> <% end %> </div> <div class="field"> <%= f.label(:birthday, User.human_attribute_name(:birthday)) %> <%= f.date_field :birthday_from, value: @search_params[:birthday_from] %> ~ <%= f.date_field :birthday_to, value: @search_params[:birthday_to] %> </div> <div class="field"> <%= f.label(:prefecture_id, User.human_attribute_name(:prefecture_id)) %> <%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, selected: @search_params[:prefecture_id], include_blank: t('helpers.select.include_blank')) %> </div> <div class="actions"> <%= f.submit(t('helpers.submit.search')) %> </div> <% end %> </div> <table> <thead> <tr> <th>Name</th> <th>Gender</th> <th>Birthday</th> <th>Prefecture</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @users.each do |user| %> <tr> <td><%= user.name %></td> <td><%= user.gender %></td> <td><%= user.birthday %></td> <td><%= user.prefecture.name %></td> <td><%= link_to 'Show', user %></td> <td><%= link_to 'Edit', edit_user_path(user) %></td> <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </tbody> </table> <br> <%= link_to 'New User', new_user_path %>app/controllers/users_controller.rbclass UsersController < ApplicationController # indexアクション以外は不要なので省略しています。 def index @search_params = user_search_params @users = User.search(@search_params).includes(:prefecture) end private def user_search_params params.fetch(:search, {}).permit(:name, :gender, :birthday_from, :birthday_to, :prefecture_id) end endapp/models/user.rbclass User < ApplicationRecord enum gender: { unanswered: 0, male: 1, female: 2, other: 9 } belongs_to :prefecture scope :search, -> (search_params) do return if search_params.blank? name_like(search_params[:name]) .gender_is(search_params[:gender]) .birthday_from(search_params[:birthday_from]) .birthday_to(search_params[:birthday_to]) .prefecture_id_is(search_params[:prefecture_id]) end scope :name_like, -> (name) { where('name LIKE ?', "%#{name}%") if name.present? } scope :gender_is, -> (gender) { where(gender: gender) if gender.present? } scope :birthday_from, -> (from) { where('? <= birthday', from) if from.present? } scope :birthday_to, -> (to) { where('birthday <= ?', to) if to.present? } scope :prefecture_id_is, -> (prefecture_id) { where(prefecture_id: prefecture_id) if prefecture_id.present? } endGitHub shin1rok/rails_search_sample
事前準備
デバッグを効率よく行うためにBetter Errorsをインストールします。
使い方は公式をみていただければと思うのですが、インストール手順を記載しておきます。1. gemfileに以下を追記する
group :development do gem 'better_errors' gem 'binding_of_caller' end2.
bundle install
3. 適当なところで
raise
してインストールできていることを確認する※プログラミング全般に言えることですが、確認を怠ると不具合が発生した際の原因の切り分けが難しくなります。めんどうに感じるかもしれませんが、1つ1つ確認しながら進めた方が結果として近道になります。
実装方針
- 検索パラメータを入力するフォームを作る(View)
- 検索パラメータを受け取る(Controller)
- 検索処理を実行する(Model)
各検索項目について
- 名前: like検索
- 性別: ラジオボタンで選択
- 誕生日: 範囲検索
- 都道府県: プルダウン選択
テーブル構成
ユーザテーブルと都道府県テーブルを作成します。
ユーザテーブルにprefecture_idを外部キーとして設定するため、都道府県テーブルから先に作成する必要があります。db/schema.rbActiveRecord::Schema.define(version: 2019_05_27_215135) do create_table "prefectures", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.string "name" t.integer "gender", default: 0, null: false t.date "birthday" t.bigint "prefecture_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["prefecture_id"], name: "index_users_on_prefecture_id" end add_foreign_key "users", "prefectures" endコマンド例
rails g model prefecture name:string --skip-test-unit --no-api --no-jbuilder rails g scaffold user name:string gender:integer birthday:date prefecture:references --skip-test-unit --no-api --no-jbuilderオプションの詳細は「rails scaffold option」とかで調べてみてください。
View
Viewで行なっていることは以下の通りです。順番に解説していきます。
- form_withで入力フォームを作る
- 検索後に検索フォームに入力された値を保持するようにする
- I18n対応
- ラジオボタンを横並びにする(Bootstrap4)
※ Viewの解説が一番難しい & 長いので、さらっと読み飛ばして、先にController, Modelを読む方がいいかもです。
再掲
# app/views/users/index.html.erb <p id="notice"><%= notice %></p> <h1>Users</h1> <div class="search-form"> <%= form_with(scope: :search, url: users_path, method: :get, local: true) do |f| %> <div class="field"> <%= f.label(:name, User.human_attribute_name(:name)) %> <%= f.text_field :name, value: @search_params[:name] %> </div> <div class="field"> <%= f.label(:gender, User.human_attribute_name(:gender)) %> <%= f.collection_radio_buttons(:gender, User.genders, :first, :first) do |r| %> <%= tag.div(class: 'form-check form-check-inline') do %> <%= r.radio_button(checked: r.value == @search_params[:gender], class: 'form-check-input') + f.label(User.human_attribute_name("gender.#{r.text}"), class: 'form-check-label') %> <% end %> <% end %> </div> <div class="field"> <%= f.label(:birthday, User.human_attribute_name(:birthday)) %> <%= f.date_field :birthday_from, value: @search_params[:birthday_from] %> ~ <%= f.date_field :birthday_to, value: @search_params[:birthday_to] %> </div> <div class="field"> <%= f.label(:prefecture_id, User.human_attribute_name(:prefecture_id)) %> <%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, selected: @search_params[:prefecture_id], include_blank: t('helpers.select.include_blank')) %> </div> <div class="actions"> <%= f.submit(t('helpers.submit.search')) %> </div> <% end %> </div> <table> <thead> <tr> <th>Name</th> <th>Gender</th> <th>Birthday</th> <th>Prefecture</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @users.each do |user| %> <tr> <td><%= user.name %></td> <td><%= user.gender %></td> <td><%= user.birthday %></td> <td><%= user.prefecture.name %></td> <td><%= link_to 'Show', user %></td> <td><%= link_to 'Edit', edit_user_path(user) %></td> <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </tbody> </table> <br> <%= link_to 'New User', new_user_path %>form_withで入力フォームを作る
入力フォームを実装するためには大きく2ステップあります。
- 入力フィールドをinputタグで作る(text_field, collection_radio_buttons, collection_select, date_fieldなど)
- 入力された情報をformタグによってサーバに送れるようにする(form_with)
Railsではそれらを効率よく作るためにViewHelper(↑のかっこ内のメソッドのことです。)が用意されています。
ViewHelperを使いこなせるようになると一気に生産性が高まるので、可能な限りViewHelperを使っていきましょう。1
とりあえず動くようにする → ブラウザからDeveloperToolsでHTMLを確認 → 正しく修正、というのを繰り返すことで少しずつ身に付いていくと思います。※Rails5.1以上の場合、form_for, form_tagではなくて、form_withを使いましょう。
Railsガイド form_forとform_tagのform_withへの統合form_with
form_with
メソッドを用いてformタグ
を生成できるようにします。検索パラメータはControllerにおいて
params[:search]
のHash形式(厳密にはHashではなくActionController::Parametersオブジェクトです。)で受け取りたいので、そのように実装します。こちらがミニマムで作成した検索フォームです。
(formだけだと検索できないので、名前の入力欄もついでに作っています。)
# erb <%= form_with(scope: :search, url: users_path, method: :get, local: true) do |f| %> <%= f.label(:name, '名前') %> <%= f.text_field :name %> <%= f.submit('検索') %> <% end %># ブラウザから見たHTML <form action="/users" accept-charset="UTF-8" method="get"><input name="utf8" type="hidden" value="✓"> <label for="search_name">名前</label> <input type="text" name="search[name]" id="search_name"> <input type="submit" name="commit" value="検索" data-disable-with="検索"> </form>察しの良い方は上記のerbとHTMLを見比べたらわかるかもですが、説明していきます。
scope: :search
scope: :search
、f.text_field :name
とすることでparams[:search]
にnameをKeyとしたHash
を設定することができます。>> params[:search][:name] => "なまえ"url: users_path
urlには
input type="submit"
ボタンを押した時にどのURLにリクエストを送ればよいのかを指定します。
今回はUsersControllerのindexアクションにリクエストを送りたいのでusers_path
を指定します。
Rails - 名前付きルートにおける_pathと _urlの違いと使い分けmethod: :get
検索はHTTPメソッドのGETにあたるので
method: :get
を設定ます。
Railsガイド Web上のリソースlocal: true
form_withではデフォルトで
remote: true
となっており、local: true
しない場合、XMLHttpRequest(Ajax)で通信が行われます。
XMLHttpRequestによって非同期通信を行うことができるのですが、今回は非同期通信をOFFにしたいので、local: true
にします。By default form_with attaches the data-remote attribute submitting the form via an XMLHTTPRequest in the background if an Unobtrusive JavaScript driver, like rails-ujs, is used. See the :local option for more.
text_field(名前の入力欄)
value: @search_params[:name]
とすることで検索パラメータを検索後も保持するようにしています。<%= f.text_field :name, value: @search_params[:name] %># 初期表示時 <input type="text" name="search[name]" id="search_name"> # `なまえ`で検索した場合 <input value="なまえ" type="text" name="search[name]" id="search_name">Ruby on Rails API - text_field
collection_radio_buttons(性別のラジオボタン選択)
collection_radio_buttonsでラジオボタンを作っています。
collection_radio_buttonsの実装を見てみるとこのようになっています。def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block) @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block) endGitHub Rails - collection_radio_buttons
引数についてそれぞれ解説すると
- method:
params[:search][:method]
のmethodに設定したいものを渡す。今回はgender
- collection: ラジオボタンは1つだけでは意味がないので、ラジオボタンの項目のcollectionを設定する。今回は
User.genders
- value_method: ラジオボタンの
value
に設定したい値をcollectionの要素に対するメソッドとして設定する。今回は:first
- text_method: ラジオボタンの
text
に設定したい値をcollectionの要素に対するメソッドとして設定する。今回は:first
collectionの要素に対するメソッド
見た目を整えるためのコードなど余計な部分を省いたシンプルなコードがこちらです。
<%= f.collection_radio_buttons(:gender, User.genders, :first, :first) do |r| %> <!-- r.valueと@search_params[:gender]が一致した場合はcheckedにtrueを設定する--> <%= r.radio_button(checked: r.value == @search_params[:gender]) + r.text %> <% end %><input type="hidden" name="search[gender]" value="" /> <input type="radio" value="unanswered" name="search[gender]" id="search_gender_unanswered" />unanswered <input type="radio" value="male" name="search[gender]" id="search_gender_male" />male <input type="radio" value="female" name="search[gender]" id="search_gender_female" />female <input type="radio" value="other" name="search[gender]" id="search_gender_other" />otherdate_field(誕生日の範囲指定)
誕生日が検索できるようにするために
date_field
を使います。また、誕生日はピンポイントで検索しても使いづらいので入力欄を2つ作り、範囲検索できるようにします。<%= f.date_field :birthday_from, value: @search_params[:birthday_from] %><input type="date" name="search[birthday_from]" id="search_birthday_from">今回の検索機能では問題ないですが、birthday_fromに設定される日付はString型(ハイフン区切りの文字列)であることは認識しておいた方がよいです。
Ruby on Rails API - date_field
collection_select(都道府県のプルダウン選択)
都道府県の一覧をプルダウン選択できるようにします。
<%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, selected: @search_params[:prefecture_id], include_blank: '選択') %><select name="search[prefecture_id]" id="search_prefecture_id"> <option value="">選択</option> <option value="1">北海道</option> <option value="2">青森県</option> 省略 <option value="46">鹿児島県</option> <option value="47">沖縄県</option> </select>基本的な使い方は
collection_radio_buttons
と同じです。Ruby on Rails API - collection_select
I18n対応
I18n(Internationalization)とは、Railsアプリケーションを国際化対応させる機能です。
国際化対応と言われると難しそうに感じるかもしれませんが、ymlファイルに定義した言語ごとの文字を表示するだけです。「国際化対応しないなら使わなくていいのでは?」と思うかもしれませんが、I18nを使うことで表記揺れ2を防ぐことにもなるので、日本限定のサービスでも使用するのが一般的です。(※筆者の観測範囲では)
インストール方法はこちらが参考になると思います。
i18nについて
使い方
検索機能に関係のある部分だけを抜粋したymlがこちらです。
config/locales/ja.ymlja: activerecord: models: user: 'ユーザ' attributes: user: name: '名前' gender: '性別' birthday: '誕生日' prefecture_id: '都道府県' user/gender: unanswered: '未回答' male: '男性' female: '女性' other: 'その他' helpers: select: prompt: 選択してください include_blank: 選択 submit: create: 登録する submit: 保存する update: 更新する search: 検索する>> User.human_attribute_name(:gender) => "性別" >> User.human_attribute_name("gender.male") => "男性" >> t('helpers.select.include_blank') => "選択"i18nのキーを調べる方法(nestされているパターン等)
Railsでgem無しに手軽にenumをi18nに対応させる
ラジオボタンを横並びにする(Bootstrap4)
Bootstrapを使ってラジオボタンを横並びにします。
言葉で説明するよりもコードを見比べた方がわかりやすいと思うのでコードを貼っておきます。
# erb <%= f.collection_radio_buttons(:gender, User.genders, :first, :first) do |r| %> <%= tag.div(class: 'form-check form-check-inline') do %> <%= r.radio_button(checked: r.value == @search_params[:gender], class: 'form-check-input') + f.label(User.human_attribute_name("gender.#{r.text}"), class: 'form-check-label') %> <% end %> <% end %># 生成されたHTML <input type="hidden" name="search[gender]" value="" /> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" value="unanswered" name="search[gender]" id="search_gender_unanswered" /> <label class="form-check-label" for="search_未回答">未回答</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" value="male" name="search[gender]" id="search_gender_male" /> <label class="form-check-label" for="search_男性">男性</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" value="female" name="search[gender]" id="search_gender_female" /> <label class="form-check-label" for="search_女性">女性</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" value="other" name="search[gender]" id="search_gender_other" /> <label class="form-check-label" for="search_その他">その他</label> </div>Bootstrap移行ガイド - 横並び(Inline)から抜粋
<div class="form-check form-check-inline"> <input class="form-check-input" type="checkbox" id="inlineCheckbox1" value="option1"> <label class="form-check-label">1</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="checkbox" id="inlineCheckbox2" value="option2"> <label class="form-check-label">2</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="checkbox" id="inlineCheckbox3" value="option3" disabled> <label class="form-check-label">3(無効)</label> </div>Controller
Controllerで行なっていることは以下の通りです。順番に解説していきます。
- 受け取った検索パラメータをチェックする(Strong Parameters)
- チェックした検索パラメータを保持する(インスタンス変数)
- 検索パラメータを元に検索する(Scope, includes)
- 検索結果を返す(インスタンス変数)
再掲
app/controllers/users_controller.rbclass UsersController < ApplicationController # indexアクション以外は不要なので省略しています。 def index @search_params = user_search_params @users = User.search(@search_params).includes(:prefecture) end private def user_search_params params.fetch(:search, {}).permit(:name, :gender, :birthday_from, :birthday_to, :prefecture_id) end end1. 受け取った検索パラメータをチェックする(Strong Parameters)
パラメータを受け取る
検索パラメータの受け取り方ですが、
params[:search]
の中にKey, Valueのセットで検索パラメータが渡されるHash形式(厳密にはHashではなくActionController::Parametersオブジェクトです。)で受け取りたいと思います。params[:search]の中には{key: value}が{name: なまえ"}のHashが入っているので以下のように値を取り出すことができます。
>> params[:search][:name] => "なまえ"パラメータをチェックする
Railsには
Strong Parameters
という機能があります。Strong Parameters
とは、システム側が意図していない値を入力された場合、サーバ側で無効化する仕組みです。悪意のあるユーザなどからの攻撃を防ぐため、受け取った値をサーバ側でチェックすることはセキュリティ上必須です。
Railsガイド(Strong Parameters)を確認してください。Strong Parametersを利用して実装したメソッドがこちらです。
def user_search_params params.fetch(:search, {}).permit(:name, :gender, :birthday_from, :birthday_to, :prefecture_id) endfetchメソッド
params.fetch(:search, {})
は、params[:search]が空の場合{}
をparams[:search]が空でない場合、params[:search]
を返してくれます。
Ruby on Rails API - fetchちなみに、
fetch(:search, {})
をrequire(:search)
に書き換えるとparams[:search]が必須、つまり検索パラメータがない場合はActionController::ParameterMissingとなり、初期表示ができなくなります。permitメソッド
permitメソッドは許可していないKeyを切り捨てるメソッドです。
user_search_params
メソッドでは引数に指定した:name, :gender, :birthday_from, :birthday_to, :prefecture_id
だけを許可して、他のKeyが渡された場合は無視します。
Ruby on Rails API - permit例えばindex.html.erbでnameを
namu
とタイポしていた場合、params[:search]ではnamu
がありますが、user_search_paramsではnamu
がなくなっています。# app/views/users/index.html.erb <%= f.text_field :namu, value: @search_params[:name] %>2. チェックした検索パラメータを保持する(インスタンス変数)
@search_params = user_search_params
としてControllerで宣言/代入することで、Viewで@search_params
を使えるようになります。3. 検索パラメータを元に検索する(Scope, includes)
検索はこの部分です。
User.search(@search_params).includes(:prefecture)
Userモデルのsearchメソッドを呼び出して(引数は
@search_params
)、結果には都道府県を含みます、と。
実装の詳細を確認せずとも内容が想像できますね。プログラミングで名前重要と言われる所以でもあります。searchメソッドはRailsのScopeという仕組みで実装しています。解説はModelの方を参照してください。
includes
includesはN + 1問題に対応するために行なっています。includesすることでSQLの発行回数を少なくすることができます。
また、N + 1問題のN + 1とは以下の合計のことです。
@users
を取得するためにSQLを実行する(1)@users
はprefecture_idを持っているけど、都道府県名は持っていないので、prefecture_idをKeyにSQLを実行する(N)ユーザ一覧ページを初期表示した際の、SQLがこちらです。
(件数が多いと見辛いので、limit(5)しています。)# User.search(@search_params).limit(5) Started GET "/users" for 127.0.0.1 at 2019-06-03 22:54:28 +0900 Processing by UsersController#index as HTML Rendering users/index.html.erb within layouts/application Prefecture Load (0.5ms) SELECT `prefectures`.* FROM `prefectures` ↳ app/views/users/index.html.erb:28 User Load (0.6ms) SELECT `users`.* FROM `users` LIMIT 5 ↳ app/views/users/index.html.erb:49 Prefecture Load (1.6ms) SELECT `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 5 LIMIT 1 ↳ app/views/users/index.html.erb:54 Prefecture Load (0.5ms) SELECT `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 35 LIMIT 1 ↳ app/views/users/index.html.erb:54 Prefecture Load (0.4ms) SELECT `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 7 LIMIT 1 ↳ app/views/users/index.html.erb:54 Prefecture Load (0.5ms) SELECT `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 18 LIMIT 1 ↳ app/views/users/index.html.erb:54 Prefecture Load (0.4ms) SELECT `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 8 LIMIT 1 ↳ app/views/users/index.html.erb:54 Rendered users/index.html.erb within layouts/application (38.0ms) Completed 200 OK in 188ms (Views: 180.7ms | ActiveRecord: 4.4ms) # User.search(@search_params).includes(:prefecture).limit(5) Started GET "/users" for 127.0.0.1 at 2019-06-03 22:55:45 +0900 Processing by UsersController#index as HTML Rendering users/index.html.erb within layouts/application Prefecture Load (0.7ms) SELECT `prefectures`.* FROM `prefectures` ↳ app/views/users/index.html.erb:28 User Load (0.6ms) SELECT `users`.* FROM `users` LIMIT 5 ↳ app/views/users/index.html.erb:49 Prefecture Load (3.4ms) SELECT `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` IN (5, 35, 7, 18, 8) ↳ app/views/users/index.html.erb:49 Rendered users/index.html.erb within layouts/application (211.5ms) Completed 200 OK in 400ms (Views: 313.4ms | ActiveRecord: 61.7ms)includesなしの方はSQLの発行が6回なのに対して、includesありの方はSQLの発行が2回になっています。
仮にユーザが10万人いる場合でもincludesありの方はSQLの発行が2回で済むため、パフォーマンスがよくなります。4. 検索結果を返す(インスタンス変数)
@users
に検索結果を代入します。Model
Modelで行なっていることは以下の通りです。順番に解説していきます。
- Enumでgenderの値を定義する
- Active Record Associations(関連付け)でUserとPrefectureの関係を定義する
- Scopeで検索用のメソッドを定義する
再掲
app/models/user.rbclass User < ApplicationRecord enum gender: { unanswered: 0, male: 1, female: 2, other: 9 } belongs_to :prefecture scope :search, -> (search_params) do return if search_params.blank? name_like(search_params[:name]) .gender_is(search_params[:gender]) .birthday_from(search_params[:birthday_from]) .birthday_to(search_params[:birthday_to]) .prefecture_id_is(search_params[:prefecture_id]) end scope :name_like, -> (name) { where('name LIKE ?', "%#{name}%") if name.present? } scope :gender_is, -> (gender) { where(gender: gender) if gender.present? } scope :birthday_from, -> (from) { where('? <= birthday', from) if from.present? } scope :birthday_to, -> (to) { where('birthday <= ?', to) if to.present? } scope :prefecture_id_is, -> (prefecture_id) { where(prefecture_id: prefecture_id) if prefecture_id.present? } endEnumでgenderの値を定義する
Enumは取りうる値の範囲が決まっている時にその値を定義することでRailsが便利な機能やメソッドを提供してくれる仕組みです。
例えば、Enumで定義していない値を設定しようとするとArgumentErrorになります。
>> User.new(gender: 3) !! #<ArgumentError: '3' is not a valid gender>他にも便利なメソッドが使えるようになります。
>> User.genders => {"unanswered"=>0, "male"=>1, "female"=>2, "other"=>9} >> User.genders[:male] => 1 >> User.new(gender: 1).gender => "male" >> User.new(gender: 1).male? => trueRuby on Rails API - Enum
システムで「性別」の情報を扱う前に知っておくべきこと
Active Record Associations(関連付け)でUserとPrefectureの関係を定義する
belongs_to :prefecture
とし、UserモデルとPrefectureモデルの関連付けを定義することで、user.prefecture.name
と書けるようになります。
(Viewで使っています。)>> User.first.prefecture.name => "秋田県"Scopeで検索用のメソッドを定義する
Scopeとは、ActiveRecordのQueryメソッドに名前を付ける機能です。戻り値はActiveRecord::Relationオブジェクトなので、ScopeからScopeを呼び出すことも可能です。
app/models/user.rbclass User < ApplicationRecord # 省略 scope :search, -> (search_params) do # search_paramsが空の場合以降の処理を行わない。 # >> {}.blank? # => true return if search_params.blank? # パラメータを指定して検索を実行する name_like(search_params[:name]) .gender_is(search_params[:gender]) .birthday_from(search_params[:birthday_from]) .birthday_to(search_params[:birthday_to]) .prefecture_id_is(search_params[:prefecture_id]) end # nameが存在する場合、nameをlike検索する scope :name_like, -> (name) { where('name LIKE ?', "%#{name}%") if name.present? } # gender_isが存在する場合、gender_isで検索する scope :gender_is, -> (gender) { where(gender: gender) if gender.present? } # birthdayが存在する場合、birthdayで範囲検索する scope :birthday_from, -> (from) { where('? <= birthday', from) if from.present? } scope :birthday_to, -> (to) { where('birthday <= ?', to) if to.present? } # prefecture_idが存在する場合、prefecture_idで検索する scope :prefecture_id_is, -> (prefecture_id) { where(prefecture_id: prefecture_id) if prefecture_id.present? } endRailsガイド(Scope)
nil? empty? blank? present? の使い分け
この記事でやっていないこと
helperへの切り出し
collection_radio_buttonsが1行になるように切り出したいですよね。こんな感じで。
app/helpers/application_form_builder.rbclass ApplicationFormBuilder < ActionView::Helpers::FormBuilder include ActionView::Helpers::TagHelper include ActionView::Context def inline_radio_buttons(method:, collection:, checked:) collection_radio_buttons(method, collection, :first, :first) do |r| tag.div(class: 'form-check form-check-inline') do r.radio_button(checked: r.value == checked, class: 'form-check-input') + self.label(r.text, class: 'form-check-label') end end end endformオブジェクト
感想
普段自分が行なっていることを技術記事という形で言語化することで、深掘りできてよかったです。
今後勉強するときは「記事にできるくらいちゃんと理解しているか?」という視点を持ちつつ勉強していくぞい?('ω'?)
- 投稿日:2019-06-22T17:39:38+09:00
#Rails で has_many の association に対してネストしたハッシュを得る
- 投稿日:2019-06-22T16:55:31+09:00
EC2へcapistranoを使ってrailsアプリケーションをデプロイしている場合のログ確認
EC2でrailsアプリケーションをデプロイしています。
その際にページへアクセスした際、以下のようなエラーが、、The page you were looking for doesn't exist.
You may have mistyped the address or the page may have moved.If you are the application owner check the logs for more information.
railsがうまく動いていない時のエラーのようなので、
エラーログをみていきます。見るべきログファイルの場所
アプリケーションディレクトリ内にあるcurrentというフォルダ
capistranoを使っている場合、ルートディレクトリはこのフォルダになるとのこと
アプリーケーション/current/log
このディレクトリに production.log unicorn.stderr というファイルがある。これらのファイルを確認していく
logの確認コマンド
$ less production.log↑ログの一番上から表示させる。
tail production.log↑ログの末尾から表示させる。
tailf production.log↑ログをリアルタイム更新で確認する
今回はtailfでproduction.logを確認した所、リアルタイムに動いていなかったため
それよりも前の段階でエラーが起きていると考えました。次にunicorn.stderrを確認したい所だったのですが、
先にunicornが動いているか確認したと所動いていませんでした。bundle exec cap production unicorn:startで解決しました。
- 投稿日:2019-06-22T16:42:52+09:00
rails g ~ で指定のディレクトリ下にフォルダを格納したい場合
超基本ですが、一応メモってことで書いときます。
apiモードで作成する時、app/controllers/api/v1の下にuserコントローラーを置きたい時は、
terminal$ rails g controller api::v1::users index show new create edit update destroyこれで、
app/controllers/api/v1/user_controller.rb # これが作成されるルーティングは、こうですね。
config/routes.rbnamespace 'api' do namespace 'v1' do resources :users end end
- 投稿日:2019-06-22T16:42:52+09:00
rails g ~ でapp/controllers/api/v1みたいな感じでファイルを格納したい場合
超基本ですが、一応メモってことで書いときます。
apiモードで作成する時、app/controllers/api/v1の下にuserコントローラーを置きたい時は、
terminal$ rails g controller api::v1::users index show new create edit update destroyこれで、
app/controllers/api/v1/user_controller.rb # これが作成されるルーティングは、こうですね。
config/routes.rbnamespace 'api' do namespace 'v1' do resources :users end end
- 投稿日:2019-06-22T15:25:55+09:00
RubyのバージョンアップをしたらHerokuにデプロイできなくなった
tl;dr
個人でつくってるプロダクトで
ふと思い立って、Rubyのバージョンアップを行ったら
Herokuにデプロイできなくなった
それの解決方法というか、、
原因
Rubyのバージョンアップの際に、
gem install bundler
をして
Bundlerのバージョンが2.0.2
となったからっぽい$ bundle -v Bundler version 2.0.2Gemfile.lockBUNDLED WITH 2.0.2Bundlerを
2.0.1
で入れ直して、Gemfile.lock
もつくりなおす$ gem uninstall bundler$ gem install bundler -v 2.0.1 $ bundle -v Bundler version 2.0.1$ rm Gemfile.lock $ bundle --without productionこれでできる
$ git push heroku masterおまけ
バージョンアップしたらRailsコマンドが叩けなくなった
rbenv: rails: command not found The `rails' command exists in these Ruby versions:$ rbenv exec gem install bundler $ rbenv rehash $ gem install rails
- 投稿日:2019-06-22T10:46:41+09:00
Rails で名前付きルーティングと通常ルーティングを併用したいとき
はじめに
Rails の URL で、通常の
/hoge/:id
のようなルーティングと/hoge/fuga
のような名前付きルーティングを併用したいという課題がありました。その際どのように対応したのかを備忘として残しておきます。
前提環境
- Rails 5.2.2.1
サンプル例
name
カラムのあるPost
モデルを例にします。このモデルの
name
カラムに文字列が入っていれば、id
ではなくname
の文字列を URL として使うことを想定します。
name
が空 →/posts/1
name
にhoge
と入っている →/posts/hoge
方針
方針としては、Rails ガイド を参考に、以下のように進めました。
- routes で利用するパラメータ識別子修正
- ActiveRecord::Base#to_param をオーバーライド
- 名前付きルーティング・通常ルーティングどちらでもレコードを find できるメソッド作成
対応
routes で利用するパラメータ識別子修正
まず、routes のパラメータ識別子を修正します。
param
オプションを使うことで可能です。
id
かname
で find したかったので、id_or_name
というパラメータ名にしています。Rails.application.routes.draw do # 中略 resources :posts, param: :id_or_name # 中略 endActiveRecord::Base#to_param をオーバーライド
次に、
ActiveRecord::Base#to_param
をオーバーライドします。
name
があればname
を、存在しなければそのままid
を利用する形としました。class Post < ApplicationRecord # 中略 def to_param return name if name.presence super end end名前付きルーティング・通常ルーティングどちらでもレコードを find できるメソッド作成
最後に、
Post
モデルに名前付きルーティング・通常ルーティングどちらでもレコードを find できるメソッドを追加します。
id
かname
でレコードを検索し、どちらかがマッチしていればそのレコードを返却します。
存在しない場合は一応ActiveRecord::RecordNotFound
で例外処理しています。class Post < ApplicationRecord # 中略 def self.find_by_id_or_name(id_or_name) if object = self.where(id: id_or_name).or(self.where(slug: id_or_name)).first object else raise ActiveRecord::RecordNotFound end end end利用するときは以下のようなメソッドの呼び出し方となります。
Post.find_by_id_or_name(params[:id_or_name])終わりに
name
重複する場合など、考慮する点はまだあるかも知れませんが、一旦こちらで対応はできました。もっと良いやり方などありましたらコメントいただけるとありがたいです!
参考
- 投稿日:2019-06-22T10:46:05+09:00
Rails ~会員登録~
これまでのあらすじ
- 仮登録でメールアドレスを登録
- 登録したメールアドレスへ本登録画面に飛ぶURLを記載したメールを送信
- メールでURLへ遷移
- 登録に必要な情報の入力
- 登録完了
- ログイン
- ログアウト
までやっていきたいと思います。
メール送信
前の記事でユーザ仮登録のモーダルウィンドウ表示まで実装しました。
で、登録ボタンで以下のアクションを呼びます。top_controller.rbdef create @temp_user = TempUser.create_temp_user(temp_users_params) respond_to do |format| if @temp_user.save UserMailer.with(temp_user: @temp_user, locale: params[:locale]).request_registration.deliver_later format.js { @status = "success" } else format.js { @status = "fail" } end end endTempUserクラスの#create_temp_userメソッドで初期化します。
temp_user.rb# 仮ユーザの作成 def create_temp_user(params) # temp_usersにmail_addressで検索 初期化する temp_user = find_or_initialize_by(mail_address: params[:mail_address]) temp_user.last_name = params[:last_name] temp_user.first_name = params[:first_name] temp_user.token = create_token temp_user.expired_at = DateTime.now + 1 return temp_user endfind_or_initialize_byでメールアドレスを検索します。
もし、仮登録済みで本登録していないメールアドレスの場合、後勝ちにして最後に入力した情報でUPDATEします。
もし、temp_userにない場合は、入力情報でINSERTします。
find_or_initialize_byを使用し、saveをするタイミングでINSERTかUPDATEか判定、UPSERTが実現できます。expired_at(有効期限)は、とりあえず1日にしていますが、設定を外出ししたいですね。
saveメソッドの戻り値でSQLの成功・失敗を判定して処理を判定しています。
成功時は、登録情報を元にUserMailerでメールを作成しています。
user_mailer.rbclass UserMailer < ApplicationMailer default from: "hogehoge@gmail.com" def request_registration @temp_user = params[:temp_user] @locale = params[:locale] mail(to: @temp_user.mail_address, subject: I18n.t("mailers.user_mailer.request_registration.subject")) end endApplicationMailerを継承したUserMailerです。
from:は送信元のメールアドレスを設定します。
request_registrationがメール送信の本体です。
paramsで引数を受け取ってメールを生成します。メール本体は、viewsの下に作ります。
request_registration.html.erb<%= stylesheet_link_tag "mailers/request_registration.css", media: "all" %> <p class="message"><%= t('mailers.user_mailer.request_registration.dear', last_name: @temp_user.last_name, first_name: @temp_user.first_name) %></p> <pre class="message"><%= t('mailers.user_mailer.request_registration.message_text', expired_at: l(@temp_user.expired_at, format: :default)) %></pre> <div class="btn"> <%= link_to t('mailers.user_mailer.request_registration.button'), {controller: 'account', action: 'regist', locale: @locale, token: @temp_user.token } %> </div>メソッド名のhtml.erb(HTMLメール)またはtext.erb(テキストメール)を雛形として作ります。
上記は、HTMLメールの雛形です。link_toでaccount_controller.rbのregistメソッドを指定しています。temp_user登録の際、生成したtokenをGETリクエストをパラメタとしてURLに付与し、link_toで生成しています。
メールを受信する
開発中に動作確認したいですが、実際に送ると、誤送信する恐れがあるので、以下のgemを入れます。
Gemfile# letter_opener_web gem 'letter_opener_web', '~> 1.0'このgemはメールを送信・受信してくれます。
config/environments/development.rbに設定を追加します。development.rbconfig.action_mailer.delivery_method = :letter_opener_webまた、routes.rbに以下のパスを設定し、送信メールを確認できるようにします。
routes.rb# letter_opener_web mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?これで(http://localhost:3000/letter_opener) でアプリが送信したメールをブラウザで確認できます。
登録
メールに記載されたURLを押下すると、登録に必要な情報を入力する画面へ遷移できます。
パスワードについて
ライブラリを使わないでログイン機能を実装するために以下のgemを使います。
Gemfile# Use ActiveModel has_secure_password gem 'bcrypt', '~> 3.1.7'Userモデルにhas_secure_passwordを追加します。
user.rbclass User < ApplicationRecord include ActiveModel::Validations has_secure_password validations: true validates :last_name, presence: true validates :first_name, presence: true validates :last_name_roman, upper_case_format: true, unless: Proc.new { |p| p.last_name_roman.blank? } validates :first_name_roman, upper_case_format: true, unless: Proc.new { |p| p.first_name_roman.blank? } validates :sex, inclusion: { in: [0, 1] } validates :birthed_on, presence: true validates :mail_address, uniqueness: true validates :password, password_format: true validate :already_used_mail_address # already used mail address def already_used_mail_address unless User.find_by(mail_address: mail_address).nil? errors.add(:mail_address, I18n.t("validate.already_use")) end end # create remember token def self.create_remember_token SecureRandom.urlsafe_base64 end # encrypt def self.encrypt(token) Digest::SHA256.hexdigest(token.to_s) end endpasswordとpassword_confirmation属性、さらにauthenticateメソッドが使用できるようになります。
さらにDB内ではpassword_digestというカラムで暗号化されたパスワードは保存されます。
(password_digestをmigrationで対象のテーブルに追加します。)アプリケーションログには、入力値は当然出力されず
DBには、暗号化されたパスワード文字列が登録されます。登録ボタンでcreateメソッドが呼び出され、登録成功したら完了画面、失敗したら再度登録画面をレンダリングします。
account_controller.rbdef create @user = User.new(users_params) if @user.save render action: :complete else render action: :regist end end
ログイン・ログアウト
application_controller.rbに以下を定義します。
application_controller.rbclass ApplicationController < ActionController::Base # filter # actionの直前に実行されるfilter before_action :set_locale before_action :current_user before_action :require_sign_in! # helper methodとして使用できる helper_method :signed_in? def set_locale I18n.locale = locale end def locale @locale ||= params[:locale] ||= I18n.default_locale end def default_url_options(options = {}) options.merge(locale: locale) end def current_user remember_token = User.encrypt(cookies[:remember_token]) @current_user ||= User.find_by(remember_token: remember_token) end def sign_in(user) remember_token = User.create_remember_token # cookieにremember_tokenをsetする cookies.permanent[:remember_token] = remember_token # remember_tokenを更新する user.update_column(:remember_token, User.encrypt(remember_token)) @current_user = user end def sign_out @current_user = nil # cookieのremember_tokenを削除する cookies.delete(:remember_token) redirect_to login_path end def signed_in? @current_user.present? end private def require_sign_in! redirect_to login_path unless signed_in? end endfilter処理として
current_userメソッド
ログイン時にCookieとDBに登録したトークン情報を突き合わせて、ユーザ情報を取得する
require_sign_in!メソッド
ユーザ情報が取得できない場合(=ログインしていない場合)ログイン画面へリダイレクトするこれをapplication_controller.rbに定義することによりapplication_controllerを継承する全てのcontrollerにこのfilter処理が走ります。
これにより画面上でログイン状態・非ログイン状態を判断するわけです。
ログイン処理はsession_controller.rbに定義しています。
session_controller.rbclass SessionsController < ApplicationController # filter # actionの直前に実行されるfilterをskipする skip_before_action :require_sign_in!, only: [:new, :create] # actionの直前に実行されるfilter before_action :set_user, only: [:create] # GET /login def new redirect_to root_path end # PUT /login def create if @user.authenticate(@session.password) sign_in(@user) else @session.sign_in_failure end render "top/index" end # DELETE /logout def destroy sign_out redirect_to login_path end private def set_user @session = Session.new(session_params) if !@session.valid? render "top/index" and return end @user = User.find_by!(mail_address: @session.mail_address) rescue @session.sign_in_failure render "top/index" end def session_params params.require(:session).permit(:mail_address, :password) end endSessionモデルはmail_addressとpasswordのログイン認証に必要な情報をもっているクラスです。
skip_before_actionは、actionの直前に実行されるfilterをskipするための宣言です。
当然ログイン画面表示とログイン処理にログイン認証のfilterが入っているとログインできないため、skipしています。createメソッドの前のみset_userのfilterが適用されます。
set_userメソッドは画面から入力されたフォーム情報を元にmail_addressでユーザ情報を取得します。
取得できない場合、例外を発生させます。例外発生時はログイン画面をレンダリングします。createメソッドは、set_userメソッドで取得したユーザ情報とパスワードを確認します。
確認できた場合、ログイン処理(application_controller.rbに定義)をします。ログイン処理は、ログイン認証していることを示す、remember_tokenを画面に保持します。
remember_tokenはUserクラスで定義したメソッドでランダム文字列で生成されます。
それをCookieに保存し、その後DB(userテーブル)UPDATEします。前述のログイン確認のfilterはこのremember_tokenを元にログインしているか、していないかを判断するというわけです。
終わりに
- 勉強会の資料なので、急いで作ったので、もうちょっと修正します。
- 参考リンクとかも記載せねば…
参考
工事中…
- 投稿日:2019-06-22T01:27:53+09:00
ActiveSupport::TimeZoneを0オフセットでインスタンス化すると1時間ずれる
指定したオフセット値(JSTなら9時間)で
ActiveSupport::TimeZone
を作り、それをもとにActiveSupport::TimeWithZone
インスタンスを作りたい。jst = ActiveSupport::TimeZone[9] # => #<ActiveSupport::TimeZone:0x00007fbbe3b034b0 @name="Osaka", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Asia/Tokyo>> jst.now # => Fri, 21 Jun 2019 21:24:26 JST +09:00+9は、問題なし。
utc = ActiveSupport::TimeZone[0] # => #<ActiveSupport::TimeZone:0x00007fbbe3afb030 @name="Casablanca", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Africa/Casablanca>> utc.now # => Fri, 21 Jun 2019 13:25:48 WEST +01:00 utc.tzinfo.current_period.utc_offset # => 0ファッ!?
+00:00
が期待されるところ+01:00
となっている!?
TZInfoのutc_offsetは0を指しているようだが...そもそもoffsetだけではtimezoneと言えないのでやろうとしていることが強引な気もするが、それはさておき、どうやるのが正解なのだろうか。
関係ありそうだが、詳しくは読んでない。
https://github.com/tzinfo/tzinfo/issues/98
動作確認環境
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin16]
activesupport (5.2.1)
tzinfo (1.2.5)
- 投稿日:2019-06-22T00:54:03+09:00
Railsの起動エラーとMySQLのエラー解除方法(個人メモ)
Railsの起動エラーと、MySQLのエラー解除方法を個人的にメモする。
【PC:MacBookPro2012(13-inch, Mid 2012)、MacOS Mojave 10.14.4】
【ruby 2.3.7p456 (2018-03-28 revision 63024) [universal.x86_64-darwin18]】
【Rails 5.2.3】
【mysql Ver 8.0.15 for osx10.14 on x86_64 (Homebrew)】MySQLのエラーについて。
まずはmysqlの場所を確認。
$ which mysql /usr/local/bin/mysqlMySQLサーバーを起動させる。
$ mysql.server start Starting MySQL ...... ERROR! The server quit without updating PID file (/usr/local/var/mysql/*****.local.pid).PIDファイルを確認する。
$ ls -la /usr/local/var/mysql/*****.local.pid ls: /usr/local/var/mysql/*****.local.pid: No such file or directoryMySQLサーバーをストップしてみる。
$ mysql.server stop ERROR! MySQL server PID file could not be found!sudoを入れて起動してみる。
$ sudo mysql.server start Password: Starting MySQL . ERROR! The server quit without updating PID file (/usr/local/var/mysql/*****.local.pid).安全にMySQLを起動してみる。
$ mysqld_safe 2019-04-24T03:34:24.6NZ mysqld_safe Logging to '/usr/local/var/mysql/*****.local.err'. 2019-04-24T03:34:24.6NZ mysqld_safe Starting mysqld daemon with databases from /usr/local/var/mysql 2019-04-24T03:34:29.6NZ mysqld_safe mysqld from pid file /usr/local/var/mysql/*****.local.pid endedとりあえずアンインストールしてみる。
$ brew uninstall mysql Uninstalling /usr/local/Cellar/mysql/8.0.15... (267 files, 234.6MB)ちゃんと削除できたか確認。
$ which mysql (何も出てこない)再度インストールしてみる。
$ brew install mysqlこれでmysql.server startしてもエラーだったので、上の操作を繰り返してみたけどダメ。
「PIDファイルが見つからない」ので、ファイルの生成をしてみる。
$ /usr/local/var/mysql/mysql > touch /usr/local/var/mysql/*****.local.pid -bash: /usr/local/var/mysql/mysql: is a directory奇跡的に起動しないか願いを込めて。
$ mysql.server start Starting MySQL ...... ERROR! The server quit without updating PID file (/usr/local/var/mysql/*****.local.pid).どこかで見つけたこのコマンドを入力してみる。
$ /usr/local/var/mysql/mysql > sudo mysql.server restart -bash: /usr/local/var/mysql/mysql: is a directory同じかい!!(ノ_<)
$ sudo chown -R _mysql:_mysql /usr/local/var/mysql /tmp/mysql.sock Password:何やらパスワードを求められた。。。いけるのか?
sudoで起動させてみよう。
$ sudo mysql.server start Starting MySQL ..... ERROR! The server quit without updating PID file (/usr/local/var/mysql/*****.local.pid).あかんのかい!(T . T)
sudoなしで起動できるようにするらしいコマンド。
$ sudo chown -R `whoami`:admin /usr/local/var/mysql /tmp/mysql.sock起動させてみる。
$ mysql.server start Starting MySQL .. SUCCESS!キターーーーーーーーー!!!!!!♪───O(≧∇≦)O────♪
よーし!早速Railsをローカル環境で表示させてみよう。Railsのシステムを起動。
$ rails sブラウザでlocalhost:3000を入力。(成功なら画面にRailsの初期画面が表示される)
なんとエラー!!恐怖の赤い画面!!
ActiveRecord::NoDatabaseError (Unknown database 'hello_sample7_development'):
え、データベースがない・・・?∑(゚Д゚)ということでデータベースを作成。
$ rails db:create Created database 'ファイル名_development' Created database 'ファイル名_test'再度Railsのシステム起動。
$ rails sRailsの初期画面が出た!!!☆:.。. o(≧▽≦)o .。.:☆
- 投稿日:2019-06-22T00:40:39+09:00
Rails+WebpackerでVue.jsとTurbolinksを同時に動かす
これは何
RailsでVue.jsとTurbolinks動かしていい感じのフロントエンド環境を構築しよう!!!
一応それぞれのざっくりとした説明。
- Rails
- みんな大好きWebフレームワーク
- Webpacker
- Railsで書いたJSやCSSをよしなにまとめてくれるやつ
- Vue.js
- 言わずと知れたJSのフロントエンドフレームワーク
- Turbolinks
- ページ移動を速くしてくれるやつ
リポジトリとバージョン情報
リポジトリ: https://github.com/rhistoba/rails_vuejs_turbolinks_template
- Ruby: 2.6.3
- Rails: 5.2.3
- Webpacker: 4.0.7
- Vue.js: 2.6.10
- Turbolinks: 5.2.0
どういう感じにVue.jsを使えるようにするのか
- Railsアクションからビューをレンダリング
turbolinks:load
時にビューからVueインスンタンスのid要素を検索- id要素が見つかればVueインスタンスを生成
- ビューのid要素以下をテンプレートとしてVueインスタンスが適用される
こんな感じで、ビューでidを指定した箇所にVueを適用するための方法を説明します。
部分的にVueを適用可能なので、いわゆる薄い使い方によりRails Wayから外れない開発が可能かと思います。手順
今回は適当に
rails new
してルートのビューだけ作成したRailsプロジェクトを対象に説明します。Webpackerを導入
GemfileにWebpackerを追加。
Gemfile# ... gem 'webpacker', '~> 4.x' # ...追加したら
bundle install
。$ bundle install以下のコマンドでプロジェクトにWebpackerをインストールする。
$ bundle exec rails webpacker:installVue.jsを導入
以下のコマンドでプロジェクトにVue.jsを追加する。
$ bundle exec rails webpacker:install:vueTurbolinksなどの導入
yarnで以下のようにパッケージを取得する。
$ yarn add turbolinks vue-turbolinks
javascript/packs/application.js
に以下を追加する。javascript/packs/application.jsimport Turbolinks from 'turbolinks' Turbolinks.start()
views/layouts/application.html.erb
で以下の一文を追加する。views/layouts/application.html.erb<!DOCTYPE html> <html> <head> ... + <%= javascript_pack_tag 'application' %> </head> ... </html>これでWebpackerにより
javascripts/packs/application.js
がビルドされるようになる。Vue.jsで完全ビルドを有効にする
ビューから取得したid要素のhtmlを、Vueインスタンスにテンプレートとして渡してコンパイルされる必要があるのですが、Vueは標準でランタイム限定ビルドのみ有効になっており、このままでは意図通りに動かせません。
参考: https://jp.vuejs.org/v2/guide/installation.html#さまざまなビルドについてそのため参考URLのページにも載っているように、Webpackの設定で完全ビルドを有効にします。
config/webpack
にvue.config.js
を作成します。config/webpack/vue.config.jsmodule.exports = { resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' } } }
config/webpack
以下の環境ごとの設定ファイルに上記の設定を取り込むようにします。
(以下はdevelopment.js
の例)config/webpack/development.jsprocess.env.NODE_ENV = process.env.NODE_ENV || 'development' const environment = require('./environment') const config = Object.assign(environment.toWebpackConfig(), require('./vue.config')) module.exports = config
environment.toWebpackConfig()
で生成される設定オブジェクトをObject.assign()
を用いて上書きしています。src/main.jsを作成
javascripts/packs/application.js
で各種Vueインスタンスを読み込む大元のファイルを作成しましょう。
javascripts
以下にsrc
ディレクトリを作成します。mkdir javascripts/src
javascripts/src
以下にmain.js
を以下の内容で作成します。main.jsimport Vue from 'vue' import TurbolinksAdapter from 'vue-turbolinks' Vue.use(TurbolinksAdapter)実際に使う
準備ができたので、実際にVueインスタンスを作成して動かします。
適当なビュー(今回は
views/home/index.html.erb
)でVueインスタンスで使われるid要素を追加して、その要素以下でVueのテンプレート表記でhtmlを記述します。views/home/index.html.erb<div id="vue-app"> {{message}} </div>
javascripts/src
以下にVueインスタンス作成のファイルを書きます。javascripts/src/app.jsimport Vue from 'vue' document.addEventListener('turbolinks:load', () => { const el = document.getElementById('vue-app') if (el) { new Vue({ el: el, data() { return { message: 'Hello Vue!' } } }) } })最後に
javascripts/src/main.js
で上記のファイルをimport
するよう追記します。javascripts/src/main.jsimport Vue from 'vue' import TurbolinksAdapter from 'vue-turbolinks' Vue.use(TurbolinksAdapter) + import './app.js'
rails s
してブラウザで以下のように確認できれば、完了です。おしまい
RailsでのVueライフをごゆるりと…