- 投稿日:2019-10-11T23:27:27+09:00
複数modelの関連づけに関するまとめ(rails アソシエーション)
今週のアウトプットはrailsでコメント投稿サイトを作っていた最中に苦戦したアソシエーションに関して復習ついでに書いていきたいと思います。
関連づけ(アソシエーション)とは
ユーザーがコメント投稿するようなサイトを例に説明すると、
誰が投稿したコメントなのかをデータとして保存しておく必要があります。まずmodelでuserとpostcommentのカラムに以下の表のようにカラムを作ります。
現在のテーブル設計だけでは、投稿したユーザのIDはわかっても、そのテーブル内のユーザ名まではわかりません。
そこで、ユーザ情報のモデルと、投稿情報のモデルとを関連付ける必要があります。
user_id(投稿したユーザを識別するID)と、id(投稿コメントを識別するID)が、該当します。
また、1人のユーザが複数の画像を投稿できます。
これをモデルで表現すると、Userモデルで表される1人のユーザに対して、複数個(N個)のPostcommentモデルを関連付けることができます。
User PostComment id id name comment user_id password 1:Nの関係 | これを、1対Nの関係と呼びます。
1:Nの関係性をモデルに実装する(アソシエーションの実行)
UserモデルにPostCommentモデルを関連付ける
まずは、Userモデルに対して、PostCommentモデルが1:Nになるよう関連付けます。
app/models/user.rbclass User < ApplicationRecord has_many :post_comment, dependent: :destroy end※dependent: :destroyは親(この場合はUser)が消滅した時は子(この場合はpost_comment)も消滅するという意味
PostCommentモデルにUserモデルを関連付ける
次に、PostCommentモデルに対して、Userモデルとの関係性を追加していきます。
app/models/post_comment.rbclass PostComment < ApplicationRecord belongs_to :user end関連づけにより参照先(userモデル)から値を参照できます。
@post_comment.user.name第3のモデルを追加する場合のアソシエーション
上記のmodelにコメント投稿機能以外に画像投稿機能を追加したい場合の関連づけとしてhas_many :throughを用いると便利でした。以下のようにmodelを追加します。
User PostComment PostImage| id id id name comment image_name user_id image_id(画像) password post_image_id 1: N : Mの関係 | User : PostComment = 1 : N
User : PostImage = L : M
PostComment : PostImage = 1 : M
であるのでmodelに関係性を追加していきます。app/models/user.rbclass User < ApplicationRecord has_many :post_comment has_many :post_image through: :post_comment endapp/models/post_comment.rbclass PostComment < ApplicationRecord belongs_to :user belongs_to :post_image endapp/models/post_image.rbclass PostImage << ApplicationRecord has_many :post_comment has_many :user through: :post_comment endhas_many thoroughを利用することでpost_commentから直接Userモデルにアクセスすることができます。
- 投稿日:2019-10-11T22:19:20+09:00
自分用メモ(ログイン・findとfind_byの違い・helperって何、使い方は?)
ログイン機構
ログインはセッションと言うリソースを作ることで実現する
sessionはcookieに保存する
ログインページへ(new)
↓
情報入力
ログイン情報送信
↓
セッションにユーザーIDが渡される
↓
ログイン完了(ユーザーIDをセッションの中から取り出せる状態)その後は
<%= @current_user.name %>
とかで取り出せるように@current_userの有無・userによってデータ出力を変化させることができる
ログイン情報入力のフォームを作る
フォームはデータベースをいじるためのもの(なので基本モデルがある)(form_for(model) do |f|)
ユーザー登録との違いは、セッションにはセッションモデルが存在しないと言うこと
→@userと言うようなインスタンス変数は存在しない
↓
自分で追加情報を明示しないといけない(今回は変化を与えるリソースの名前と、対応するURL)<%= form_for(:session,url: login_path) %>
f.email_form![]()
↓
送信されるデータは"session[:password]"
つまりparams[:session][:password]で受け取るログイン機能を作る
入力したsessionをparamsで受け取りemailを入力した[:email]から探す
↓
見つかったらuserに代入する
↓
userに入っているpasswordと
↓ セッション入力されたpasswordが一致するかauthenticateでチェックする
↓
(一致する?)
○ × →render new
↓
session[:user_id]=user.idセッションの[:user_id]にuser.idを代入する
session[:user_id]がnilなら「誰もログインしていない状態」
session[:user_id]に値が代入されているなら、そのIDを持つユーザーがログイン中ログイン者取得の簡略化
ログインしているかいないか、しているなら誰かを確認の確認が今後必要になってくる。
これはsession[:user_id]で取得すれば良い。
何回も呼び出すのが面倒なので、メソッドで定義しておく。def current_user
@current_user =User.find_by(id: session[:user_id])
end
(findの場合、ユーザー存在しない場合エラーが発生してしまうので、今回はfind_byを使う)
(@current_userに代入することで、データベースを検索する回数を初めの一回だけにする。)↓定義する前に、、代入演算子を理解しておく「||=」
「||=」と言う代入演算子
a ||= xxx
aが偽か未定義の場合、aに xxx を代入する。
これを書き換えると、
a = a || xxx
これはつまり、aに値が入っている場合、aにaは代入されるのでそのままである。
つまり、aに値が入っていない(nil)、もしくはfalseの時だけaが代入される。続き
現在ログイン中のユーザーを取り出す(いる場合)
def current_user
if session[:user_id]
@current_user = @current_user || User.find_by(id: session[:user_id])
end
endログインしているユーザーが存在する場合(session[:user_id]がnilでない場合)で、
@current_userがnilもしくはfalseの場合、@current_userにsession[:user_id]を代入する。
(@current_userが存在する場合、そのまま)「||=」を使って書き換え ↓
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
endfindとfind_byの使い分け
find
→idのみ(idがわかっている時のみ使用可能)
idがわからない場合は使えないfind_by
→idじゃなくても良い(帰ってくる結果は一つだけ)
idがわからない場合(nilになる場合)こちらを使うidがわかっているときはfind
idがわからないときはfind_byを使うhelperって何
Viewをシンプルに書くためのモジュール。
例としては form_for、link_to基本的に、Viewでヘルパーメソッドを呼び出す
・Controllerで呼び出したい場合
→そのままでは呼び出せないので、application.controllerに一文加える必要があるclass ApplicationController < ActionController::Base
include SessionsHelper ←← これ
end・helperを作るには?
アプリ全体でhelperを使いたい場合
→app/helpers/application_helper.rb に定義する
特定のモデルでhelperを使いたい場合
→app/helpers/(特定のモデル名)_helper.rb に定義する・具体的にどうやって書くの?(セッションモデルにのみ適用の場合)(今回はcontrollerで使用する)
module SessionsHelper
(ここに書く)
end
(中身)
def log_in(user)
session[:user_id]=user.id
end呼び出したいときは対象のコントローラーにメソッドを書くだけでok
pathにユーザー渡すのよくわからん
- 投稿日:2019-10-11T21:32:54+09:00
Railsチュートリアル 第9章<復習>
第9章の復習メモです。
個人的に重要と思ったことを書きます。前回と同様、以下3つの視点で書きます。
- 分かったこと
- 分からなかったこと
- 今回はスルーしたこと
分かったこと
永続セッション
前章では、一時cookieを使ったので、ブラウザを閉じると情報が消えてしまった。本章では、ブラウザを閉じても消えない永続cookieを使った。
前章の通りRailsでは、sessionメソッドを使うことで、一時cookieに情報を保存できる。sessionメソッドは、保存する際に情報を暗号化してくれるので、安全性が高い。
一方で、永続cookieの場合は、cookiesメソッドを使う。cookiesメソッドは、自動で暗号化する機能が無いため、安全性が低い。今回、永続cookieのセキュリティを強化するため、以下の対策を行った。
- cookieにユーザIDを保存する際、暗号化の処理を行う。
- ユーザIDに加えて、サーバ上でトークンを発行し、cookieに保存する。
今回、永続セッションを作成した手順は以下の通り。
- ランダムな文字列を生成し、これをトークンとする。
- ブラウザのcookiesにトークンを保存する。有効期限も設定しておく。
- トークンをデータベースに保存する。その際、ハッシュ値に変換する。
- ブラウザのcookiesに、暗号化したユーザーIDを保存する。
- 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。
分からなかったこと、今回はスルーしたこと
- 永続セッション実装の詳細
今回は、セッションそのものの仕組みについて、理解することに努めました。コーディングは一通り実装してみて、流れは分かったかな、という感じです。
- テスト全般
参考
以下を参考にさせていただきました。
https://qiita.com/hot_study_man/items/147f8b767b4135fe6fe4
https://www.masalog.site/entry/2017/08/31/171712
- 投稿日:2019-10-11T20:37:00+09:00
EC2のRuby/Rails環境構築中のwe're sorry, but something went wrongでハマった話
こんばんは!ポートフォリオをいよいよデプロイしようとした時にハマったエラー
we're sorry, but something went wrong
[ec2-user@ip-172-31-23-189 ~]$ unicorn_rails -c config/unicorn.rb -E production -Dで問題なくunicornが走ったと思い
http://<ElasticIP>:30000
にアクセスするとwe're sorry, but something went wrongいつも通り再起動
[ec2-user@ip-172-31-23-189 ~]$ sudo shutdown -r now [ec2-user@ip-172-31-23-189 ~]$ sudo service mysql stop [ec2-user@ip-172-31-23-189 ~]$ sudo service mysql start [ec2-user@ip-172-31-23-189 ~]$ cd /var/www/sample-app [ec2-user@ip-172-31-23-189 sample-app]$ cd /var/www/sample-app [ec2-user@ip-172-31-23-189 sample-app]$ unicorn_rails -c config/unicorn.rb -E production -Dまだnginxの設定していない状態だけど、いつもだとこれでうまくいくはず。だけど今回はダメだった。
production.rbのログ確認
エラーの糸口が掴めずメンターに聞くと教えてくれた
[ec2-user@ip-172-31-23-189 sample-app]$ cd log [ec2-user@ip-172-31-23-189 log]$ cat production.rb
catコマンド
でログの内容を確認していくと見覚えのあるエラーを発見。ActionView::Template::Error
解決
トップページに設置している画像が存在してないよってことでした。
<%= image_tag("hoge.png"), class:"hoge" %>
hoge.png
はapp/assets/images配下に置いてた画像でgitignore
してました。ひとまず
img src
に書き換える<img src="http://hogehoge.png" alt="" class-"hoge">これでもう一度、unicornを再起動して走らせたら無事にアクセスできました。
アセットファイルをコンパイル
[ec2-user@ip-172-31-40-237 sample-app]$ rails assets:precompile Yarn executable was not detected in the system. Download Yarn at https://yarnpkg.com/en/docs/install怒られた。
[ec2-user@ip-172-31-40-237 sample-app]$ npm install yarn -g //これでも怒られたら [ec2-user@ip-172-31-40-237 sample-app]$ sudo npm install yarn -g //これでいけるはず [ec2-user@ip-172-31-40-237 sample-app]$ rails assets:precompile ~~ success Saved lockfile. //unicornをkill -9 [pid]してunicorn再起動 [ec2-user@ip-172-31-40-237 sample-app]$ RAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -D無事にレイアウトも綺麗になりました。
参考
EC2にてnpm install yarn -gが失敗する
https://qiita.com/tsumita7/items/a40a367088018b5bbe33まとめ
2度目のデプロイですが、やはり一筋縄ではいきません。ハマったエラーはメモって次回はハマらないようにしていきます。
終わり
- 投稿日:2019-10-11T20:11:45+09:00
【初心者向け】PG二年目の私がコーディングでもっと早くから気をつけたかったこと
はじめに
コードを書く時、「こういう時ってどう書いたらいいの?」「そもそも何が綺麗なコードなの?」と悩んだことはありませんか?
私はあります。
この記事はかの有名なリーダブルコード
を読んだ際、私自身が出来ていなかったこと、意識しなければならないと思ったことを纏めたものです。
自分なりに纏めていますので、おかしい記述等もあるかもしれませんが、その際はご指摘ください。命名規則について
具体的な名前を付ける
- 誰が見ても、その名前だけで以下の2点が分かるような名前を付ける。
- 何をするメソッドか
- 何が格納されている変数か
- 以下の項目を意識すると具体的な名前になりやすい。
- 動詞を使うときは、より明確な動詞を選ぶ。汎用的なものはなるべく使わない。(類語を調べ、その中から選ぶといい)
- 私はこちらの記事を参考にするようにしています。
- 値の単位を付ける。(時間、サイズなど)
- 「どんな」を意識する。具体的にいうと形容詞や中身を連想できる名詞を添える。
records
よりもmessages
、 ハッシュを格納する変数であれば○○_hash
の方が直感的に中身の想像が出来る。リファクタリングするときに意識すること
ベースとして意識することについて
- 同じことは何度も書かない。
- 同じことを何度も書く必要がある場合はメソッドに分ける、変数に格納する
- 並び順に意味を持たせる。
- 複数変数が出てくる場合、定数が複数出てくる場合はその並びに意味を持たせる。
- 例:アルファベット順、画面での並び順、ファイルへの出力順など
- 行数が多い場合は適度に改行を挟み、見やすくする。
- 例:処理のまとまり単位で改行、など。
コメントについて
書いてはいけないコメント
- コードを見れば誰でも分かること
- メソッド名や変数名について説明するようなコメント(それは付けている名前が悪い)
不具合があるコードに付けるコメント
- 以下のprefixを付ける
prefix 意味 TODO: あとで直す FIXME: 既知のバグがあるコード HACK: あまり綺麗じゃない XXX: 大きな問題がある ※
prefix
が大文字の場合は大きな問題、小文字の場合は小さな問題という意味合いになります。
※TODO(ラテ太郎):
みたいな書き方をします。コメントを書いてもいい場所
- 読み手が一見で分からない or 疑問を持ちそうなところ
- なんで?これは何?と思われるようなコードであるならば、コメントの記載が必要です。
- 間違えて使われる可能性がありそうなところ
簡潔なコメントの書き方
その
のような指示語は避ける。(自分が思っているものと違う認識をされる可能性がある為、具体的に書くのが良いです。)かどうか
という表現は使わない。
- どうなったらどう、という書き方を心がける
×: Aかどうか
○ : AならばB
- 情報密度の高い言葉を使う。
- 自分の伝えたい内容が集約された言葉を選ぶことを意識すればコメントが簡潔になります。
- 実例を書く
ループ・ロジックの単純化
条件式
- 条件は肯定が正義。(わかりやすい)
- ド・モルガンの法則を駆使してなるべく肯定的な条件になるようにする。否定の否定、は使わない。悪。
- 目立つ条件、重要な条件から書く。
- 何でもかんでも三項演算子を使わない(わかりにくくなるため)
- 早期リターンできる時は早期リターンする(早期リターンすることでネストが減る。)
まとめ
やっぱり命名って難しい!!!!!
まずは上記のような、常に付きまとってくる問題と戦いながら、可読性の高いコードを書けるようになりたいと思います。参考
- 投稿日:2019-10-11T20:09:05+09:00
データを削除しようとしたらそのテーブルに主キーがなくて消せないんだけど
冒頭
LaravelからRailsに改修をする案件をしていたときのことで、
既存のDBがあるためそれをself.table_name
で指定してあげて使えるようにしているのですが、
データを論理削除する際に以下の問題にぶち当たりました。問題のエラー
ActiveRecord::StatementInvalid (Mysql2::Error: Unknown column 'テーブル名.' in 'where clause')
え、なんでカラムがないの?
と思いSQLを出力させたところ、UPDATE `テーブル名` SET `テーブル名`.`deleted_at` = '2019-10-11 18:12:27' , `テーブル名`.`updated_at` = '2019-10-11 18:12:27' WHERE `テーブル名`.`` IS NULLといった状態に。
DB設計書を確認したところ、どうやら主キー(Primary Key)がこのテーブルないとさ。
なるほど、そりゃ主キーないんだからカラムが空なわけ。調査
where句を別のものに指定できないものかと調べたらRails6から実装されているこんなものを見つけました。
# Finds and destroys all records matching the specified conditions. # This is short-hand for <tt>relation.where(condition).destroy_all</tt>. # Returns the collection of objects that were destroyed. # # If no record is found, returns empty array. # # Person.destroy_by(id: 13) # Person.destroy_by(name: 'Spartacus', rating: 4) # Person.destroy_by("published_at < ?", 2.weeks.ago) def destroy_by(*args) where(*args).destroy_all endだけどこれ結局のところ
delete_all
使ってるんで意味ないやん。
あ" き" ら" め" た"対応
生SQLをつかうことにした。
sql = <<-SQL UPDATE `テーブル名` SET `テーブル名`.`deleted_at` = '#{Time.current}' , `テーブル名`.`updated_at` = '#{Time.current}' WHERE `テーブル名`.`カラム名` = 'XXX' SQL con = ActiveRecord::Base.connection con.execute(sql)Railsつよつよの方へ
もしなにかしら方法をしっていたら教えてください?♂️
いや、普通に主キー作ればいいんだけどさ...
- 投稿日:2019-10-11T18:53:40+09:00
「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題解いてみた。
はじめに
この記事は「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題を解く過程から、先輩のレビューをいただき、振り返るところまでを纏めた記事です。
備忘録・振り返り的な要素が強い為、フランクな書き方をしておりますが、大目に見ていただければと思います。軽い読み物だと思って読んでください。
また解き方に稚拙な箇所もあるかと思いますがご容赦ください。指摘等は大歓迎です。励みになります。登場人物
- 私
- 社会人2年目PG。Rubyでコーディングし始めて1年とちょっと。うっかりポンコツ。最近Ruby Goldを取得した。
- ラテ太郎(アイコン参照)
- 私の心の中に住んでいる妖精。
白黒つけないいいやつ。ゆるい見掛けによらずしっかりしている。最後の砦。- タピオカ先輩
- ラテ太郎の先輩妖精。コーディングが得意。
フェーズ1 「考える・自力で解く」
実際に出題された問題
# NameIndex ## 問題 - カタカナ文字列の配列を渡すと、ア段の音別にグループ分けした配列を返すプログラムを作成せよ。 - 各要素は 50 音順にソートもすること。 ## 例 - IN: `['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']` - OUT: `[ ['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']] ]` ## 提出時、以下全ての条件を満たしていること - `RSpec` でエラーが発生していないこと - `RuboCop` で警告が出ていないこと - `Coverage` が `100%` であること考えたこと
私「なるほどなるほど、
辞書みたい
な感じでそれぞれの名前を纏めてあげればいいんだ。out
の形ってなんだかgroup_by
した時の形に似てない?(※この時の私は空前のgroup_by
ブーム)同じ感じ
でやれたらいいのにな〜。」ラテ「こんなイメージで合ってるかな?」
class NameIndex def self.create_index(names) names.group_by { |name| name.chr } end end私「そうそう、そんな感じ。私は
group_by
のところをgroup_by(&:chr)
って書くかな。(※書かないとRuboCopに怒られる。)」class NameIndex def self.create_index(names) names.group_by(&:chr) end end names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'] NameIndex.create_index(names) =>{"キ"=>["キシモト"], "イ"=>["イトウ"], "バ"=>["ババ"], "カ"=>["カネダ"], "ワ"=>["ワダ"], "ハ"=>["ハマダ"]}私「これを
to_a
で配列にしたらイメージに近くない?なんかイメージに近い気がする!……でもこれって並び替えも必要だよね。うーん、各要素はソートすること
って書いてあったよなぁ。各要素をソートする……。」※シンキングタイム
私「最初から配列の中身ソートしとけばええやんけ…………………………………………………………………………。」
ラテ「気付くのおっそ…………。」
class NameIndex def self.create_index(names) names.sort.group_by(&:chr).to_a end end names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'] NameIndex.create_index(names) =>[["イ", ["イトウ"]], ["カ", ["カネダ"]], ["キ", ["キシモト"]], ["ハ", ["ハマダ"]], ["バ", ["ババ"]], ["ワ", ["ワダ"]]]私「これです!!!!!!!!!!!!」
私「ちょっとどうしよう凄いしっくりきた。」
私「初めて一行で書けたのでは?」
私「やった〜〜〜〜〜〜〜〜〜〜〜嬉しい!」
私「RSpec書いて、時間を置いてから見直して、リファクタリングできるところあったらリファクタリングしよ!」・
・
・
・
・
・
・
・ラテ「お判りいただけただろうか…………。」
ラテ「こいつは凄い愚かなヤツです。どの辺が愚かかというと、
自分もやればできるじゃん
という気持ちに慢心しているところが愚かなのです。見直しは重要だということは小学生でも知っているのです。勿論、こいつにもそういう気持ちはあったのだと思うのです。あるからこそ時間を置いてから見直して
なんて言ってるのです。けれど、時間を置きすぎてはならないのです。なぜって時間には限りがあるから…………。」・
・
・
・
・
・
・
・
・
・
・
・私「待って?」
私「これ、ア
とかハ
で纏まってなくない?」
私「辞書みたいなイメージ
が先行してたけどこれ問題と違う
よね?」私「(大混乱)」
私「落ち着け、落ち着け私。きっと
ここまでの課程で考えてきたこと
と今までの経験が私を助けてくれるはず。」私「グループ化するっていう発送は悪くないはず。
キーがインデックス、valueがそのインデックスに含まれる文字、みたいなハッシュ
を作ったらいいんじゃないか?マッピング的な……ちょっと違う気がするけど……。」私「あ〜お、って
範囲オブジェクト
でいける?数字とアルファベット以外でもいける?ええい、ままよ!食らえ!」('ア'..'オ').to_a => ["ア", "ィ", "イ", "ゥ", "ウ", "ェ", "エ", "ォ", "オ"]私「すっっっっっっっっっっっっご」
私「まじか……いやまあそうよな……できるよな……できるんか……凄いなRuby……。」
私「待てよ、カタカナだったらあれよな、
ヴ
とかあるよな。え、ヴァネッサとか出てくるかな電話帳。いやでもこれ苗字だけとか書いてないしな。友達にヴァネッサとかおるかもしれんし……ヴァネッサだけ行き場ないとか可哀想よな……。」ラテ「そうして出来上がった定数がこれ。」
INDEX = { 'ア': ('ア'..'オ').to_a << 'ヴ', 'カ': ('カ'..'ゴ').to_a, 'サ': ('サ'..'ゾ').to_a, 'タ': ('タ'..'ド').to_a, 'ナ': ('ナ'..'ノ').to_a, 'ハ': ('ハ'..'ボ').to_a, 'マ': ('マ'..'モ').to_a, 'ヤ': ('ヤ'..'ヨ').to_a, 'ラ': ('ラ'..'ロ').to_a, 'ワ': ('ワ'..'ン').to_a }.freeze私「
ン
っている???」
私「いや、ンダホさんとかおるかもしれんし……」私「入れよう」
私「問題はここからよな」
私「ア
はア
のグループ、ってグループ分けしたい。」
私「グループ分けしたいけどどうやってメンバー選出する……?」※シンキングタイム
私「メンバー選出といえば
select
やんけ…………。」
私「さっき作った範囲の中に頭文字が当てはまる子(文字列)を選出してあげたらいいんじゃん?include?
でそれはチェックできる……で、それをうまいこと配列にして……なんかやっと見えてきたぞ…………。」class NameIndex INDEX = { 'ア': ('ア'..'オ').to_a << 'ヴ', 'カ': ('カ'..'ゴ').to_a, 'サ': ('サ'..'ゾ').to_a, 'タ': ('タ'..'ド').to_a, 'ナ': ('ナ'..'ノ').to_a, 'ハ': ('ハ'..'ボ').to_a, 'マ': ('マ'..'モ').to_a, 'ヤ': ('ヤ'..'ヨ').to_a, 'ラ': ('ラ'..'ロ').to_a, 'ワ': ('ワ'..'ン').to_a }.freeze def self.create_index(names) INDEX.map do |key, value| index_names = names.select { |name| value.include?(name.chr) } end end end私「そうそう、これで最初に想定してたみたいに
要素をソート
して。index_names
が空じゃなかったらインデックスをつけてあげればいけるんじゃないか。」class NameIndex INDEX = { 'ア': ('ア'..'オ').to_a << 'ヴ', 'カ': ('カ'..'ゴ').to_a, 'サ': ('サ'..'ゾ').to_a, 'タ': ('タ'..'ド').to_a, 'ナ': ('ナ'..'ノ').to_a, 'ハ': ('ハ'..'ボ').to_a, 'マ': ('マ'..'モ').to_a, 'ヤ': ('ヤ'..'ヨ').to_a, 'ラ': ('ラ'..'ロ').to_a, 'ワ': ('ワ'..'ン').to_a }.freeze def self.create_index(names) INDEX.map do |key, value| index_names = names.select { |name| value.include?(name.chr) }.sort [key.to_s, index_names] unless index_names.empty? end end end names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'] NameIndex.create_index(names) =>[["ア", ["アマネ"]], ["カ", ["キシモト"]], nil, nil, nil, ["ハ", ["ハマダ", "ババ"]], nil, nil, nil, ["ワ", ["ワダ"]]]私「盲点」
私「うっそやん………そりゃそうだわ…………なんで気付かんのじゃ……。」
私「
compact
しよ………。」
私「ごちゃごちゃしてるし、names
が空だったら素直に早期リターンしよ」ラテ「そして完成したのがこれ。」
class NameIndex INDEX = { 'ア': ('ア'..'オ').to_a << 'ヴ', 'カ': ('カ'..'ゴ').to_a, 'サ': ('サ'..'ゾ').to_a, 'タ': ('タ'..'ド').to_a, 'ナ': ('ナ'..'ノ').to_a, 'ハ': ('ハ'..'ボ').to_a, 'マ': ('マ'..'モ').to_a, 'ヤ': ('ヤ'..'ヨ').to_a, 'ラ': ('ラ'..'ロ').to_a, 'ワ': ('ワ'..'ン').to_a }.freeze def self.create_index(names) return [] if names.empty? INDEX.map do |key, value| index_names = names.select { |name| value.include?(name.chr) }.sort [key.to_s, index_names] unless index_names.empty? end.compact end end私「あっ、あ……RSpecも書き直しやん…………。先輩ごめんなさい………。(15分オーバー)」
フェーズ1で学んだこと
- カタカナも範囲オブジェクトにできる。
select
を使えばグループ化は簡単- 慢心しない、簡単にできたと思った時ほど見直しはしっかり。
- 時間に余裕を持って作業する。
- 問題は定期的に読み直す。自分の認識が間違っていないか確認する。
- 方向性を見失わないようにテストファーストで作業をする。
フェーズ2 タピオカ先輩からレビューをもらう
この課題を解いた後、見守っていたタピオカ先輩からレビューをもらった。
タピ「レビューしたんだけどさ。」
私「はい。」
タピ「今回悪いところなかった。」
私「???」
私「またまたぁ」
タピ「いや本当。今回の評価ポイント(要件を満たせているか・可読性・テストコードの充実度)は満たせてる。」
私「(動揺)(困惑)」
タピ「とりあえずレビューしていこうか」ポイント1 injectで無駄なく回す
タピ「私さん、
map
compact
してたじゃん。」
私「はい。map
だとnil
が混在しちゃうので」
タピ「じゃあ、nil
が混ざらないようにループ回したらよかったんじゃない?」
私「アッ」
map compact
orselect map
で作れる配列はinject
で作れinject
で作れるならeach_with_object
にしなさい(RuboCopの好み)タピ「この記事が参考になる。」
タピ「今回の問題だと、nil
になるものののチェックで無駄に処理が走るよね?例えば、電話帳にはア行しか登録されてないのに、他の行についての処理も走る。ループでいうなら2回ループが走ってる。必要なものだけの配列を作りたいならinject
を使えば1回で済む。」
私「そっか……そっか……そうですね……(知識としては持っていたのに未だ使いこなせていないことに対する悔しさに襲われる)」ポイント2 冗長にならないようなコーディングをする
タピ「あとさ、定数のハッシュ。キーと配列の0番目の値が一緒なの、なんか冗長じゃない?」
私「確かに……。」
タピ「しかもハッシュのキー、to_s
してるじゃん、to_s
するくらいならロケット記法
で最初からキーを文字列にしておけばよかったんじゃない?」
私「確かに……。」
タピ「しかもこのキーって配列作るときに使ってるだけじゃん?」
私「そうですね。そのためだけに用意しちゃいました。そのキーでまとめなきゃ!!って気持ちが強くて、キーを。」
タピ「つまりこう定義したら万事解決だったってこと。」class NameIndex SYLLABARIES = [ ('ア'..'オ').to_a << 'ヴ', ('カ'..'ゴ').to_a, ('サ'..'ゾ').to_a, ('タ'..'ド').to_a, ('ナ'..'ノ').to_a, ('ハ'..'ボ').to_a, ('マ'..'モ').to_a, ('ヤ'..'ヨ').to_a, ('ラ'..'ロ').to_a, ('ワ'..'ン').to_a ].freeze class << self def create_index(names) names.sort.group_by(&method(:initial)).to_a end private def initial(name) SYLLABARIES.find { |values| values.include?(name[0]) }.first end end end私「これです」
私「こういうの書きたかったんです」
私「
group_by
使われてるし、メインのメソッドは一行で書かれてるし、超カッケー(配列か〜〜〜〜〜!!マッピングしなきゃって気持ちが強すぎた)」〜タピオカ先輩の解説タイム〜
タピ「例えば、ア〜オ+ヴの中に、名字の1文字目が含まれていたらア行に纏められるわけでしょ。
group_by
の中でアカサタナ〜が返れば今回の想定した配列が作れるじゃん。つまり、『名字の1文字目が
SYLLABARIES
の配列のどれかに含まれていたら、その配列の1文字目が返ればいい』ってことね。それが
initial
メソッド。」タピ「そのメソッドを
group_by
と合わせて実行すればいいだけ。今回は&method
を使って書いてみた。」私「(
group_by
とはこうやって使うのか)」タピ「どう?納得?」
私「納得しかないです……すげ……」
フェーズ2で学んだこと
group_by
はブロック内の返り値でグルーピングされる=>自分の思う返り値が返るようなメソッドを作って呼んであげればグルーピングはできる(今回はgroup_by
が使えた!)&method
を使うと短く書ける(参考: &演算子と、procと、Object#method について理解しなおす)- 冗長だと感じるところは徹底的に排除する(同じ文字がなんども出てくる、等。今回は定数の中身が冗長だった。)
最後に
やり方、考え方の方向性は合っていましたが、テクニカルな部分がまだまだだなと実感しました。しかしこれで
group_by
はマスターです。&method
はあまり使ったことがなかったので、使える場面では使っていきたいと思います。
また、自分の書くコードは冗長になりがちなので、「これって冗長じゃない?」という気持ちをもってコーディングすることを意識する必要があるなと感じました。
次の回ではもっといいコードを書けるように腕を磨いておきます。
- 投稿日:2019-10-11T18:42:36+09:00
Railsチュートリアル 第6章 ユーザーのモデルを作成する - ユーザーを検証する
現状のUserモデルの問題点
name
属性と以下は、
name
属性の値がnil
であるレコードがRDB上に存在することを示す例です。現状のUserモデルの実装では、このようなデータも有効とされてしまいます。>> User.find(4) User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] => #<User id: 4, name: nil, email: "foo@bar.com", created_at: "2019-10-03 11:06:14", updated_at: "2019-10-03 11:06:14">さらに、
nil
を取ることができてしまいます。>> User.find(4).update_attributes(email: nil) User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] (0.1ms) SAVEPOINT active_record_1 SQL (0.2ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", nil], ["updated_at", "2019-10-03 11:17:27.810162"], ["id", 4]] (0.3ms) RELEASE SAVEPOINT active_record_1 => true >> User.find(4) User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] => #<User id: 4, name: nil, email: nil, created_at: "2019-10-03 11:06:14", updated_at: "2019-10-03 11:17:27">あるべき姿
ユーザーアカウントを内容とするRDBのレコードの場合、例えば以下のような制約は実装されていてほしいものです。
name
属性は空であってはならない
- ログイン時のユーザー名として、メールアドレスを用いる予定であるため
検証/バリデーションとは
Railsの文脈において、検証ないしバリデーションという言葉は、「属性値に対して何らかの制約を課すためのActive Recordの機能」を指します。よく使われる制約の一例としては、以下のようなものが挙げられます。
- 存在性の制約
- 長さの制約
- フォーマットの制約
- 一意性の制約
バリデーション実装を題材とした、テスト駆動開発の基本の習得
バリデーション実装というのは、テスト駆動開発と相性がよい対象といえます。その理由は以下の通りです。
- 「値に対してルールを適用する」という動作は、テストの成功・失敗を定義しやすいこと
- 状態に依存しないため、特に困難なくテストを実装できる
- バリデーションが失敗した場合、結果としてやや複雑な動作が要求されること
- どのような理由でバリデーションが通らなかったかをユーザーに提示する動作
- RDBに当該変更を反映しない動作
- やや複雑な動作であるため、テストなしでは正常動作の立証が困難であること
というわけで、Railsチュートリアルにおいては、バリデーション実装を題材としてテスト駆動開発を行っています。
具体的には、以下のような手法で開発を進めていくということです。
- 有効なモデルであれば通るようなテストを書く
- 有効なモデルのオブジェクトを作成する
- この時点ではテストは通るはず
- 作成したオブジェクトの属性のうち、1つを有効でない属性に意図的に変更する
- バリデーションで失敗するかどうかをテストする
- この時点でテストが失敗する
テストコードの実装
モデルに対するテストのモックは、
rails generate model
コマンドの実行結果として生成されます。例えば、現在題材としているUserモデルに対しては、以下のモックが既に存在します。test/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end見ての通り、現時点では単なるモックです。この時点で重要なのは、「Railsにおけるモデルに対するテストクラスは、
ActiveSupport::TestCase
というクラスを継承している」という点でしょうか。コントローラに対するテストがActionDispatch::IntegrationTest
というクラスを継承していたのとは違いますね。ここから実際のテストを書いていきましょう。まずは、有効なUserかどうかをテストするコードです。
est/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end test "should be valid" do assert @user.valid? end endテストメソッドにおいて
setup
というメソッドが使われています。Railsのテストフレームワークにおいて、「各テストが走る直前に実行される処理」を記述するためのメソッドです。@user
はインスタンス変数ですが、setupメソッド内で宣言しておけば、すべてのテスト内で当該インスタンス変数が使えるようになります。というわけで、今書いたテストコードによって、
valid?
メソッドを使ってUserオブジェクトの有効性をテストできる、というわけです。初めてのテストコード実行
モデルオブジェクトに対するテストを実行するには、開発環境で
rails test:models
というコマンドを実行します。# rails test:models Started with run options --seed 19540 1/1: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.08566s 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips余談 - 誤って
rails test:model
というコマンドを実行してしまうとどうなるか誤って
rails test:model
というコマンドを実行してしまうと、以下のようにスタックトレースを含む長いエラーメッセージが表示されます。# rails test:model rails aborted! Don't know how to build task 'test:model' (See the list of available tasks with `rails --tasks`) Did you mean? test:models /usr/local/bundle/gems/railties-5.1.6/lib/rails/commands/rake/rake_command.rb:21:in `block in perform' /usr/local/bundle/gems/railties-5.1.6/lib/rails/commands/rake/rake_command.rb:18:in `perform' /usr/local/bundle/gems/railties-5.1.6/lib/rails/command.rb:46:in `invoke' /usr/local/bundle/gems/railties-5.1.6/lib/rails/commands.rb:16:in `<top (required)>' /var/www/sample_app/bin/rails:9:in `require' /var/www/sample_app/bin/rails:9:in `<top (required)>' /usr/local/bundle/gems/spring-2.0.2/lib/spring/client/rails.rb:28:in `load' /usr/local/bundle/gems/spring-2.0.2/lib/spring/client/rails.rb:28:in `call' /usr/local/bundle/gems/spring-2.0.2/lib/spring/client/command.rb:7:in `call' /usr/local/bundle/gems/spring-2.0.2/lib/spring/client.rb:30:in `run' /usr/local/bundle/gems/spring-2.0.2/bin/spring:49:in `<top (required)>' /usr/local/bundle/gems/spring-2.0.2/lib/spring/binstub.rb:31:in `load' /usr/local/bundle/gems/spring-2.0.2/lib/spring/binstub.rb:31:in `<top (required)>' /var/www/sample_app/bin/spring:15:in `<top (required)>' bin/rails:3:in `load' bin/rails:3:in `<main>' (See full trace by running task with --trace)演習 - バリデーション実装を題材とした、テスト駆動開発の基本の習得
1. コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。
>> User.new.valid? => true2. 6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。
>> user.valid? => true存在性を検証する
存在性の検証とは、「渡された属性が存在することを検証する」ことを言います。この節では、以下の存在性検証を実装します。
- ユーザー情報がRDBに保存される前に、nameフィールドが存在することを検証する
- ユーザー情報がRDBに保存される前に、emailフィールドが存在することを検証する
name
属性の存在性に関するテスト
name
属性の存在性に関するテストの追加
test/models/user_test.rb
に、まずはname
属性の存在性に関するテストを追加します。`test/models/user_test.rb`require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end test "should be valid" do assert @user.valid? end + + test "name should be present" do + @user.name = " " + assert_not @user.valid? + end end
この時点では、テストが失敗するようになります。
# rails test:models Started with run options --seed 50708 FAIL["test_name_should_be_present", UserTest, 0.11231679999036714] test_name_should_be_present#UserTest (0.11s) Expected true to be nil or false test/models/user_test.rb:15:in `block in <class:UserTest>' 2/2: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.13430s 2 tests, 2 assertions, 1 failures, 0 errors, 0 skips
name
属性の存在を検査するにはモデルオブジェクトのクラス定義において、第1引数を
:name
としたvalidates
メソッドを、{ presence: true }
というオプションハッシュを第2引数として与えて用いることにより実現できます。app/models/user.rbclass User < ApplicationRecord + validates :name, presence: true end
以下の3つのコードは、いずれも同じ意味です。
class User < ApplicationRecord validates :name, presence: true endclass User < ApplicationRecord validates(:name, presence: true) endclass User < ApplicationRecord validates(:name, { presence: true }) end重要なのは以下の事柄です。
- Rubyにおいては、特別な場合を除き、メソッド呼び出しの
()
は省略できる- 最後の引数がハッシュである場合、当該ハッシュに対する
{}
は省略できるUserモデルに検証を追加した結果を確認してみる
コンソールを起動して、Userモデルに検証を追加した結果を見てみます。
>> user = User.new(name: "", email: "mhartl@example.com") => #<User id: nil, name: "", email: "mhartl@example.com", created_at: nil, updated_at: nil> >> user.valid? => false
valid?
メソッドの戻り値がfalse
になっていますね。どの検証が失敗したかを確認するには、検証が失敗した際に生成される
errors
メソッドを使うと便利です。>> user.errors.full_messages => ["Name can't be blank"] >> user.name.blank? => trueエラーメッセージにblankとあるので、
blank?
メソッドで確認してみました。確かにblank?
の結果がtrue
になっていますね。validでないモデルオブジェクトは、RDBに保存できない
valid?
がfalse
であるようなモデルオブジェクトは、RDBに保存することができません。>> user.save (1.6ms) SAVEPOINT active_record_1 (0.2ms) ROLLBACK TO SAVEPOINT active_record_1 => false
ROLLBACK
されていますね。今度こそテストは成功
ここまで確認したところで、改めて
rails test:models
を実行してみます。# rails test:models Started with run options --seed 17899 2/2: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.11375s 2 tests, 2 assertions, 0 failures, 0 errors, 0 skips今度こそテストは成功しました。
test/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end test "should be valid" do assert @user.valid? end test "name should be present" do @user.name = " " assert_not @user.valid? end + test "email should be present" do + @user.email = " " + assert_not @user.valid? + end end
ここまでの変更に加え、
test/models/user_test.rb
に対して上記の変更を行います。この時点では、以下の通りテストは通りません。
# rails test:models Started with run options --seed 4227 FAIL["test_email_should_be_present", UserTest, 0.10753690000274219] test_email_should_be_present#UserTest (0.11s) Expected true to be nil or false test/models/user_test.rb:20:in `block in <class:UserTest>' 3/3: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.12862s 3 tests, 3 assertions, 1 failures, 0 errors, 0 skips現時点でテストが通るようにするためには、
app/models/user.rb
に以下の変更を加えます。app/models/user.rbclass User < ApplicationRecord validates :name, presence: true + validates :email, presence: true end
改めてテストを実行します。
# rails test:models Started with run options --seed 54749 3/3: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.10906s 3 tests, 3 assertions, 0 failures, 0 errors, 0 skips今度こそテストが通りました。
改めて全体のテストを実行
全てのテストを実行してみます。
# rails test Running via Spring preloader in process 2372 Started with run options --seed 40322 11/11: [=================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.63199s 11 tests, 22 assertions, 0 failures, 0 errors, 0 skips通りました。
演習 - 存在性の検証
モデルオブジェクト(
app/models
)に変更を加えた場合、Railsコンソールを再起動しなければ、モデルオブジェクトに対する変更が反映されません。1.1. 新しいユーザー
u
を作成し、作成した時点では有効ではない (invalid) ことを確認してください。>> u = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> >> u.valid? => false1.2. なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。
>> u.errors.full_messages => ["Name can't be blank", "Email can't be blank"]こういうときに使うのは、
errors.full_messages
メソッドですね。結果、以下2つの文字列から成る配列を返してきました。
- Name can't be blank
- Email can't be blank
2.1.
u.errors.messages
を実行すると、ハッシュ形式でエラーが取得できることを確認してください。>> u.errors.messages => {:name=>["can't be blank"], :email=>["can't be blank"]}2.2. emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
errors.messages
はハッシュを返すのでしたね。ハッシュのfind
メソッドを使います。keyがfind
するのです。>> u.errors.messages.find {|k,v| k == :email} => [:email, ["can't be blank"]]私事ですが、ハッシュの
find
メソッドを、および、ハッシュに関するブロックの構文を覚えていませんでした。長さを検証する
今度はユーザー名・メールアドレスの長さに制限を与えていきます。
ユーザー名の長さに制限を与える理由として、Railsチュートリアルでは、「ユーザーの名前はサンプルWebサイトに表示される」旨を挙げていました。一方、メールアドレスの長さに制限を与える理由としては、「ほとんどのRDBMSでは文字列の上限を255文字としている」旨を挙げていました。
というわけで、文字列の長さの制限は以下とします。
- Name属性…最長50文字
- Email属性…最長255文字
長さに関するテストを追加する
というわけで、以下のコードを
test/models/user_test.rb
に追加していきます。test/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase ...略 + + test "name should not be too long" do + @user.name = "a" * 51 + assert_not @user.valid? + end + + test "email should be not to long" do + @user.email = "a" * 244 + "@example.com" + assert_not @user.valid? + end end
長い文字列を作る方法
Rubyでは、文字列のかけ算によって長い文字列を容易に生成することができます。
>> "a" * 51 => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" >> ("a" * 51).length => 51上記は、51文字の文字列を生成した例です。今回の学習では、Name属性のバリデーションに用いています。
>> "a" * 244 + "@example.com" => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com" >> ("a" * 244 + "@example.com").length => 256上記は、メールアドレスとして正しい書式になるような256文字の文字列を生成した例です。今回の学習では、Email属性のバリデーションに用いています。
テストは失敗する
# rails test:models Started with run options --seed 56934 FAIL["test_name_should_not_be_too_long", UserTest, 0.11051520000910386] test_name_should_not_be_too_long#UserTest (0.11s) Expected true to be nil or false test/models/user_test.rb:25:in `block in <class:UserTest>' FAIL["test_email_should_be_not_to_long", UserTest, 0.11830520001240075] test_email_should_be_not_to_long#UserTest (0.12s) Expected true to be nil or false test/models/user_test.rb:30:in `block in <class:UserTest>' 5/5: [===================================] 100% Time: 00:00:00, Time: 00:00:00現状で
rails test:models
は失敗します。各FAILメッセージの冒頭には、今追加したテストの名前をスネークケースにしたものが来ています。新たに追加したテストが、確かに正しく動作していますね。長さを強制するためのコードをモデルに追加する
Railsのモデルクラスの
validates
メソッドにおいて、最終引数のオプションハッシュで、:length
というキーを持つ属性が利用できます。:length
属性は、引数としてハッシュを取ります。:length
属性の引数たるハッシュに:maximum
というキーを持つ属性を与えると、:maximum
属性の値により、モデルオブジェクトの対応する属性の最大長を決めることができます。早速、Userモデルのコードを書き換えていきましょう。
app/models/user.rbclass User < ApplicationRecord - validates :name, presence: true + validates :name, presence: true, length: { maximum: 50 } - validates :email, presence: true + validates :email, presence: true, length: { maximum: 255 } end上記が「Name属性の最大長50文字・Email属性の最大長255文字という制約をUserクラスに追加する」というコードの例です。
テストが成功した!
Userモデルのコードを書き換えたところで、再び
rails test:models
を実行してみましょう。# rails test:models Started with run options --seed 55077 5/5: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.11015s 5 tests, 5 assertions, 0 failures, 0 errors, 0 skips今度こそテストが成功しました。
演習 - 長さの検証
1. 長すぎる
name
と>> u = User.new(name: "#{("a".."z").to_a.join * 2}", email: "#{("a".."z").to_a.join * 10}@example.net") => #<User id: nil, name: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", email: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", created_at: nil, updated_at: nil> >> u.name.length => 52 >> u.email.length => 272 >> u.valid? => false上記は「
name
属性もvalid?
は確かにfalse
を返しています。>> u = User.new(name: "#{("a".."z").to_a.join * 2}", email: "#{("a".."z").to_a.join}@example.net") => #<User id: nil, name: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", email: "abcdefghijklmnopqrstuvwxyz@example.net", created_at: nil, updated_at: nil> >> u.name.length => 52 >> u.email.length => 38 >> u.valid? => false上記、今度は「
name
属性が長すぎる」という例です。valid?
は確かにfalse
を返しています。>> u = User.new(name: "#{("a".."z").to_a.join}", email: "#{("a".."z").to_a.join * 10}@example.net") => #<User id: nil, name: "abcdefghijklmnopqrstuvwxyz", email: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", created_at: nil, updated_at: nil> >> u.name.length => 26 >> u.email.length => 272 >> u.valid? => false上記、今度は「
name
属性の長さは適切だが、valid?
は確かにfalse
を返しています。>> u = User.new(name: "#{("a".."z").to_a.join}", email: "#{("a".."z").to_a.join * 2}@example.net") => #<User id: nil, name: "abcdefghijklmnopqrstuvwxyz", email: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", created_at: nil, updated_at: nil> >> u.name.length => 26 >> u.email.length => 64 >> u.valid? => true上記、最後は「
name
属性もvalid?
がtrue
になりました。余談…
"#{("a".."z").to_a.join}"
の意味
"abcdefghijklmnopqrstuvwxyz"
という26文字の文字列を得るためのコードです。実際に行われている処理は以下のとおりです。
"a"
から始まり、"z"
で終わる範囲を生成する- 1.で生成した範囲を配列に変換する
- 2.で生成した配列を文字列に変換する
>> ('a'..'z').to_a.join => "abcdefghijklmnopqrstuvwxyz" >> ('a'..'z').to_a.join.length => 26この文字列に
* 10
という演算を行えば、260文字の文字列を生成することができます。見事に255文字を超えてきますね。>> (('a'..'z').to_a.join * 10) => "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" >> (('a'..'z').to_a.join * 10).length => 2602. 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。
>> u = User.new(name: "#{("a".."z").to_a.join * 2}", email: "#{("a".."z").to_a.join * 10}@example.net") => #<User id: nil, name: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", email: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", created_at: nil, updated_at: nil> >> u.valid? => false >> u.errors.full_messages => ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]
- Name is too long (maximum is 50 characters)
- Email is too long (maximum is 255 characters)
以上のエラーメッセージが確認できます。そういえば、
errors
オブジェクトの値は副作用により与えられるのですね。フォーマットを検証する
メールアドレスの形式には、やや複雑なルールが存在します。そのため、単に文字数を制限するだけでは、メールアドレスのバリデーションとしては不十分です。というわけで、「メールアドレスとして最低限の体裁を整えていなければならない」という条件を実装しましょう。
文字列の配列を作る
%w[]
書式>> %w[foo bar baz] => ["foo", "bar", "baz"]以下の例は、
each
メソッドを使って、addresses
配列の各要素を繰り返し取り出す例です。>> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp] => ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"] >> addresses.each do |address| ?> puts address >> end USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp => ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]テストを記述する
有効なメールアドレスに対するテスト
test/models/user_test.rbclass UserTest < ActiveSupport::TestCase ...略 + test "email validation should be accept valid addresses" do + valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn] + valid_addresses.each do |valid_address| + @user.email = valid_address + assert @user.valid?, "#{valid_address.inspect} should be valid" + end + end end
assert
の第2引数にエラーメッセージを追加していることがポイントです。このようにすれば、どのメールアドレスでテストが失敗したかを特定できます。assert @user.valid?, "#{valid_address.inspect} should be valid"
inspect
メソッドというのは、第4章で出てきたメソッドですね。今回は、詳細な文字列情報を得るために使っています。このテストを追加した時点では、テストは問題なく通ります。
# rails test:models Started with run options --seed 22570 6/6: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.10920s 6 tests, 10 assertions, 0 failures, 0 errors, 0 skips一つ前のテスト追加の時点では
5 tests, 5 assertions
だったのが、今回は6 tests, 10 assertions
になっています。テスト対象のメールアドレスとして、有効なメールアドレス5つから成る配列を与えたからですね。無効なメールアドレスに対するテスト
test/models/user_test.rbclass UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end ...略 + + test "email validation should reject invalid addresses" do + invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com] + invalid_addresses.each do |invalid_address| + @user.email = invalid_address + assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" + end + end end
この時点では、テストが失敗するようになります。
# rails test:models Started with run options --seed 33633 FAIL["test_email_validation_should_reject_invalid_addresses", UserTest, 0.079405600001337] test_email_validation_should_reject_invalid_addresses#UserTest (0.08s) "user@example,com" should be invalid test/models/user_test.rb:45:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:43:in `each' test/models/user_test.rb:43:in `block in <class:UserTest>' 7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.13012s 7 tests, 11 assertions, 1 failures, 0 errors, 0 skips有効なメールアドレスであるかどうかを判別する、実用的な正規表現
Railsチュートリアルでは、「有効なメールアドレスであるかどうかを判別する実用的な正規表現」として、以下のものが挙げられていました。
\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\zこの正規表現の意味については、Railsチュートリアル本文内表6.1にまとめられています。
上述正規表現の問題点
ただし、この正規表現を有効なメールアドレスかどうかの判別に使うことは、日本においては若干の問題があります。以下のような「有効でないにもかかわらず、未だ日本国内で使われている可能性があるメールアドレス」が有効であると判定されてしまうためです。
.user@example.com
(メールアドレスの先頭に.
がある)user.@example.com
(@
の直前に.
がある)foo..bar@example.com
(@
より前で.
が連続している)これらの有効でないメールアドレスは、2009年3月以前のNTTドコモやauにおいて、キャリアメールのメールアドレスとして作成することができてしまっていたものです。
上記スクリーンショットのとおり、Rubularで試した場合も上述した有効でないメールアドレスの例が有効であると判定されています。
メールアドレスの有効性判定をUserモデルに追加する
上述有効でないメールアドレスの問題はさておき、正規表現によるメールアドレスの有効性判定をUserモデルに実装してみましょう。
app/models/user.rbclass User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } + VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i - validates :email, presence: true, length: { maximum: 255 } + validates :email, presence: true, length: { maximum: 255 }, + format: { with: VALID_EMAIL_REGEX } endこれでテストが通るようになります。
# rails test:models Started with run options --seed 38765 7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.13943s 7 tests, 15 assertions, 0 failures, 0 errors, 0 skipsassertionsの数が11から15に増えました。いいですねぇ。
演習 - フォーマットの検証
1. リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
2. 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。
「foo@bar..comのようにドットが連続した無効なメールアドレスを許容する」というのは、上述スクリーンショットのような挙動をいいます。「
@
以降で.
が連続している」というパターンですね。このようなメールアドレスを無効と判定できるような実装を追加したい…その第一段階としてのテストの追加です。
test/models/user_test.rbclass UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end ..略 test "email validation should reject invalid addresses" do - invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.foo@bar_baz.com foo@bar+baz.com] + invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.foo@bar_baz.com foo@bar+baz.com foo@bar..com] invalid_addresses.each do |invalid_address| @user.email = invalid_address assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" end end endこの時点で、テストを走らせてみます。
# rails test:models Started with run options --seed 27813 FAIL["test_email_validation_should_reject_invalid_addresses", UserTest, 0.1293631999869831] test_email_validation_should_reject_invalid_addresses#UserTest (0.13s) "foo@bar..com" should be invalid test/models/user_test.rb:45:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:43:in `each' test/models/user_test.rb:43:in `block in <class:UserTest>' 7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.13355s 7 tests, 16 assertions, 1 failures, 0 errors, 0 skips期待通り(?)、テストは失敗しました。
"foo@bar..com" should be invalid
というメッセージ内容を見るに、失敗してほしいところできちんと失敗しているようですね。2.2. 次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。
app/models/user.rb
を書き換えていきます。app/models/user.rbclass User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } - VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i + VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX } endテストします。
# rails test:models Started with run options --seed 39103 7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.13441s 7 tests, 16 assertions, 0 failures, 0 errors, 0 skipsテストが通りました。
3. foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
一意性を検証する
メールアドレスの一意性についての重要注意
「メールアドレスの一意性」という観点において、Railsチュートリアルには重要な指摘事項が2点あります。
- 実用上、メールアドレスの大文字小文字は区別されない
- バリデーションも大文字小文字を区別しない形で実装しなければならない
- 「文字列型の大文字小文字を区別するかしないか」は、RDBMSの種類によって異なるので、その違いを吸収するメカニズムを実装しなければならない
- Active Recordには、RDBMSレベルでのデータの一意性を保証する仕組みはない
- ほぼ同時に同内容の
POST
リクエストを受け取った場合など、重複してはならないカラムに重複が発生するおそれがある「これらを踏まえた上で、相応の対策が必要となる」ということです。…恐れずに行ってみましょう!
重複するメールアドレスを拒否する(Rails側で)
テストを書く
一意性をテストするためには、「実際にレコードをRDBMSに保存する」という操作が必要になります。ここで使うのは、モデルオブジェクトの
save
メソッドです。テストコードに登場するのは初めてですね。test/models/user_test.rbclass UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end ...略 + + test "email addresses should be unique" do + duplicate_user = @user.dup + @user.save + assert_not duplicate_user.valid? + end end
テストは通らない
この時点でテストは通りません。
assert_not
の時点で、RDBMS上にduplicate_user
と同一内容のmail属性値を持つレコードが存在するのに、duplicate_user
のvalid?
がtrue
になるためです。Started with run options --seed 45524 FAIL["test_email_addresses_should_be_unique", UserTest, 0.11071269999956712] test_email_addresses_should_be_unique#UserTest (0.11s) Expected true to be nil or false test/models/user_test.rb:52:in `block in <class:UserTest>' 8/8: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.21069s 8 tests, 17 assertions, 1 failures, 0 errors, 0 skipsテストを通す
上記テストを通すためには、Userモデルにおける
uniqueness: true
というオプションを追加すればOKです。app/models/user.rbclass User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, - format: { with: VALID_EMAIL_REGEX } + format: { with: VALID_EMAIL_REGEX }, + uniqueness: true end実は、「メールアドレスは、実用上大文字小文字を区別しない」ことに対応しなければならなかった
前述「メールアドレスの一意性についての重要注意」にあった事柄の一つですね。Railsチュートリアルのテストコードでは、「メールアドレスを大文字に変換して比較する」という操作を追加しています。
test/models/user_test.rbclass UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end ...略 test "email addresses should be unique" do duplicate_user = @user.dup + duplicate_user.email = @user.email.upcase @user.save assert_not duplicate_user.valid? end end
「メールアドレスを大文字に変換して比較する」というのが何を指すか、Railsコンソールで順を追って確認してみます。
>> user = User.create(name: "Example User", email: "user@example.com") (0.1ms) SAVEPOINT active_record_1 User Exists (1.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "user@example.com"], ["LIMIT", 1]] SQL (14.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2019-10-09 23:07:22.293648"], ["updated_at", "2019-10-09 23:07:22.293648"]] (0.1ms) RELEASE SAVEPOINT active_record_1 => #<User id: 1, name: "Example User", email: "user@example.com", created_at: "2019-10-09 23:07:22", updated_at: "2019-10-09 23:07:22"> >> user.email.upcase => "USER@EXAMPLE.COM" >> duplicate_user = user.dup => #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil> >> duplicate_user.email = user.email.upcase => "USER@EXAMPLE.COM" >> duplicate_user.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "USER@EXAMPLE.COM"], ["LIMIT", 1]] => true最後の
duplicate_user.valid?
がtrue
になっています。前述"email addresses should be unique"
テストを通すためには、duplicate_user.valid?
はfalse
にならなければなりません。モデルオブジェクトにおいて、「一意性検証において大文字小文字を区別しない」という実装をするには、
:uniquness
オプションの:case_sensitive
オプションの値false
とすればOKです。app/models/user.rbclass User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX } format: { with: VALID_EMAIL_REGEX }, - uniqueness: true + uniqueness: { case_sensitive: false } end「
:uniqueness
オプションの値にハッシュのみを与えている」という点は注目に値します。このような書き方をした場合、Railsは:uniquness
オプションをtrue
と判断した上で処理を行います。この時点で、Rails側のテストは通るようになりました。
# rails test:models Started with run options --seed 11495 8/8: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.21977s 8 tests, 17 assertions, 0 failures, 0 errors, 0 skips# rails test Running via Spring preloader in process 2521 Started with run options --seed 16633 16/16: [=================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.45354s 16 tests, 36 assertions, 0 failures, 0 errors, 0 skipsRDBMSレベルでメールアドレスの一意性を保証する
現時点で、Rails側でメールアドレスの重複を排除する仕組みは実装できています。しかし、前述「メールアドレスの一意性についての重要注意」で言及したように、RDBMSレベルでメールアドレスの一意性を保証する仕組みは未だ実装されていません。そのため、現時点では「重複するメールアドレスを持つ複数レコードがRDBMSに登録されてしまう」という事態の発生を排除しきれません。Railsチュートリアルでは、「Submitボタンを素早く2回クリックしてしまった場合」を例として、そうした事態が発生しうることについて言及しています。
というわけで、RDBMS側にもメールアドレスの一意性を保証する仕組みを実装する必要があります。具体的には以下の手順を踏みます。
- RDBMS上のemailカラムにインデックスを追加する
- 追加したインデックスについて、一意性制約を追加する
変更を加えるのはRDBMS側ですが、この操作はRails側から行うことが可能です。「マイグレーションの実装」という操作ですね。ただ、今回はマイグレーションの新規実装ではなく、既存のモデルへの構造の追加なので、新規実装時とは異なる手順を踏む必要があります。
既存のモデルに構造を追加する
既存のモデルに構造を追加する場合、
migration
ジェネレーターを用いてマイグレーションを直接作成する必要があります。# rails generate migration add_index_to_users_email Running via Spring preloader in process 2535 invoke active_record create db/migrate/[timestamp]_add_index_to_users_email.rb
db/migrate/{[timestamp]_add_index_to_users_email.rb
というファイルが生成されました。これが新たに作成したマイグレーションの実体です。生成時点のマイグレーションには、特に何も定義されていません。ここにメールアドレスの一意性制約を追加していきます。
class AddIndexToUsersEmail < ActiveRecord::Migration[5.1] def change + add_index :users, :email, unique: true end end
追加したコードのポイントは以下です。
users
テーブルの- インデックスはRails(ActiveRecord::Migration)の
add_indexメソッド
により追加することができる
- デフォルトでは一意性制約を追加しない
- オプションとして
unique: true
を指定することにより、一意性制約を追加できるマイグレーションのコードを変更したら、最後に実際にマイグレートの処理を行います。
# rails db:migrate == [timestamp] AddIndexToUsersEmail: migrating ============================= -- add_index(:users, :email, {:unique=>true}) -> 0.0222s == [timestamp] AddIndexToUsersEmail: migrated (0.0224s) ====================無事マイグレートの処理が完了しました。
fixture
Railsにおけるfixtureというのは、テストに際して初期データを投入する機構のことです。モデルオブジェクトに対応して存在します。最初の生成は、
rails generate model
により自動で行われます。test/fixtures/users.yml# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html one: name: MyString email: MyString two: name: MyString email: MyString上記は自動生成されたままの状態のfixture定義データです。見ての通り、メールアドレスが一意ではありません。そのため、「RDBMSにメールアドレスの一意性制約を追加した時点で、一意性制約違反によりテストが通らなくなった」というのが現状です。
なお、そもそもfixtureの内容自体が有効でない代物なのですが、fixtureの内容はバリデーションを通らないゆえ、ここまで問題にはなっていませんでした。
現時点およびこの先しばらくはfixtureを使うことはありません。なので、ここでは単にfixture定義ファイルの内容を単純に空にしてしまうという解決策をとります。
test/fixtures/users.yml- # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - one: - name: MyString - email: MyString - - two: - name: MyString - email: MyString -
fixtureの中身を空にした時点で、モデルに対するテストは通るようになります。
# rails test:models Started with run options --seed 158 8/8: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.20588s 8 tests, 17 assertions, 0 failures, 0 errors, 0 skips大文字小文字を区別するRDBMSへの対処
インデックスにおいて大文字と小文字を区別するかは、使用するRDBMSによって異なります。
今回のアプリケーションでは、メールアドレスのインデックスにおいて大文字と小文字は区別してほしくありません。例えば、eRAseRmOToRpHAntOM@example.comとEraSErMotOrPhaNTom@ExAMPle.comは同一の文字列と解釈してほしいです。そのため、インデックスにおいて大文字と小文字を区別するRDBMSへの対処が必要となります。
今回は、「データベースに保存される直前に、すべての文字列を小文字に変換する」という方法をとります。例えば、"EraSErMotOrPhaNTom@ExAMPle.com"という文字列が渡されたら、保存直前に"erasermotorphantom@example.com"に変換してしまうわけです。
Active Recordのコールバック(callback)メソッドとは
RailsのActive Recordライブラリでは、オブジェクトのライフサイクル中の特定タイミングに実行されるメソッドを定義することができます。例えば、作成時・保存時・更新時・削除時・検索時・検証時・データベースからの読み込み時などがそのタイミングです。こうした特定タイミングに実行されるメソッドを「コールバック」といいます。
ここで「特定タイミング」と言いました。より厳密には、特定タイミングの「前」「後」というのも指定することができます。例えば、作成前・作成後。保存前・保存後…というような指定も可能です。
コールバックメソッドの実装
繰り返しますと、今回実装するのは、「データベースに保存される直前に、
before_save
という名前で定義されています。では実装していきましょう。対象ファイルは
app/models/user.rb
です。app/models/user.rbclass User < ApplicationRecord + before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } end
重要なポイントは以下です。
- before_saveメソッドに渡しているのはブロックである
- 「小文字に変換する」という処理は、
String
クラスのdowncase
メソッドで実現している- このコードにおける
self
は、現在のユーザーを指す- 代入式右辺の
self
は省略することができる
- 一方、代入式左辺の
self
は省略することができないこの実装で実現できること
- RDBMSのemail属性に一意性制約を追加することにより、email属性の値が重複するユーザーの存在をRDBMSレベルで排除できるようになった
- RDBMSのemail属性にインデックスを追加することにより、検索効率が向上する
- メールアドレスからユーザーを引く際に全表スキャンをしなくて済むようになった
演習 - 一意性を検証する
1.1. リスト 6.33を参考に、メールアドレスを小文字にするテストをリスト 6.32に追加してみましょう。
ちなみに追加するテストコードでは、データベースの値に合わせて更新する
reload
メソッドと、値が一致しているかどうか確認するassert_equal
メソッドを使っています。class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end ...略 test "email addresses should be unique" do duplicate_user = @user.dup duplicate_user.email = @user.email.upcase @user.save assert_not duplicate_user.valid? end + + test "email addresses should be saved as lower-case" do + mixed_case_email = "Foo@ExAMPle.com" + @user.email = mixed_case_email + @user.save + assert_equal mixed_case_email.downcase, @user.reload.email + end end
"email addresses should be saved as lower-case"
という名前のテストが、設問内容に対応するテストですね。以下の処理を行っています。
- 大文字小文字が混ざったメールアドレスの例を
mixed_case_email
オブジェクトとして定義する- テストで使うユーザーオブジェクトのEmail属性に、
mixed_case_email
を代入する- テストで使うユーザーオブジェクトをRDBMS上に保存する
mixed_case_email
の値を全て小文字にしたものと、改めてRDBMSから読み込んだユーザーオブジェクトのemail属性の値が一致していればテスト成功1.2. リスト 6.33のテストがうまく動いているか確認するためにも、
before_save
の行をコメントアウトしてred
になることを確認してみましょう。app/models/user.rbclass User < ApplicationRecord - before_save { self.email = email.downcase } + # before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } end上述のコードは、
before_save
の行をコメントアウトした状態です。これでテストを実行してみましょう。# rails test:models Started with run options --seed 47663 FAIL["test_email_addresses_should_be_saved_as_lower-case", UserTest, 0.11753190000308678] test_email_addresses_should_be_saved_as_lower-case#UserTest (0.12s) Expected: "foo@example.com" Actual: "Foo@ExAMPle.com" test/models/user_test.rb:60:in `block in <class:UserTest>' 9/9: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.27076s 9 tests, 18 assertions, 1 failures, 0 errors, 0 skipsそれらしいエラーメッセージがありますね。以下の部分です。
test_email_addresses_should_be_saved_as_lower-case#UserTest (0.12s) Expected: "foo@example.com" Actual: "Foo@ExAMPle.com"「
"foo@example.com"
が入ってくるべきところに、"Foo@ExAMPle.com"
が入ってきている」という趣旨のエラーメッセージです。1.3.
before_save
の行のコメントアウトを解除するとgreen
になることを確認してみましょう。app/models/user.rbclass User < ApplicationRecord - # before_save { self.email = email.downcase } + before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } end変更内容は上述となります。テストを実行してみましょう。
# rails test:models Started with run options --seed 6755 9/9: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.26453s 9 tests, 18 assertions, 0 failures, 0 errors, 0 skipsテストは無事完了しました。
2. テストスイートの実行結果を確認しながら、
before_save
コールバックをemail.downcase!
に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。
app/models/user.rbclass User < ApplicationRecord - before_save { self.email = email.downcase } + before_save { email.downcase! } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } endモデルのテストから先に実行してみましょう。コマンドは
rails test:models
ですね。# rails test:models Started with run options --seed 33265 9/9: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.26898s 9 tests, 18 assertions, 0 failures, 0 errors, 0 skips無事完了しました。
続いて、テストスイート全体も実行してみましょう。こちらのコマンドは
rails test
です。# rails test Running via Spring preloader in process 2614 Started with run options --seed 40098 17/17: [=================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.36793s 17 tests, 37 assertions, 0 failures, 0 errors, 0 skips無事完了しました。
- 投稿日:2019-10-11T18:23:06+09:00
Railsチュートリアル 第8章<復習>
第8章の復習メモです。
個人的に重要と思ったことを書きます。前回と同様、以下3つの視点で書きます。
- 分かったこと
- 分からなかったこと
- 今回はスルーしたこと
分かったこと
セッションについて
今回は、ログイン、ログアウトの機能を実装した。
- ユーザがログインし、ログアウトするまでの間、接続情報(ユーザID等)を保持する必要がある。
- 接続情報は、ブラウザとサーバ間にセッションを確立することで保持される。
- 情報が保持される場所は、ブラウザ上(HTTPプロトコルは状態を保持できないため、この方法を使う)。
- 保持する形式は、cookiesを用いる。これは、小さなテキストデータ形式である。
- 本章では、一時cookieを使う。この場合、ブラウザを閉じたら接続が破棄される。永続クッキーは9章で扱う。
- 一時cookiesに保存された情報は、sessionメソッドを用いて、Railsアプリケーションから参照できる(今回は、ログイン中のユーザIDを保存した)。
ルーティングの確認
rails routes
コマンドで、ルーティングの一覧が表示される。$ rails routes Prefix Verb URI Pattern Controller#Action root GET / static_pages#home help GET /help(.:format) static_pages#help about GET /about(.:format) static_pages#about contact GET /contact(.:format) static_pages#contact signup GET /signup(.:format) users#new login GET /login(.:format) sessions#new POST /login(.:format) sessions#create logout DELETE /logout(.:format) sessions#destroy users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroyflash.now
7章で学習したflash変数は、次のリクエストが終了するまでの間、表示される変数である。
しかし、今回はrenderメソッドを用いたので、flashが必要以上に残ってしまった(renderはリクエストが発生しない)。
この場合、flash.nowを使うと解決する。flash.nowは、現在のリクエストが終了するまでの間、表示される。flash.now[:danger] = 'Invalid email/password combination' # ← ここで使用 render 'new'以下を参考にさせていただきました。
https://qiita.com/shi-ma-da/items/ea433c337d2a691ff1bc
https://kossy-web-engineer.hatenablog.com/entry/2018/10/05/063957sessionメソッドの使い方
session情報の追加
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id # ← session情報にユーザIDを追加 end endsession情報の削除
deleteメソッドを使う。
session.delete(:user_id)分からなかったこと、今回はスルーしたこと
- テスト全般
- アプリケーションの仕様の詳細
- 投稿日:2019-10-11T18:01:24+09:00
中間テーブルが更新されない
解決方法
optional: trueをつける!
参考: http://soccer1356abc.hatenablog.com/entry/2018/09/22/210221
user.rbhas_many :talent_users has_many :talents, through: :talent_userstalent.rbhas_many :talent_users has_many :users, dependent: :destroy, through: :talent_userstalent_users.rbbelongs_to :user belongs_to :talent belongs_to :shop, optional: trueuser登録時にtalentも紐づけてcreateしたかった
この時shopは関係ないからoptional: true
をつけるネストさせる
route.rbresources :users do resource :talent_users, only: [:create] endパラメーターに登録したいタレントのidが入るようにしておけばOK!
users_controller.rbdef user_params params.require(:user).permit( :email, :password, :tel ,:sex, :first_name, :last_name, :first_name_kana, :last_name_kana, { :talent_ids=> [] }) end
- 投稿日:2019-10-11T17:57:00+09:00
[初学者]rootメソッド
- 投稿日:2019-10-11T17:18:23+09:00
Rails gem 'webpacker', github: 'rails/webpacker'導入後に rails webpacker:installでエラー
rails webpacker:install実行したらエラーが。。
ターミナルxxxx-no-MacBook-Pro:collabfield groovy$ rails webpacker:install RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment rails aborted! Webpacker configuration file not found /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/config/webpacker.yml /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/config/environment.rb:5:in `<top (required)>' /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/bin/rails:9:in `<top (required)>' /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/bin/spring:15:in `require' /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/bin/spring:15:in `<top (required)>' ./bin/rails:3:in `load' ./bin/rails:3:in `<main>' Caused by: Errno::ENOENT: No such file or directory @ rb_sysopen - /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/config/webpacker.yml /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/config/environment.rb:5:in `<top (required)>' /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/bin/rails:9:in `<top (required)>' /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/bin/spring:15:in `require' /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/bin/spring:15:in `<top (required)>' ./bin/rails:3:in `load' ./bin/rails:3:in `<main>' Tasks: TOP => app:template => environment (See full trace by running task with --trace)
Errno::ENOENT: No such file or directory @ rb_sysopen - /Users/xxxx/Desktop/rails_middle_tutorial/collabfield/config/webpacker.yml
とか出てるので調べてみるとwebpacker.ymlをconfig配下に手動で作るとか載っているのがありました。
早速試してみることに..webpacker.ymlを手動で作成したらその中に以下の参考サイトからコピペする。
https://raw.githubusercontent.com/rails/webpacker/master/lib/install/config/webpacker.yml再度 rails webpacker:installを実行
ターミナルxxxx-no-MacBook-Pro:collabfield xxxx$ rails webpacker:install identical config/webpacker.yml Copying webpack core config create config/webpack create config/webpack/development.js create config/webpack/environment.js create config/webpack/production.js create config/webpack/test.js Copying postcss.config.js to app root directory create postcss.config.js Copying babel.config.js to app root directory create babel.config.js Copying .browserslistrc to app root directory create .browserslistrc Creating JavaScript app source directory create app/javascript create app/javascript/packs/application.js apply /Users/groovy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/bundler/gems/webpacker-2cef2fd6c208/lib/install/binstubs.rb Copying binstubs exist bin create bin/webpack create bin/webpack-dev-server append .gitignore Installing all JavaScript dependencies [4.0.7] run yarn add @rails/webpacker from "." yarn add v1.12.3 info No lockfile found. [1/4] ? Resolving packages... warning @rails/webpacker > postcss-preset-env > postcss-color-functional-notation > postcss-values-parser > flatten@1.0.2: I wrote this module a very long time ago; you should use something else. [2/4] ? Fetching packages... [3/4] ? Linking dependencies... warning "@rails/webpacker > pnp-webpack-plugin > ts-pnp@1.1.4" has unmet peer dependency "typescript@*". [4/4] ? Building fresh packages... success Saved lockfile. warning Your current version of Yarn is out of date. The latest version is "1.19.1", while you're on "1.12.3". info To upgrade, run the following command: $ curl --compressed -o- -L https://yarnpkg.com/install.sh | bash success Saved 592 new dependencies. info Direct dependencies └─ @rails/webpacker@4.0.7 info All dependencies ├─ @babel/core@7.6.4 ├─ @babel/generator@7.6.4 ├─ @babel/helper-builder-binary-assignment-operator-visitor@7.1.0 ├─ @babel/helper-call-delegate@7.4.4 ├─ @babel/helper-create-class-features-plugin@7.6.0 ├─ @babel/helper-define-map@7.5.5 ├─ @babel/helper-explode-assignable-expression@7.1.0 ├─ @babel/helper-wrap-function@7.2.0 ├─ @babel/helpers@7.6.2 ├─ @babel/highlight@7.5.0 ├─ @babel/parser@7.6.4 ├─ @babel/plugin-proposal-async-generator-functions@7.2.0 ├─ @babel/plugin-proposal-class-properties@7.5.5 ├─ @babel/plugin-proposal-dynamic-import@7.5.0 ├─ @babel/plugin-proposal-json-strings@7.2.0 ├─ @babel/plugin-proposal-object-rest-spread@7.6.2 ├─ @babel/plugin-proposal-optional-catch-binding@7.2.0 ├─ @babel/plugin-proposal-unicode-property-regex@7.6.2 ├─ @babel/plugin-transform-arrow-functions@7.2.0 ├─ @babel/plugin-transform-async-to-generator@7.5.0 ├─ @babel/plugin-transform-block-scoped-functions@7.2.0 ├─ @babel/plugin-transform-block-scoping@7.6.3 ├─ @babel/plugin-transform-classes@7.5.5 ├─ @babel/plugin-transform-computed-properties@7.2.0 ├─ @babel/plugin-transform-destructuring@7.6.0 ├─ @babel/plugin-transform-dotall-regex@7.6.2 ├─ @babel/plugin-transform-duplicate-keys@7.5.0 ├─ @babel/plugin-transform-exponentiation-operator@7.2.0 ├─ @babel/plugin-transform-for-of@7.4.4 ├─ @babel/plugin-transform-function-name@7.4.4 ├─ @babel/plugin-transform-literals@7.2.0 ├─ @babel/plugin-transform-member-expression-literals@7.2.0 ├─ @babel/plugin-transform-modules-amd@7.5.0 ├─ @babel/plugin-transform-modules-commonjs@7.6.0 ├─ @babel/plugin-transform-modules-systemjs@7.5.0 ├─ @babel/plugin-transform-modules-umd@7.2.0 ├─ @babel/plugin-transform-named-capturing-groups-regex@7.6.3 ├─ @babel/plugin-transform-new-target@7.4.4 ├─ @babel/plugin-transform-object-super@7.5.5 ├─ @babel/plugin-transform-parameters@7.4.4 ├─ @babel/plugin-transform-property-literals@7.2.0 ├─ @babel/plugin-transform-reserved-words@7.2.0 ├─ @babel/plugin-transform-runtime@7.6.2 ├─ @babel/plugin-transform-shorthand-properties@7.2.0 ├─ @babel/plugin-transform-spread@7.6.2 ├─ @babel/plugin-transform-sticky-regex@7.2.0 ├─ @babel/plugin-transform-template-literals@7.4.4 ├─ @babel/plugin-transform-typeof-symbol@7.2.0 ├─ @babel/plugin-transform-unicode-regex@7.6.2 ├─ @babel/preset-env@7.6.3 ├─ @babel/runtime@7.6.3 ├─ @babel/types@7.6.3 ├─ @rails/webpacker@4.0.7 ├─ @types/q@1.5.2 ├─ @webassemblyjs/floating-point-hex-parser@1.8.5 ├─ @webassemblyjs/helper-code-frame@1.8.5 ├─ @webassemblyjs/helper-fsm@1.8.5 ├─ @webassemblyjs/helper-wasm-section@1.8.5 ├─ @webassemblyjs/wasm-edit@1.8.5 ├─ @webassemblyjs/wasm-opt@1.8.5 ├─ @xtuc/ieee754@1.2.0 ├─ abbrev@1.1.1 ├─ acorn@6.3.0 ├─ ajv-errors@1.0.1 ├─ ajv-keywords@3.4.1 ├─ ajv@6.10.2 ├─ amdefine@1.0.1 ├─ ansi-styles@3.2.1 ├─ anymatch@2.0.0 ├─ are-we-there-yet@1.1.5 ├─ argparse@1.0.10 ├─ arr-flatten@1.1.0 ├─ array-find-index@1.0.2 ├─ asn1.js@4.10.1 ├─ asn1@0.2.4 ├─ assert@1.5.0 ├─ assign-symbols@1.0.0 ├─ async-each@1.0.3 ├─ async-foreach@0.1.3 ├─ asynckit@0.4.0 ├─ atob@2.1.2 ├─ autoprefixer@9.6.4 ├─ aws-sign2@0.7.0 ├─ aws4@1.8.0 ├─ babel-loader@8.0.6 ├─ babel-plugin-macros@2.6.1 ├─ base@0.11.2 ├─ base64-js@1.3.1 ├─ bcrypt-pbkdf@1.0.2 ├─ big.js@5.2.2 ├─ binary-extensions@1.13.1 ├─ block-stream@0.0.9 ├─ boolbase@1.0.0 ├─ brace-expansion@1.1.11 ├─ braces@2.3.2 ├─ browserify-aes@1.2.0 ├─ browserify-cipher@1.0.1 ├─ browserify-des@1.0.2 ├─ browserify-sign@4.0.4 ├─ browserify-zlib@0.2.0 ├─ buffer-xor@1.0.3 ├─ buffer@4.9.1 ├─ builtin-status-codes@3.0.0 ├─ cacache@11.3.3 ├─ cache-base@1.0.1 ├─ caller-callsite@2.0.0 ├─ caller-path@2.0.0 ├─ callsites@2.0.0 ├─ camelcase-keys@2.1.0 ├─ caniuse-lite@1.0.30000999 ├─ case-sensitive-paths-webpack-plugin@2.2.0 ├─ caseless@0.12.0 ├─ chokidar@2.1.8 ├─ chownr@1.1.3 ├─ chrome-trace-event@1.0.2 ├─ cipher-base@1.0.4 ├─ class-utils@0.3.6 ├─ cliui@5.0.0 ├─ clone-deep@4.0.1 ├─ coa@2.0.2 ├─ code-point-at@1.1.0 ├─ collection-visit@1.0.0 ├─ color-convert@1.9.3 ├─ color-name@1.1.3 ├─ color-string@1.5.3 ├─ color@3.1.2 ├─ combined-stream@1.0.8 ├─ commander@2.20.3 ├─ commondir@1.0.1 ├─ compression-webpack-plugin@2.0.0 ├─ concat-map@0.0.1 ├─ concat-stream@1.6.2 ├─ console-browserify@1.1.0 ├─ console-control-strings@1.1.0 ├─ constants-browserify@1.0.0 ├─ convert-source-map@1.6.0 ├─ copy-concurrently@1.0.5 ├─ copy-descriptor@0.1.1 ├─ core-js-compat@3.2.1 ├─ core-js@3.2.1 ├─ core-util-is@1.0.2 ├─ create-ecdh@4.0.3 ├─ create-hmac@1.1.7 ├─ cross-spawn@6.0.5 ├─ crypto-browserify@3.12.0 ├─ css-blank-pseudo@0.1.4 ├─ css-color-names@0.0.4 ├─ css-declaration-sorter@4.0.1 ├─ css-has-pseudo@0.10.0 ├─ css-loader@2.1.1 ├─ css-prefers-color-scheme@3.1.1 ├─ css-select-base-adapter@0.1.1 ├─ css-select@2.0.2 ├─ css-tree@1.0.0-alpha.33 ├─ css-unit-converter@1.1.1 ├─ css-what@2.1.3 ├─ cssdb@4.4.0 ├─ cssesc@2.0.0 ├─ cssnano-preset-default@4.0.7 ├─ cssnano-util-raw-cache@4.0.1 ├─ cssnano-util-same-parent@4.0.1 ├─ cssnano@4.1.10 ├─ csso@3.5.1 ├─ currently-unhandled@0.4.1 ├─ cyclist@1.0.1 ├─ dashdash@1.14.1 ├─ date-now@0.1.4 ├─ debug@2.6.9 ├─ decamelize@1.2.0 ├─ decode-uri-component@0.2.0 ├─ deep-extend@0.6.0 ├─ delayed-stream@1.0.0 ├─ delegates@1.0.0 ├─ des.js@1.0.0 ├─ detect-file@1.0.0 ├─ detect-libc@1.0.3 ├─ diffie-hellman@5.0.3 ├─ dom-serializer@0.2.1 ├─ domain-browser@1.2.0 ├─ domelementtype@1.3.1 ├─ domutils@1.7.0 ├─ dot-prop@4.2.0 ├─ duplexify@3.7.1 ├─ ecc-jsbn@0.1.2 ├─ electron-to-chromium@1.3.280 ├─ emoji-regex@7.0.3 ├─ emojis-list@2.1.0 ├─ enhanced-resolve@4.1.0 ├─ entities@2.0.0 ├─ error-ex@1.3.2 ├─ es-abstract@1.15.0 ├─ es-to-primitive@1.2.0 ├─ escape-string-regexp@1.0.5 ├─ eslint-scope@4.0.3 ├─ esprima@4.0.1 ├─ esrecurse@4.2.1 ├─ estraverse@4.3.0 ├─ events@3.0.0 ├─ execa@1.0.0 ├─ expand-brackets@2.1.4 ├─ expand-tilde@2.0.2 ├─ extend@3.0.2 ├─ extglob@2.0.4 ├─ extsprintf@1.3.0 ├─ fast-deep-equal@2.0.1 ├─ fast-json-stable-stringify@2.0.0 ├─ file-loader@3.0.1 ├─ fill-range@4.0.0 ├─ find-cache-dir@2.1.0 ├─ findup-sync@3.0.0 ├─ flatted@2.0.1 ├─ flatten@1.0.2 ├─ flush-write-stream@1.1.1 ├─ for-in@1.0.2 ├─ forever-agent@0.6.1 ├─ form-data@2.3.3 ├─ from2@2.3.0 ├─ fs-minipass@1.2.7 ├─ fs.realpath@1.0.0 ├─ fsevents@1.2.9 ├─ fstream@1.0.12 ├─ gauge@2.7.4 ├─ gaze@1.1.3 ├─ get-caller-file@2.0.5 ├─ get-stream@4.1.0 ├─ getpass@0.1.7 ├─ glob-parent@3.1.0 ├─ glob@7.1.4 ├─ global-modules@2.0.0 ├─ global-prefix@3.0.0 ├─ globule@1.2.1 ├─ har-schema@2.0.0 ├─ har-validator@5.1.3 ├─ has-ansi@2.0.0 ├─ has-unicode@2.0.1 ├─ has-value@1.0.0 ├─ has-values@1.0.0 ├─ has@1.0.3 ├─ hash.js@1.1.7 ├─ hex-color-regex@1.1.0 ├─ hmac-drbg@1.0.1 ├─ hosted-git-info@2.8.5 ├─ hsl-regex@1.0.0 ├─ hsla-regex@1.0.0 ├─ html-comment-regex@1.1.2 ├─ http-signature@1.2.0 ├─ https-browserify@1.0.0 ├─ iconv-lite@0.4.24 ├─ icss-replace-symbols@1.1.0 ├─ icss-utils@4.1.1 ├─ ieee754@1.1.13 ├─ ignore-walk@3.0.3 ├─ import-cwd@2.1.0 ├─ import-fresh@2.0.0 ├─ import-from@2.1.0 ├─ import-local@2.0.0 ├─ in-publish@2.0.0 ├─ indent-string@2.1.0 ├─ infer-owner@1.0.4 ├─ inflight@1.0.6 ├─ ini@1.3.5 ├─ interpret@1.2.0 ├─ invariant@2.2.4 ├─ invert-kv@2.0.0 ├─ is-absolute-url@2.1.0 ├─ is-accessor-descriptor@1.0.0 ├─ is-arrayish@0.2.1 ├─ is-binary-path@1.0.1 ├─ is-color-stop@1.1.0 ├─ is-data-descriptor@1.0.0 ├─ is-date-object@1.0.1 ├─ is-descriptor@1.0.2 ├─ is-directory@0.3.1 ├─ is-extglob@2.1.1 ├─ is-finite@1.0.2 ├─ is-obj@1.0.1 ├─ is-plain-obj@1.1.0 ├─ is-plain-object@2.0.4 ├─ is-regex@1.0.4 ├─ is-resolvable@1.1.0 ├─ is-stream@1.1.0 ├─ is-svg@3.0.0 ├─ is-symbol@1.0.2 ├─ is-typedarray@1.0.0 ├─ is-utf8@0.2.1 ├─ is-windows@1.0.2 ├─ is-wsl@1.1.0 ├─ isarray@1.0.0 ├─ isexe@2.0.0 ├─ isstream@0.1.2 ├─ js-base64@2.5.1 ├─ js-levenshtein@1.1.6 ├─ js-tokens@4.0.0 ├─ jsesc@2.5.2 ├─ json-parse-better-errors@1.0.2 ├─ json-schema-traverse@0.4.1 ├─ json-schema@0.2.3 ├─ json-stringify-safe@5.0.1 ├─ json5@2.1.1 ├─ jsprim@1.4.1 ├─ last-call-webpack-plugin@3.0.0 ├─ lcid@2.0.0 ├─ load-json-file@1.1.0 ├─ loader-runner@2.4.0 ├─ locate-path@3.0.0 ├─ lodash.get@4.4.2 ├─ lodash.has@4.5.2 ├─ lodash.memoize@4.1.2 ├─ lodash.template@4.5.0 ├─ lodash.templatesettings@4.2.0 ├─ lodash.uniq@4.5.0 ├─ lodash@4.17.15 ├─ loose-envify@1.4.0 ├─ loud-rejection@1.6.0 ├─ make-dir@2.1.0 ├─ mamacro@0.0.3 ├─ map-age-cleaner@0.1.3 ├─ map-obj@1.0.1 ├─ map-visit@1.0.0 ├─ mdn-data@2.0.4 ├─ mem@4.3.0 ├─ memory-fs@0.4.1 ├─ meow@3.7.0 ├─ miller-rabin@4.0.1 ├─ mime-db@1.40.0 ├─ mime-types@2.1.24 ├─ mimic-fn@2.1.0 ├─ mini-css-extract-plugin@0.7.0 ├─ minimalistic-crypto-utils@1.0.1 ├─ minimatch@3.0.4 ├─ minimist@1.2.0 ├─ minipass@2.9.0 ├─ minizlib@1.3.3 ├─ mixin-deep@1.3.2 ├─ mkdirp@0.5.1 ├─ ms@2.1.2 ├─ nan@2.14.0 ├─ nanomatch@1.2.13 ├─ needle@2.4.0 ├─ nice-try@1.0.5 ├─ node-gyp@3.8.0 ├─ node-libs-browser@2.2.1 ├─ node-pre-gyp@0.12.0 ├─ node-releases@1.1.35 ├─ node-sass@4.12.0 ├─ nopt@3.0.6 ├─ normalize-package-data@2.5.0 ├─ normalize-range@0.1.2 ├─ normalize-url@1.9.1 ├─ npm-bundled@1.0.6 ├─ npm-packlist@1.4.6 ├─ npm-run-path@2.0.2 ├─ npmlog@4.1.2 ├─ nth-check@1.0.2 ├─ num2fraction@1.2.2 ├─ oauth-sign@0.9.0 ├─ object-assign@4.1.1 ├─ object-copy@0.1.0 ├─ object-inspect@1.6.0 ├─ object-keys@1.1.1 ├─ object.assign@4.1.0 ├─ object.getownpropertydescriptors@2.0.3 ├─ object.values@1.1.0 ├─ optimize-css-assets-webpack-plugin@5.0.3 ├─ os-browserify@0.3.0 ├─ os-homedir@1.0.2 ├─ os-locale@3.1.0 ├─ os-tmpdir@1.0.2 ├─ osenv@0.1.5 ├─ p-defer@1.0.0 ├─ p-finally@1.0.0 ├─ p-is-promise@2.1.0 ├─ p-limit@2.2.1 ├─ p-locate@3.0.0 ├─ p-try@2.2.0 ├─ pako@1.0.10 ├─ parallel-transform@1.2.0 ├─ parse-json@4.0.0 ├─ parse-passwd@1.0.0 ├─ pascalcase@0.1.1 ├─ path-browserify@0.0.1 ├─ path-complete-extname@1.0.0 ├─ path-dirname@1.0.2 ├─ path-exists@3.0.0 ├─ path-key@2.0.1 ├─ path-parse@1.0.6 ├─ path-type@1.1.0 ├─ performance-now@2.1.0 ├─ pinkie@2.0.4 ├─ pnp-webpack-plugin@1.5.0 ├─ posix-character-classes@0.1.1 ├─ postcss-attribute-case-insensitive@4.0.1 ├─ postcss-calc@7.0.1 ├─ postcss-color-functional-notation@2.0.1 ├─ postcss-color-gray@5.0.0 ├─ postcss-color-hex-alpha@5.0.3 ├─ postcss-color-mod-function@3.0.3 ├─ postcss-color-rebeccapurple@4.0.1 ├─ postcss-colormin@4.0.3 ├─ postcss-convert-values@4.0.1 ├─ postcss-custom-media@7.0.8 ├─ postcss-custom-properties@8.0.11 ├─ postcss-custom-selectors@5.1.2 ├─ postcss-dir-pseudo-class@5.0.0 ├─ postcss-discard-comments@4.0.2 ├─ postcss-discard-duplicates@4.0.2 ├─ postcss-discard-empty@4.0.1 ├─ postcss-discard-overridden@4.0.1 ├─ postcss-double-position-gradients@1.0.0 ├─ postcss-env-function@2.0.2 ├─ postcss-flexbugs-fixes@4.1.0 ├─ postcss-focus-visible@4.0.0 ├─ postcss-focus-within@3.0.0 ├─ postcss-font-variant@4.0.0 ├─ postcss-gap-properties@2.0.0 ├─ postcss-image-set-function@3.0.1 ├─ postcss-import@12.0.1 ├─ postcss-initial@3.0.1 ├─ postcss-lab-function@2.0.1 ├─ postcss-load-config@2.1.0 ├─ postcss-loader@3.0.0 ├─ postcss-logical@3.0.0 ├─ postcss-media-minmax@4.0.0 ├─ postcss-merge-longhand@4.0.11 ├─ postcss-merge-rules@4.0.3 ├─ postcss-minify-font-values@4.0.2 ├─ postcss-minify-gradients@4.0.2 ├─ postcss-minify-params@4.0.2 ├─ postcss-minify-selectors@4.0.2 ├─ postcss-modules-extract-imports@2.0.0 ├─ postcss-modules-local-by-default@2.0.6 ├─ postcss-modules-scope@2.1.0 ├─ postcss-modules-values@2.0.0 ├─ postcss-nesting@7.0.1 ├─ postcss-normalize-charset@4.0.1 ├─ postcss-normalize-display-values@4.0.2 ├─ postcss-normalize-positions@4.0.2 ├─ postcss-normalize-repeat-style@4.0.2 ├─ postcss-normalize-string@4.0.2 ├─ postcss-normalize-timing-functions@4.0.2 ├─ postcss-normalize-unicode@4.0.1 ├─ postcss-normalize-url@4.0.1 ├─ postcss-normalize-whitespace@4.0.2 ├─ postcss-ordered-values@4.1.2 ├─ postcss-overflow-shorthand@2.0.0 ├─ postcss-page-break@2.0.0 ├─ postcss-place@4.0.1 ├─ postcss-preset-env@6.7.0 ├─ postcss-pseudo-class-any-link@6.0.0 ├─ postcss-reduce-initial@4.0.3 ├─ postcss-reduce-transforms@4.0.2 ├─ postcss-replace-overflow-wrap@3.0.0 ├─ postcss-safe-parser@4.0.1 ├─ postcss-selector-matches@4.0.0 ├─ postcss-selector-not@4.0.0 ├─ postcss-svgo@4.0.2 ├─ postcss-unique-selectors@4.0.1 ├─ prepend-http@1.0.4 ├─ private@0.1.8 ├─ process-nextick-args@2.0.1 ├─ process@0.11.10 ├─ prr@1.0.1 ├─ pseudomap@1.0.2 ├─ psl@1.4.0 ├─ public-encrypt@4.0.3 ├─ pumpify@1.5.1 ├─ punycode@1.4.1 ├─ q@1.5.1 ├─ qs@6.5.2 ├─ query-string@4.3.4 ├─ querystring-es3@0.2.1 ├─ querystring@0.2.0 ├─ randomfill@1.0.4 ├─ rc@1.2.8 ├─ read-cache@1.0.0 ├─ read-pkg@1.1.0 ├─ readdirp@2.2.1 ├─ redent@1.0.0 ├─ regenerate-unicode-properties@8.1.0 ├─ regenerator-transform@0.14.1 ├─ regjsgen@0.5.0 ├─ regjsparser@0.6.0 ├─ remove-trailing-separator@1.1.0 ├─ repeat-element@1.1.3 ├─ repeating@2.0.1 ├─ request@2.88.0 ├─ require-main-filename@2.0.0 ├─ resolve-cwd@2.0.0 ├─ resolve-dir@1.0.1 ├─ resolve-url@0.2.1 ├─ ret@0.1.15 ├─ rgb-regex@1.0.1 ├─ rgba-regex@1.0.0 ├─ rimraf@2.7.1 ├─ run-queue@1.0.3 ├─ safer-buffer@2.1.2 ├─ sass-graph@2.2.4 ├─ sass-loader@7.3.1 ├─ sax@1.2.4 ├─ scss-tokenizer@0.2.3 ├─ semver@5.7.1 ├─ serialize-javascript@1.9.1 ├─ set-value@2.0.1 ├─ setimmediate@1.0.5 ├─ shallow-clone@3.0.1 ├─ shebang-command@1.2.0 ├─ shebang-regex@1.0.0 ├─ simple-swizzle@0.2.2 ├─ snapdragon-node@2.1.1 ├─ snapdragon-util@3.0.1 ├─ sort-keys@1.1.2 ├─ source-list-map@2.0.1 ├─ source-map-resolve@0.5.2 ├─ source-map-support@0.5.13 ├─ source-map-url@0.4.0 ├─ spdx-correct@3.1.0 ├─ spdx-exceptions@2.2.0 ├─ split-string@3.1.0 ├─ sprintf-js@1.0.3 ├─ sshpk@1.16.1 ├─ stable@0.1.8 ├─ static-extend@0.1.2 ├─ stdout-stream@1.4.1 ├─ stream-browserify@2.0.2 ├─ stream-each@1.2.3 ├─ stream-http@2.8.3 ├─ strict-uri-encode@1.1.0 ├─ string_decoder@1.1.1 ├─ string.prototype.trimleft@2.1.0 ├─ string.prototype.trimright@2.1.0 ├─ strip-bom@2.0.0 ├─ strip-eof@1.0.0 ├─ strip-indent@1.0.1 ├─ strip-json-comments@2.0.1 ├─ style-loader@0.23.1 ├─ stylehacks@4.0.3 ├─ supports-color@6.1.0 ├─ svgo@1.3.0 ├─ tar@2.2.2 ├─ terser-webpack-plugin@1.4.1 ├─ terser@4.3.8 ├─ through2@2.0.5 ├─ timers-browserify@2.0.11 ├─ timsort@0.3.0 ├─ to-arraybuffer@1.0.1 ├─ to-fast-properties@2.0.0 ├─ to-object-path@0.3.0 ├─ to-regex-range@2.1.1 ├─ tough-cookie@2.4.3 ├─ trim-newlines@1.0.0 ├─ true-case-path@1.0.3 ├─ ts-pnp@1.1.4 ├─ tslib@1.10.0 ├─ tty-browserify@0.0.0 ├─ tunnel-agent@0.6.0 ├─ tweetnacl@0.14.5 ├─ typedarray@0.0.6 ├─ unicode-canonical-property-names-ecmascript@1.0.4 ├─ unicode-match-property-ecmascript@1.0.4 ├─ unicode-match-property-value-ecmascript@1.1.0 ├─ unicode-property-aliases-ecmascript@1.0.5 ├─ union-value@1.0.1 ├─ unique-slug@2.0.2 ├─ unquote@1.1.1 ├─ unset-value@1.0.0 ├─ upath@1.2.0 ├─ uri-js@4.2.2 ├─ urix@0.1.0 ├─ url@0.11.0 ├─ use@3.1.1 ├─ util-deprecate@1.0.2 ├─ util.promisify@1.0.0 ├─ util@0.11.1 ├─ uuid@3.3.3 ├─ v8-compile-cache@2.0.3 ├─ validate-npm-package-license@3.0.4 ├─ vendors@1.0.3 ├─ verror@1.10.0 ├─ vm-browserify@1.1.0 ├─ watchpack@1.6.0 ├─ webpack-assets-manifest@3.1.1 ├─ webpack-cli@3.3.9 ├─ webpack@4.41.0 ├─ which-module@2.0.0 ├─ which@1.3.1 ├─ wide-align@1.1.3 ├─ worker-farm@1.7.0 ├─ wrap-ansi@5.1.0 ├─ xtend@4.0.2 ├─ yallist@3.1.1 ├─ yargs-parser@13.1.1 └─ yargs@13.2.4 ✨ Done in 24.18s. Installing dev server for live reloading run yarn add --dev webpack-dev-server from "." yarn add v1.12.3 [1/4] ? Resolving packages... [2/4] ? Fetching packages... [3/4] ? Linking dependencies... warning "@rails/webpacker > pnp-webpack-plugin > ts-pnp@1.1.4" has unmet peer dependency "typescript@*". warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0". warning " > webpack-dev-server@3.8.2" has unmet peer dependency "webpack@^4.0.0". [4/4] ? Building fresh packages... success Saved lockfile. success Saved 101 new dependencies. info Direct dependencies └─ webpack-dev-server@3.8.2 info All dependencies ├─ @types/events@3.0.0 ├─ @types/glob@7.1.1 ├─ @types/minimatch@3.0.3 ├─ @types/node@12.7.12 ├─ accepts@1.3.7 ├─ ansi-colors@3.2.4 ├─ ansi-html@0.0.7 ├─ array-flatten@1.1.1 ├─ array-union@1.0.2 ├─ array-uniq@1.0.3 ├─ async-limiter@1.0.1 ├─ async@1.5.2 ├─ batch@0.6.1 ├─ body-parser@1.19.0 ├─ bonjour@3.5.0 ├─ buffer-indexof@1.1.1 ├─ cliui@4.1.0 ├─ compressible@2.0.17 ├─ compression@1.7.4 ├─ connect-history-api-fallback@1.6.0 ├─ content-disposition@0.5.3 ├─ cookie-signature@1.0.6 ├─ cookie@0.4.0 ├─ deep-equal@1.1.0 ├─ default-gateway@4.2.0 ├─ del@4.1.1 ├─ destroy@1.0.4 ├─ detect-node@2.0.4 ├─ dns-equal@1.0.0 ├─ dns-packet@1.3.1 ├─ dns-txt@2.0.2 ├─ ee-first@1.1.1 ├─ eventemitter3@4.0.0 ├─ eventsource@1.0.7 ├─ express@4.17.1 ├─ faye-websocket@0.10.0 ├─ finalhandler@1.1.2 ├─ follow-redirects@1.9.0 ├─ forwarded@0.1.2 ├─ globby@6.1.0 ├─ handle-thing@2.0.0 ├─ hpack.js@2.1.6 ├─ html-entities@1.2.1 ├─ http-deceiver@1.2.7 ├─ http-parser-js@0.4.10 ├─ http-proxy-middleware@0.19.1 ├─ http-proxy@1.18.0 ├─ internal-ip@4.3.0 ├─ ip-regex@2.1.0 ├─ ip@1.1.5 ├─ ipaddr.js@1.9.1 ├─ is-absolute-url@3.0.3 ├─ is-arguments@1.0.4 ├─ is-path-cwd@2.2.0 ├─ is-path-in-cwd@2.1.0 ├─ is-path-inside@2.1.0 ├─ json3@3.3.3 ├─ killable@1.0.1 ├─ loglevel@1.6.4 ├─ media-typer@0.3.0 ├─ merge-descriptors@1.0.1 ├─ methods@1.1.2 ├─ mime@2.4.4 ├─ multicast-dns-service-types@1.1.0 ├─ multicast-dns@6.2.3 ├─ negotiator@0.6.2 ├─ node-forge@0.9.0 ├─ object-is@1.0.1 ├─ obuf@1.1.2 ├─ on-headers@1.0.2 ├─ opn@5.5.0 ├─ original@1.0.2 ├─ p-map@2.1.0 ├─ p-retry@3.0.1 ├─ path-is-inside@1.0.2 ├─ path-to-regexp@0.1.7 ├─ portfinder@1.0.24 ├─ proxy-addr@2.0.5 ├─ querystringify@2.1.1 ├─ raw-body@2.4.0 ├─ regexp.prototype.flags@1.2.0 ├─ retry@0.12.0 ├─ select-hose@2.0.0 ├─ selfsigned@1.10.7 ├─ serve-index@1.9.1 ├─ serve-static@1.14.1 ├─ sockjs-client@1.4.0 ├─ sockjs@0.3.19 ├─ spdy-transport@3.0.0 ├─ spdy@4.0.1 ├─ thunky@1.0.3 ├─ type-is@1.6.18 ├─ unpipe@1.0.0 ├─ utils-merge@1.0.1 ├─ wbuf@1.7.3 ├─ webpack-dev-middleware@3.7.2 ├─ webpack-dev-server@3.8.2 ├─ websocket-extensions@0.1.3 ├─ ws@6.2.1 ├─ yargs-parser@11.1.1 └─ yargs@12.0.5 ✨ Done in 8.46s. You need to allow webpack-dev-server host as allowed origin for connect-src. This can be done in Rails 5.2+ for development environment in the CSP initializer config/initializers/content_security_policy.rb with a snippet like this: policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? Webpacker successfully installed ? ?うまくいったようです。
でも何が原因だったかわかりません。
どなたか教えてくださいませー。
- 投稿日:2019-10-11T17:04:10+09:00
[初学者]ルーティングについて
目的
学習の備忘録と初学者の参考資料として投稿
ルーティング
ルーティングは、ブラウザから届いたリクエスト(HTTPメソッド+URL)に対して、コントローラーで定義したアクションを結びつけるルールです。
上記は参考例です。
ルーティングの確認
ターミナルで $ rails routes あるいは...ブラウザで http://localhost:3000/rails/info/routes と入力どちらでも確認出来ます。
HTTPメソッド
HTTPメソッドとは、「クライアントがサーバーにしてほしいことを依頼するための手段」のこと。
主に使うのは『GET』『POST』『PUT』『DELETE』の4つぐらいです。
それぞれの働きは『GET』 ・・・データを取得するときに利用する。
『POST』 ・・・サーバーにデータを送信する時に利用する。アカウント作成や投稿するなど新規作成で使われる。
『PUT』 ・・・サーバーにデータを送信する時に利用する。既存データの更新などで使われる。
『DELETE』・・・既存データを削除するときに利用する。
任意のアクションを呼び出したい時は
http://(ホスト名)/コントロール名/アクション名
で呼び出すことが可能です。
まとめ
今回は簡単なさわりだけを書いています。今後さらに深掘りして書いていきます。
今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします
- 投稿日:2019-10-11T16:58:47+09:00
Rails で CSRF トークンの検証を制御する
特定のコントローラで検証を無効化する
skip_forgery_protection
を使います。
これはskip_before_action :verify_authenticity_token
のラッパーなのでonly: :create
等のオプションを指定できます。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base skip_forgery_protection end大本のコントローラで指定して継承先のコントローラで重複して使用はできないので、その場合は
protect_from_forgery with: :exception
等を明示的に指定します。app/controllers/article_controller.rbclass ArticleController < ApplicationController protect_from_forgery with: :exception, only: :create def create # ... end end検証を完全に無効化する
検証そのものを行わないようにします。
config/application.rbconfig.action_controller.allow_forgery_protection = false警告だけを消す
検証は行って
WARN
のログは出さないようにします。config/application.rbconfig.action_controller.log_warning_on_csrf_failure = falseRails 5.2 や 6 以降で
null_session
rails 5.2 以降はデフォルトで
protect_from_forgery with: :exception
となるような変更(https://github.com/rails/rails/commit/ec4a836919c021c0a5cf9ebeebb4db5e02104a55)が入り、ApplicationController
からはprotect_from_forgery
の記述が消されました。
ApplicationController
にprotect_from_forgery null: :session
としても反映されないので以前のようにwith: :null_session
を使いたい場合は以下のようにします。config/application.rbconfig.action_controller.default_protect_from_forgery = falseapp/controllers/application_controller.rbclass ApplicationController < ActionController::Base # protect_from_forgery に with オプションを渡さない場合は with: :null_session と同等 protect_from_forgery end
- 投稿日:2019-10-11T16:47:31+09:00
graphql-ruby×nuxt.js 画像アップロード
走り書きすみません。綺麗にします。
Rails
carrierwaveuploader/carrierwaveを設定した後のお話。
ModelとUploaderを設定してね。GraphqlController
jaydenseric/apollo-upload-clientコレを使うと、ActionController::Parametersに入ってくる値が変化する。(リクエストを変更している。)
~/app/controllers/graphql_controller.rbclass GraphqlController < ActionController::API def execute if params[:operations].present? param = JSON.parse(params[:operations]) query = param["query"] operation_name = param["operationName"] variables = { "file" => params["1"], } else # コレ要らないかも? variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] end context = { session: session, current_user: current_user, } result = ApiSchema.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result rescue JWT::ExpiredSignature, JWT::DecodeError, JWT::VerificationError => e render json: { error: { message: e.message } }, status: :unauthorized rescue => e raise e unless Rails.env.development? handle_error_in_development e end endImageType
~/app/graphql/scalar_types/image_type.rbmodule ScalarTypes class ImageType < Types::BaseScalar graphql_name 'ImageType' description 'ActionDispatch::Http::UploadedFile' def coerce_input(file, _context) ActionDispatch::Http::UploadedFile.new( filename: file.original_filename, type: file.content_type, head: file.headers, tempfile: file.tempfile ) end def coerce_result(value, _context) I18n.l(value, format: :default) end end end中身の確認
variables["file"].original_filename # => "hogehoge.jpg" variables["file"].content_type # => "image/jpeg" variables["file"].headers # => "Content-Disposition: form-data; name=\"1\"; filename=\"hogehoge.jpg\"\r\nContent-Type: image/jpeg\r\n" variables["file"].tempfile # => #<File:/tmp/RackMultipart20191011-1-vtsg19.jpg>Mutation
~/app/graphql/mutations/user_resource/update_user_profile_image.rbmodule Mutations module UserResource class UpdateUserProfileImage < Mutations::BaseMutation null false argument :profile_image, ScalarTypes::ImageType, required: true field :results, Boolean, null: true def resolve(profile_image:) ActiveRecord::Base.transaction do user = context[:current_user] user.remove_profile_image! if user.profile_image user.profile_image = profile_image user.save ? { results: true } : { results: false } end end end end end~/app/graphql/object_types/user_type.rbmodule ObjectTypes class UserType < Types::BaseObject field :id, ID, null: false field :profile_image, ScalarTypes::ImageType, null: true field :created_at, GraphQL::Types::ISO8601DateTime, null: true field :updated_at, GraphQL::Types::ISO8601DateTime, null: true end endNuxt
Apollo設定
以下のコードは使いません。
~/apollo/client-configs/default.jsimport { HttpLink } from 'apollo-link-http' export default () => { const httpLink = new HttpLink({ uri: 'http://localhost:3000/graphql' }) }このnode_moduleを使います。jaydenseric/apollo-upload-client
入力フォーム
vue-upload-componentインストールする。
~/apollo/client-configs/default.jsimport { ApolloLink } from 'apollo-link' import { InMemoryCache } from 'apollo-cache-inmemory' import { createUploadLink } from 'apollo-upload-client' export default () => { const uploadLink = new createUploadLink({ uri: 'http://localhost:3000/graphql' }) const current_user = JSON.parse(localStorage.getItem('current_user')) const middlewareLink = new ApolloLink((operation, forward) => { operation.setContext({ headers: { authorization: !current_user ? '' : current_user.token ? `Bearer ${current_user.token}` : '' } }) return forward(operation) }) const link = ApolloLink.from([ middlewareLink, uploadLink ]) return { link, cache: new InMemoryCache() } }~/pages/settings/profile.vue<template> <v-container> <label for="profile_image">Button</label> <file-upload extensions='gif,jpg,jpeg,png,webp' accept='image/png,image/gif,image/jpeg,image/webp' name='profile_image' v-model='profileImage' @input-filter='inputFilter' @input-file='inputFile' ref='upload'/> </v-container> </template> <script> import UpdateUserProfileImage from '~/apollo/gql/mutations/user_resource/updateUserProfileImage.gql' import FileUpload from 'vue-upload-component' export default { components: { FileUpload }, data: () => ({ profileImage: [] }), methods: { async storeProfileImage(file) { await this.$apollo.mutate({ mutation: UpdateUserProfileImage, variables: { file: file.file } }).then(res => { console.log(res) }).catch(err => { console.error(err) }) }, inputFile(newFile) { if (newFile) { this.$nextTick(function() { this.storeProfileImage(newFile) }) } }, inputFilter(newFile, oldFile, prevent) { if (newFile && !oldFile) { if (!/\.(gif|jpg|jpeg|png|webp)$/i.test(newFile.name)) { return prevent() } } if (newFile && (!oldFile || newFile.file !== oldFile.file)) { newFile.url = '' let URL = window.URL || window.webkitURL if (URL && URL.createObjectURL) { newFile.url = URL.createObjectURL(newFile.file) } } } } } </script>GQL
引数の名前が$fileでなければ、エラーが発生するっぽい。
jaydenseric/apollo-upload-client
Usage
Use FileList, File, Blob or ReactNativeFile instances anywhere within query or mutation variables to send a > GraphQL multipart request.
~/apollo/gql/mutations/user_resource/updateUserProfileImage.gqlmutation UpdateUserProfileImage($file: ImageType!) { updateUserProfileImage(profileImage: $file) { results } }残った疑問
- ↓調べる。
ActionController::Parameters
- 投稿日:2019-10-11T15:49:04+09:00
【Ruby】世界のナベアツのあのギャグで遊んでみよう
目次
- 1. この記事の狙い
- 2. ターゲット層
- 3. 実行環境
- 4. 世界のナベアツについて
- 5. Rubyコード
- 5-1. 期待する挙動
- 5-2. Comedianクラス実装
- 5-3. 実際に動かしてみよう
1. この記事の狙い
世の中には伊藤淳一さんのチェリー本(私も大変おせわになりました
)をはじめとした入門書、オンライン上でも無料の教材が揃っているので、インプットで不便することはない。
しかし、アウトプットをしないと自分の力に変換出来ないし忘れてしまう。なるべくなら肩の力を抜いて遊びながら基礎をおさらい出来たらあまり疲れないで済むで良い。
現実世界にはそんな遊びに適した材料が転がってるので、自分のリフレッシュも兼ねて思いつく限りで記事に書き起こしてみようというのが狙い。2. ターゲット層
- Rubyの入門書読んだけどまだ自力でスクラッチでコード書けない初心者の方
- 「こんなのもコードにしたら面白いんじゃない?」という好奇心に満ちた方(レベル問わず)
3. 実行環境
- ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]
- MacOS Version 10.14.6
4. 世界のナベアツについて
- 元ジャリズムのコンビ芸人
- 「3の倍数と3のつく数字の時だけアホになる」ギャグで一斉風靡
- 現在は桂三度の芸名で落語家として活動中
- 詳しくはWikipediaを参照
5. Rubyコード
5-1. 期待する挙動
- オブジェクトを生成すると芸能プロダクションと芸名を添えて自己紹介する
- 芸名が「世界のナベアツ」の時だけ例のギャグを発動する
- アホの状態は数字に「!」を付けて表現
5-2. Comedianクラス実装
class Comedian def initialize(name:, age:, agency:) @name = name @age = age @agency = agency puts "こんにちは。私は#{@agency}で芸人をやっております#{@name}です。" return false unless @name == '世界のナベアツ' arr = 1.upto(40).map { |num| go_crazy(num) } puts arr.join(',') end def go_crazy(num) num % 3 == 0 || num.to_s.include?('3') ? "#{num}!" : num.to_s # データ型は全て文字列に統一 end end少し解説を入れる。
- initializeメソッドはオブジェクトを初期化するメソッド。
クラス.new
時に呼ばれる。なお、initializeメソッド自体はデフォルトでプライベートメソッド(外部からアクセス出来ない)ため、クラス.initialize
は不可。- 属性は
name
age
agency
の3つ。オブジェクト生成時の引数を単にComedian('芸名', 30, 'ナベプロ')
のようにすると何を表しているのか分かりづらい(可読性が低い)ので、キーワード引数を使用(今回はデフォルト値なし)。return false unless @name == '世界のナベアツ'
では、もし芸名が「世界のナベアツ」出ない場合、それ以降の処理が流れないようにfalse
を返している。この記法はGuard Clauseと呼ばれるもの。同じ処理をしようと以下のコードを書くとRubocopのテストに怒られる。if @name == '世界のナベアツ' arr = 1.upto(40).map { |num| go_foolish(num) } puts arr.join(',') end
- 例のギャグは「1から40までの数字」という範囲なので、mapを使ってループでギャグインスタンスメソッドを呼び、実行結果を配列に格納。そのまま出力すると配列の
[]
や文字列の""
が表示され美しくないのでjoinを使い,
を境に結合- 呼び出し元の処理の中身は、
num % == 0
で3で割り切れる数、num.to_s.include?('3')
で引数の数字を一旦文字列に変換し'3'という文字が含まれているか判定。いずれかの条件に当てはまる場合は引数の数字を文字列変換し「!」を付ける。それ以外の数字はそのまま integer で出力。5-3. 実際に動かしてみよう
irb(main):016:0> Comedian.new(name: '世界のナベアツ', age: 50, agency: '吉本興業') こんにちは。私は吉本興業で芸人をやっております世界のナベアツです。 1,2,3!,4,5,6!,7,8,9!,10,11,12!,13!,14,15!,16,17,18!,19,20,21!,22,23!,24!,25,26,27!,28,29,30!,31!,32!,33!,34!,35!,36!,37!,38!,39!,40 => #<Comedian:0x00007f90840aa6d8 @name="世界のナベアツ", @age=50, @agency="吉本興業">ちゃんとアホになった。
irb(main):018:0> Comedian.new(name: '千原ジュニア', age: 45, agency: '吉本興業') こんにちは。私は吉本興業で芸人をやっております千原ジュニアです。 => #<Comedian:0x00007f90839629e8 @name="千原ジュニア", @age=45, @agency="吉本興業">違う芸人ではギャグは発動しない。
- 投稿日:2019-10-11T14:44:32+09:00
Ruby 正規表現の学習2
正規表現の様々なパターンを使ってみる
先日は
subメソッド
、matchメソッド
の基本的な使い方を載せてみました。
今回は、正規表現の様々なパターンを使って少しだけ応用的な使用方法を使ってみたいと思います。
今回、使ってみるパターンは以下の3つ。
- 電話番号のハイフンを取り除く
- パスワードに英数字8文字以上という制約を設定
- メールアドレスからドメインの部分のみ抽出
1. 電話番号からハイフンを取り除く
ターミナルirb(main):001:0> tel = '090-1234-5678' => "090-1234-5678" irb(main):002:0> tel.sub(/-/,'') => "0901234-5678" # 最初のハイフンしか置換えされない irb(main):003:0> tel.gsub(/-/,'') => "09012345678"ポイント
- グローバルマッチの
g
sub
の前にg
が追加された、gsub
メソッドと言い、g
が意味するのは、グローバルマッチと言う。
文字列内で指定した文字が複数含まれている場合、その全てを置換えすると言う意味になる。
gsub
だけではなくsub
を使用した場合は、初めの1つだけ置換えされることになる。2. パスワードに英数字8文字以上という制約を設定
以下はパスワードに「Hoge1234」という大文字小文字を区別した英字と数字を使用して、
matchメソッド
を使用して記述してみる。ターミナルirb(main):001:0> pass = 'Hoge1234' => "Hoge1234" irb(main):002:0> pass.match(/[a-z\d{8,}/i) => #<MatchData "Hoge1234">ポイント
[a-z]
: 角括弧で囲まれた文字のいずれか1個にマッチ\d
: 数字にマッチ{n, m}
: 直前の文字が少なくとも n 回、多くても m 回出現するものにマッチi
: 大文字・小文字を区別しない検索[a-z] : 角括弧で囲まれた文字のいずれか 1個にマッチ
a~cの英字を抽出
「dog」にはa〜cのどの英字も含まれていないのでマッチしない。ターミナルirb(main):001:0> 'dog'.match(/[a-c]/) => nil\d : 数字にマッチ
\d
のd
は数字を表す。数字と表すd
のような文字を特殊文字と呼び、特殊文字を使用する場合は直前に\
を記述するというルールがある。[a-z\d]
は「英数字のいずれか1つにマッチ」という意味になる。ターミナルirb(main):001:0> 'I have 3 pens'.match(/\d/) => #<MatchData "3">{n, m} : 直前の文字が少なくとも n 回、多くても m 回出現するものにマッチ
少なくとも4回、多くても6回出現するものにマッチ
波括弧を使用することで文字数の制約を追加することができる。{4,6}
は、直前の文字が少なくとも下記の場合は4回多くても6回数字がマッチという意味になり、2回目のirbはマッチする数字がないのでnil
と返される。ターミナルirb(main):001:0> '12345678'.match(/\d{4,6}/) => #<MatchData "123456"> irb(main):002:0> '123'.match(/\d{4,6}/) => nili : 大文字・小文字を区別しない検索
i
オプションを加えることで大文字・小文字を区別しないで検索する。i
オプションをつけずに[a-z]
と小文字で記述すると大文字にマッチしなくなる。- 大文字・小文字の区別
ターミナルirb(main):003:0> 'Cat'.match(/cat/) => nil irb(main):004:0> 'Cat'.match(/cat/i) => #<MatchData "Cat">実践的な使用例
irbpass = 'Hoge1234' if pass.match(/[a-z\d]{8,}/i) // パスワード設定の処理 else puts 'パスワードの形式が間違えています。' endメールアドレスからドメインの部分のみ抽出
「hoge@sample-taka.com」というアドレスから「@sample-taka.com」の部分のみを取得したい場合。
ターミナルirb(main):001:0> mail = 'hoge@sample-taka.com' => "hoge@sample-taka.com" irb(main):002:0> mail.match(/@.+/) => #<MatchData "@sample-taka.com">ポイント
.
: どの1 文字にもマッチ+
: 直前の文字の 1 回以上の繰り返しにマッチ
.
どの1文字にもマッチハイフンやピリオドなど含めた全ての英数字において、どの1文字にもマッチする。
(例)
ターミナルirb(main):001:0> 'hoge'.match(/./) => #<MatchData "h">
+
直前の文字の 1 回以上の繰り返しにマッチ直前の文字が 1 回以上の繰り返しにマッチする。
(例)
ターミナルirb(main):001:0> 'aaabb'.match(/a+/) => #<MatchData "aaa">以上の例に沿ってみると
.+
は何かしたの文字が一回以上繰り返されるものにマッチする。- 先頭に
@
をつけることで「@から始まり、何かしらの文字が 1 回以上口返すものにマッチ」という意味になる。まとめ
パターン 意味 [a-z] 角括弧で囲まれた文字のいずれか 1 個にマッチ \d 数字にマッチ {n,m} 直前の文字が少なくとも n 回、多くても m 回出現するものにマッチ . どの 1 文字にもマッチ + 直前の文字の 1 回以上の繰り返しにマッチ まだまだ奥深い正規表現ですが以上のことだけは最低限おさえて置きたいと思います。。。
- 投稿日:2019-10-11T13:34:11+09:00
Railsで既存モデルをポリモーフィック化したら大変だった話
前提
社内でやろうとしてたことそのまま書けないので、
例えを探しましたが・・・わかりにくいかもですw
ポリモーフィックについては、こちらの記事がわかりやすかったです!感謝感激。モデルは下記とします。
モデル図 *()の中がモデル名
普通自動車(Car)-<部品(Part)>-人(Owner)Car
has_many :parts
has_many :owners, through: :partsPart
belongs_to :cars
belongs_to :ownersOwner
has_many :parts
has_many :cars, through: :parts目的は、
「ある人が所持している、ある普通自動車に搭載されている部品達をDBで管理できる」
とします。もうちょっと具体的にいうと、
「AさんのIDから次郎号という自動車を特定できて、
かつ次郎号に搭載されている部品(前輪・後輪とか)も把握できる」
みたいな感じです。異なるモデルも同一のモデルで管理したい
上記モデルをポリモーフィック可したらどうなるかというと
モデルはこんな感じになります。*()の中がモデル名
モデル図
なんかの乗り物-<部品(Part)>-人(Owner)なんか乗り物(ここではCarとShipとしておきます)
has_many :parts
has_many :owners, as: :vehicle, through: :partsPart
belongs_to :vehicles, polymorphic: true
belongs_to :ownersOwner
has_many :parts
has_many :cars, as: :vehicle, through: :parts
has_many :ships, as: :vehicle, through: :parts要するに
「ある人が所持している、ある乗り物に搭載されている部品達をDBで管理できる」何が良いの?となりますが
普通自動車以外の乗り物(バイク、船 etc...)の部品もPartモデルに突っ込めます!
Partモデルにvehicle_typeとvehicle_idというカラムを追加してあげることで
(vehicle_type = 'Car' とか vehicle_type = 'Ship'とか・・・)
Partからみたら、繋がりは一つに見えるのに色々な乗り物モデルと繋がれる
という状態になります。何が大変だったか
さて、本題ですが
1つ前のセクションでポリモーフィック化は完了したとします。
となると、既存のソース内で変更が必要になってきます。まず当時の私は、下記のクエリは問題ないと思っていました。(いつ使うんだこのクエリは!と言わないでくださいw)
「ある車に紐付いている、personを取り出す。条件は、男性で前輪を持っている人」
car = Car.find_by("123-456-789")
car.persons.joins(:parts).find_by('persons.sex = ? AND parts.name = ?', "male",
"前輪")これを叩くと、以下を含んだクエリが作成されます。
WHERE "parts"."car_id" = $1
ポリモーフィック化する以前は、Carモデルしかなかったのでこれで良かったのですが
ポリモーフィック後はvehicle_idというカラムに変わっているのでエラーになります。ここで注意すべきは、「じゃあparts.vehicle_idにすればいいのでは?」・・それでは、事足りないということです。
具体的には、先ほどのクエリをこんな風にすればOKです。
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', car.id, 'Car', "male", "前輪")
vehicle_idと合わせてvehicle_typeも引き合いに出すことで期待する値を確実に取ることができます。まとめ
既存モデルをポリモーフィック化すると、そのモデルの先で叩くクエリを書き直さなければならない場合があります。
その際は、XXX_typeというようにクエリへモデルのタイプ(String)を含める必要があります。おまけ: さらに抽象度を上げるには?
下記のクエリは、Carモデル用にハードコーディングされてます。
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', car.id, 'Car', "male", "前輪")
こんな風に変えるといい感じです!
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', vehicle.id, vehicle.class.to_s, "male", "前輪")
こうすれば、仮に変数vehicleにCarモデルが入ろうが、Shipモデルが入ろうが吸収してくれます。参考
- 投稿日:2019-10-11T12:43:51+09:00
Rails6 のちょい足しな新機能を試す94(ActiveModel falsy symbol編)
はじめに
Rails 6 に追加された新機能を試す第94段。 今回は、
ActiveModel falsy simbol
編です。
Rails 6 では、:false
など、false
を連想させる symbol が ActiveModel (ActiveRecord) の値として、 false として扱われるようになりました。Ruby 2.6.4, Rails 6.0.0 で確認しました。
なお、こちらは、Rails 5.2.4 以降でも同様の振舞いに変更されるものと思われます。
$ rails --version Rails 6.0.0プロジェクトを作る
$ rails new rails_sandbox $ cd rails_sandbox今回は Book モデルを作って rails console で確認してみます。
Book モデルを作る
title
とpublished
の2つの属性を持つ Book モデルを作ります。$ bin/rails g model Book title published:boolean
seed データを作る
seed データを作ります。
db/seeds.rbBook.create( [ { title: 'Agile Web Development with Rails 5.1', published: true }, { title: 'Agile Web Development with Rails 6', published: false } ] )rails console を実行する
rails console を使って確認してみます。
:"0"
,:f
,:F
,:false
,:FALSE
,:off
,:OFF
が falsy な値として扱われます。
ActiveModel::Type::Boolean::FALSE_VALUES
でどの値が falsy な値となるのか確認することができます。irb(main):001:0> ActiveModel::Type::Boolean::FALSE_VALUES => #<Set: {false, 0, "0", :"0", "f", :f, "F", :F, "false", :false, "FALSE", :FALSE, "off", :off, "OFF", :OFF}>:"0" の場合
irb(main):002:0> Book.where(published: :"0") Book Load (0.3ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:f の場合
irb(main):003:0> Book.where(published: :f) Book Load (0.8ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:F の場合
irb(main):004:0> Book.where(published: :F) Book Load (0.8ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:false の場合
irb(main):005:0> Book.where(published: :false) Book Load (0.7ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:FALSE の場合
irb(main):006:0> Book.where(published: :FALSE) Book Load (0.7ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:off の場合
irb(main):007:0> Book.where(published: :off) Book Load (0.8ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>:OFF の場合
irb(main):008:0> Book.where(published: :OFF) Book Load (0.7ms) SELECT "books".* FROM "books" WHERE "books"."published" = $1 LIMIT $2 [["published", false], ["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 2, title: "Agile Web Development with Rails 6", published: false, created_at: "2019-09-28 00:34:09", updated_at: "2019-09-28 00:34:09">]>Rails 5.2.3 では
symbol (Rails 6 では falsy になる symbol) は truthy な値として扱われます。
試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try094_falsy_symbol参考情報
- 投稿日:2019-10-11T12:36:14+09:00
クラスメソッドはリファクタリングしにくい
- 投稿日:2019-10-11T10:15:50+09:00
GoogleのOpenIDを使ったログインの実装
概要
OpenIDについて調べたので、実際にGoogleのOpenIDを利用してのログインの実装方法を調べました。
採用技術
- Vue.js
- Ruby on Rails
- GoogleSignIn
実装
クライアント
クライアントはVue.jsで作っていきます。Vue.jsについての説明は省略します。
今回はImplicit Flowでのログインを行いたいので、GoogleSignInを利用します。実装方法はこちらで説明されています。
ただ、今回Vue.jsを利用するので、そのまま利用はできませんでした。
概要は以下のようなものです。index.htmlの<div id="app"></div>
にApp.jsが描画されると思ってください。index.html
<!DOCTYPE html> <html lang="en"> <head> <script src="https://apis.google.com/js/platform.js"></script> <title>app</title> </head> <body> <div id="app"></div> </body> </html>App.js
<template> <div> <div v-if="!signedIn" id="google-signin-button"></div> <a href="#" @click="signOut" v-if="signedIn">Sign out</a> </div> </template> <script> export default { name: 'app', data() { return { signedIn: false } }, mounted() { this.renderSignInButton(); }, methods: { renderSignInButton() { gapi.load("auth2", (signin2) => { gapi.auth2.init({ client_id: 'YOUR_CLIENT_ID.apps.googleusercontent.com' scope: 'profile email', hosted_domain: 'YOUR_DOMAIN' // ドメインを限定したい場合 }); gapi.signin2.render('google-signin-button', { onsuccess: this.onSignIn, }) }); }, onSignIn(googleUser) { this.signedIn = true; }, } } </script>GoogleSignInの実装サンプルでは
class="g-signin2"
としているところにボタンを描画してくれるんだと思います。ただ、App.jsの中身が描画されるのが間に合わないらしく、そのままではGoogleSignInボタンを表示してくれませんでした。なのでここを参考にmountedでボタンを描画してます。API側実装
クライアントからAPIへリクエストするときはAuthorizationヘッダーでIDTokenを渡し、APIはそのIDTokenを検証することでリクエストの認証を行います。
以下では自分のプロフィール情報を取得するAPIを作成します。クライアントからのリクエストはこんな感じ(apiはlocalhost:3000で起動しているものとします。)
var auth2 = gapi.auth2.getAuthInstance(); var idToken = auth2.currentUser.get().getAuthResponse().id_token; fetch("http://localhost:3000/my/profile", { headers: { Authorization: `Bearer ${idToken}` } }).then((res) => { return res.json(); }).then((json)=>{ console.log(json); });API側ではとりあえずapplication_controllerに認証処理を記述します。
認証処理
class ApplicationController < ActionController::API before_action :verify_id_token def verify_id_token return false unless request.headers['Authorization'].present? # ①IDTokenを取り出してデコード id_token = request.headers['Authorization'].gsub(/Bearer /, '') decoded_token = JWT.decode id_token, nil, false # ②Googleから公開鍵情報を取得 res = Faraday.get('https://www.googleapis.com/oauth2/v3/certs') keys = JSON.parse(res.body)['keys'] key = keys.find { |item| item['kid'] == decoded_token[1]['kid'] } # ③公開鍵情報から公開鍵作成 exponential = OpenSSL::BN.new(Base64.urlsafe_decode64(key['e']), 2) modulus = OpenSSL::BN.new(Base64.urlsafe_decode64(key['n']), 2) public_key = OpenSSL::PKey::RSA.new.set_key(modulus, exponential, nil).public_key # ④ruby-jwtでIDTokenを検証 raise JWT::VerificationError if decoded_token[0]['hd'] != 'YOUR_DOMAIN' @id_token = JWT.decode id_token, public_key, true, aud: "YOUR_CLIENT_ID.apps.googleusercontent.com", iss: "accounts.google.com", verify_aud: true, verify_iss: true, algorithm: 'RS256' rescue JWT::DecodeError => exception # ログ出力などなど end def current_user return unless @id_token @current_user ||= User.find_or_create_by(google_user_id: @id_token[0]['sub']) end def authenticate! render status: :forbidden unless current_user.present? end end②Googleから公開鍵情報を取得
IDTokenを検証するための公開鍵を取得します。
公開鍵がどこにあるかというと、こちらで説明されています。以下のURLからOpenIDConnectの情報が取れるみたいです。https://accounts.google.com/.well-known/openid-configurationこの
/.well-known/openid-configuration
ですが、OpenIDConnectの仕様にも記載されているので、他のOpenIDプロバイダーを利用する際もこんなURLで公開されているんだと思います。この情報から、公開鍵は以下のURLにあるとわかります。
https://www.googleapis.com/oauth2/v3/certs④ruby-jwtでIDTokenを検証
基本的にはruby-jwtがいい感じで検証してくれます。
以下の3つは必ず検証するよう記載されていました。
- iss(accounts.google.com)
- aud(プロジェクトID)
- exp
expは特にコード上書いていませんが、ruby-jwtがチェックしてくれるみたいです。
また、ドメインを限定したい場合は、hd
にドメインが記載されているのでこちらもチェックすると良いと思います。アクションに認証をかける
あとは必要なコントローラーで使うだけです。
app/controllers/my/profiles_controller.rb
class My::ProfilesController < ApplicationController before_action :authenticate! def show render json: current_user end end終わりに
上記コードは認証の概要を理解するためにかなり簡略なもので終わらせています。
公開鍵をキャッシュしたり、検証済みのIDTokenをキャッシュしておいたりなど改善点はいっぱいあるとは思います。
ですがとりあえずかなり便利そうだし、わりと簡単に使えるということはわかりました。
- 投稿日:2019-10-11T06:02:10+09:00
Controllerのコールバックメソッド(before/after_action)
目的
Ruby on Railsでアプリを使用する際にコールバックメソッドを使用するが、詳細まで理解する必要があると思ったため備忘録的に残しておく。
コールバックメソッドとは
オブジェクトの特定のタイミングで呼び出されるメソッドのこと。
その中でもよく使われるbefore/after_actionを紹介する。before_action,after_actionとは
Controllerでbefore_actionを定義することで、アクションの前後に処理(フィルター)を差し込むことが可能になります。
一般的に、複数のアクションで共通して必要になる処理などを定義することが多い。before_actionの使い方
Controllerにbefore_actionとして処理したいメソッドを定義します。
※after_actionも使い方は同一です。blogs_controller.rbclass UsersController < ApplicationController before_action :set_blog ・・・ def set_blog @blog = "before_actionの学習中" end endshow.html.erb<h1>Listing Users</h1> <p>before_actionで定義した@blog: <%= @blog.id %></p> <!— ←この行を追加 —> <table> </table> <br><img width="653" alt="スクリーンショット 2019-10-10 22.45.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509302/2ad90bd9-5974-cf20-9eff-eebef1b4de1b.png"> <%= link_to 'New User', new_blog_path %>アプリケーションを起動した画面が以下になります。
上記で定義したset_blogメソッドの@blogの値が表示されていますね。
これによりアクションの前に処理を差し込むことができました。only,excect,if,unlessオプション
railsの他のメソッド同様にオプションを持っています。
class UsersController < ApplicationController before_action :set_blog, only:[:new, edit] end上記のようにonlyオプションを使って書いた場合set_blogはnew,editアクションの前だけで実行されます。
exceptオプションは逆で指定したアクション以外でbefore_actionを実行します。また、ifオプションはラムダを渡すことによって式がtrueの時だけ実行させることができます。
unlessオプションは式がtrue以外の時だけ実行される。class UsersController < ApplicationController # current_user.editable?がtrueの時に before_action :set_blog, if: -> { current_user.editable? } endまとめ
・before/after_actionはControllerの前後に処理を差し込むことができる
・only,except,if,unlessオプションを使うことで条件付きでコールバックを使用できる