20191011のRailsに関する記事は22件です。

複数modelの関連づけに関するまとめ(rails アソシエーション)

今週のアウトプットはrailsでコメント投稿サイトを作っていた最中に苦戦したアソシエーションに関して復習ついでに書いていきたいと思います。

関連づけ(アソシエーション)とは

ユーザーがコメント投稿するようなサイトを例に説明すると、
誰が投稿したコメントなのかをデータとして保存しておく必要があります。

まずmodelでuserとpostcommentのカラムに以下の表のようにカラムを作ります。
現在のテーブル設計だけでは、投稿したユーザのIDはわかっても、そのテーブル内のユーザ名まではわかりません。
そこで、ユーザ情報のモデルと、投稿情報のモデルとを関連付ける必要があります。
user_id(投稿したユーザを識別するID)と、id(投稿コメントを識別するID)が、該当します。
また、1人のユーザが複数の画像を投稿できます。
これをモデルで表現すると、Userモデルで表される1人のユーザに対して、複数個(N個)のPostcommentモデルを関連付けることができます。

User PostComment
id id
name comment
email user_id
password
1:Nの関係 |

これを、1対Nの関係と呼びます。

1:Nの関係性をモデルに実装する(アソシエーションの実行)

UserモデルにPostCommentモデルを関連付ける

まずは、Userモデルに対して、PostCommentモデルが1:Nになるよう関連付けます。

app/models/user.rb
class User < ApplicationRecord
    has_many :post_comment, dependent: :destroy
end

※dependent: :destroyは親(この場合はUser)が消滅した時は子(この場合はpost_comment)も消滅するという意味

PostCommentモデルにUserモデルを関連付ける

次に、PostCommentモデルに対して、Userモデルとの関係性を追加していきます。

app/models/post_comment.rb
class 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
email 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.rb
class User < ApplicationRecord
    has_many :post_comment
    has_many :post_image through: :post_comment
end
app/models/post_comment.rb
class PostComment < ApplicationRecord
    belongs_to :user
    belongs_to :post_image
end
app/models/post_image.rb
class PostImage << ApplicationRecord
    has_many :post_comment
    has_many :user through: :post_comment
end

has_many thoroughを利用することでpost_commentから直接Userモデルにアクセスすることができます。

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

自分用メモ(ログイン・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 :e-mail:

送信されるデータは"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
end

findと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にユーザー渡すのよくわからん

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

Railsチュートリアル 第9章<復習>

第9章の復習メモです。
個人的に重要と思ったことを書きます。

前回と同様、以下3つの視点で書きます。

  • 分かったこと
  • 分からなかったこと
  • 今回はスルーしたこと

分かったこと

永続セッション

前章では、一時cookieを使ったので、ブラウザを閉じると情報が消えてしまった。本章では、ブラウザを閉じても消えない永続cookieを使った。

前章の通りRailsでは、sessionメソッドを使うことで、一時cookieに情報を保存できる。sessionメソッドは、保存する際に情報を暗号化してくれるので、安全性が高い。
一方で、永続cookieの場合は、cookiesメソッドを使う。cookiesメソッドは、自動で暗号化する機能が無いため、安全性が低い。

今回、永続cookieのセキュリティを強化するため、以下の対策を行った。

  • cookieにユーザIDを保存する際、暗号化の処理を行う。
  • ユーザIDに加えて、サーバ上でトークンを発行し、cookieに保存する。

今回、永続セッションを作成した手順は以下の通り。

  1. ランダムな文字列を生成し、これをトークンとする。
  2. ブラウザのcookiesにトークンを保存する。有効期限も設定しておく。
  3. トークンをデータベースに保存する。その際、ハッシュ値に変換する。
  4. ブラウザのcookiesに、暗号化したユーザーIDを保存する。
  5. 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。

分からなかったこと、今回はスルーしたこと

  • 永続セッション実装の詳細

今回は、セッションそのものの仕組みについて、理解することに努めました。コーディングは一通り実装してみて、流れは分かったかな、という感じです。

  • テスト全般

参考

以下を参考にさせていただきました。
https://qiita.com/hot_study_man/items/147f8b767b4135fe6fe4
https://www.masalog.site/entry/2017/08/31/171712

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

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度目のデプロイですが、やはり一筋縄ではいきません。ハマったエラーはメモって次回はハマらないようにしていきます。

終わり

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

【初心者向け】PG二年目の私がコーディングでもっと早くから気をつけたかったこと

はじめに

コードを書く時、「こういう時ってどう書いたらいいの?」「そもそも何が綺麗なコードなの?」と悩んだことはありませんか?
私はあります。
この記事はかの有名な リーダブルコード を読んだ際、私自身が出来ていなかったこと、意識しなければならないと思ったことを纏めたものです。
自分なりに纏めていますので、おかしい記述等もあるかもしれませんが、その際はご指摘ください。

命名規則について

具体的な名前を付ける

  • 誰が見ても、その名前だけで以下の2点が分かるような名前を付ける。
    • 何をするメソッドか
    • 何が格納されている変数か
  • 以下の項目を意識すると具体的な名前になりやすい。
    • 動詞を使うときは、より明確な動詞を選ぶ。汎用的なものはなるべく使わない。(類語を調べ、その中から選ぶといい)
      • 私はこちらの記事を参考にするようにしています。
    • 値の単位を付ける。(時間、サイズなど)
    • 「どんな」を意識する。具体的にいうと形容詞や中身を連想できる名詞を添える。
      • records よりも messages、 ハッシュを格納する変数であれば○○_hash の方が直感的に中身の想像が出来る。

リファクタリングするときに意識すること

ベースとして意識することについて

  • 同じことは何度も書かない。
    • 同じことを何度も書く必要がある場合はメソッドに分ける、変数に格納する
  • 並び順に意味を持たせる。
    • 複数変数が出てくる場合、定数が複数出てくる場合はその並びに意味を持たせる。
    • 例:アルファベット順、画面での並び順、ファイルへの出力順など
  • 行数が多い場合は適度に改行を挟み、見やすくする。
    • 例:処理のまとまり単位で改行、など。

コメントについて

書いてはいけないコメント

  • コードを見れば誰でも分かること
  • メソッド名や変数名について説明するようなコメント(それは付けている名前が悪い)

不具合があるコードに付けるコメント

  • 以下のprefixを付ける
prefix 意味
TODO: あとで直す
FIXME: 既知のバグがあるコード
HACK: あまり綺麗じゃない
XXX: 大きな問題がある

prefixが大文字の場合は大きな問題、小文字の場合は小さな問題という意味合いになります。
TODO(ラテ太郎): みたいな書き方をします。

コメントを書いてもいい場所

  • 読み手が一見で分からない or 疑問を持ちそうなところ
    • なんで?これは何?と思われるようなコードであるならば、コメントの記載が必要です。
  • 間違えて使われる可能性がありそうなところ

簡潔なコメントの書き方

  • そののような指示語は避ける。(自分が思っているものと違う認識をされる可能性がある為、具体的に書くのが良いです。)
  • かどうかという表現は使わない。
    • どうなったらどう、という書き方を心がける
    • ×: Aかどうか ○ : AならばB
  • 情報密度の高い言葉を使う。
    • 自分の伝えたい内容が集約された言葉を選ぶことを意識すればコメントが簡潔になります。
  • 実例を書く

ループ・ロジックの単純化

条件式

  • 条件は肯定が正義。(わかりやすい)
    • ド・モルガンの法則を駆使してなるべく肯定的な条件になるようにする。否定の否定、は使わない。悪。
  • 目立つ条件、重要な条件から書く。
  • 何でもかんでも三項演算子を使わない(わかりにくくなるため)
  • 早期リターンできる時は早期リターンする(早期リターンすることでネストが減る。)

まとめ

やっぱり命名って難しい!!!!!
まずは上記のような、常に付きまとってくる問題と戦いながら、可読性の高いコードを書けるようになりたいと思います。

参考

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

データを削除しようとしたらそのテーブルに主キーがなくて消せないんだけど

冒頭

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つよつよの方へ

もしなにかしら方法をしっていたら教えてください?‍♂️
いや、普通に主キー作ればいいんだけどさ...

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

「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 or select 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はあまり使ったことがなかったので、使える場面では使っていきたいと思います。
また、自分の書くコードは冗長になりがちなので、「これって冗長じゃない?」という気持ちをもってコーディングすることを意識する必要があるなと感じました。
次の回ではもっといいコードを書けるように腕を磨いておきます。

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

Railsチュートリアル 第6章 ユーザーのモデルを作成する - ユーザーを検証する

現状のUserモデルの問題点

name属性とemail属性に、あらゆる文字列を取ることができる

以下は、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">

さらに、email属性の値にも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属性は空であってはならない
  • email属性はメールアドレスのフォーマットに従う書式である
  • email属性は一意の値である必要がある
    • ログイン時のユーザー名として、メールアドレスを用いる予定であるため

検証/バリデーションとは

Railsの文脈において、検証ないしバリデーションという言葉は、「属性値に対して何らかの制約を課すためのActive Recordの機能」を指します。よく使われる制約の一例としては、以下のようなものが挙げられます。

  • 存在性の制約
  • 長さの制約
  • フォーマットの制約
  • 一意性の制約

バリデーション実装を題材とした、テスト駆動開発の基本の習得

バリデーション実装というのは、テスト駆動開発と相性がよい対象といえます。その理由は以下の通りです。

  • 「値に対してルールを適用する」という動作は、テストの成功・失敗を定義しやすいこと
    • 状態に依存しないため、特に困難なくテストを実装できる
  • バリデーションが失敗した場合、結果としてやや複雑な動作が要求されること
    • どのような理由でバリデーションが通らなかったかをユーザーに提示する動作
    • RDBに当該変更を反映しない動作
  • やや複雑な動作であるため、テストなしでは正常動作の立証が困難であること

というわけで、Railsチュートリアルにおいては、バリデーション実装を題材としてテスト駆動開発を行っています。

具体的には、以下のような手法で開発を進めていくということです。

  1. 有効なモデルであれば通るようなテストを書く
  2. 有効なモデルのオブジェクトを作成する
    • この時点ではテストは通るはず
  3. 作成したオブジェクトの属性のうち、1つを有効でない属性に意図的に変更する
  4. バリデーションで失敗するかどうかをテストする
    • この時点でテストが失敗する

テストコードの実装

モデルに対するテストのモックは、rails generate modelコマンドの実行結果として生成されます。例えば、現在題材としているUserモデルに対しては、以下のモックが既に存在します。

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

見ての通り、現時点では単なるモックです。この時点で重要なのは、「Railsにおけるモデルに対するテストクラスは、ActiveSupport::TestCaseというクラスを継承している」という点でしょうか。コントローラに対するテストがActionDispatch::IntegrationTestというクラスを継承していたのとは違いますね。

ここから実際のテストを書いていきましょう。まずは、有効なUserかどうかをテストするコードです。

est/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
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?
=> true

2. 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.rb
  class User < ApplicationRecord
+   validates :name, presence: true
  end

以下の3つのコードは、いずれも同じ意味です。

class User < ApplicationRecord
  validates :name, presence: true
end
class User < ApplicationRecord
  validates(:name, presence: true)
end
class 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

今度こそテストは成功しました。

email属性の存在性に関するテスト

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

+   test "email should be present" do
+     @user.email = "    "
+     assert_not @user.valid?
+   end
  end

ここまでの変更に加え、email属性の有効性に対するテストを追加するためには、まず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.rb
  class 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?
=> false

1.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が:emailである要素を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.rb
  require '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.rb
  class 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. 長すぎるnameemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。

>> 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属性もemail属性も長すぎる」という例です。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

上記、今度は「email属性の長さは適切だが、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属性の長さは適切だが、email属性が長すぎる」という例です。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属性もemail属性も適切な長さである」という例です。valid?trueになりました。

余談…"#{("a".."z").to_a.join}"の意味

"abcdefghijklmnopqrstuvwxyz"という26文字の文字列を得るためのコードです。実際に行われている処理は以下のとおりです。

  1. "a"から始まり、"z"で終わる範囲を生成する
  2. 1.で生成した範囲を配列に変換する
  3. 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
=> 260

2. 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。

>> 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.rb
  class 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.rb
  class 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において、キャリアメールのメールアドレスとして作成することができてしまっていたものです。

スクリーンショット 2019-10-09 7.42.43.png

上記スクリーンショットのとおり、Rubularで試した場合も上述した有効でないメールアドレスの例が有効であると判定されています。

メールアドレスの有効性判定をUserモデルに追加する

上述有効でないメールアドレスの問題はさておき、正規表現によるメールアドレスの有効性判定をUserモデルに実装してみましょう。

app/models/user.rb
  class 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 skips

assertionsの数が11から15に増えました。いいですねぇ。

演習 - フォーマットの検証

1. リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

スクリーンショット 2019-10-09 8.05.26.png

2. 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。

スクリーンショット 2019-10-09 8.06.32.png

foo@bar..comのようにドットが連続した無効なメールアドレスを許容する」というのは、上述スクリーンショットのような挙動をいいます。「@以降で.が連続している」というパターンですね。

このようなメールアドレスを無効と判定できるような実装を追加したい…その第一段階としてのテストの追加です。

test/models/user_test.rb
  class 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.rb
  class 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で使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

スクリーンショット 2019-10-10 7.44.38.png

一意性を検証する

メールアドレスの一意性についての重要注意

「メールアドレスの一意性」という観点において、Railsチュートリアルには重要な指摘事項が2点あります。

  • 実用上、メールアドレスの大文字小文字は区別されない
    • バリデーションも大文字小文字を区別しない形で実装しなければならない
    • 「文字列型の大文字小文字を区別するかしないか」は、RDBMSの種類によって異なるので、その違いを吸収するメカニズムを実装しなければならない
  • Active Recordには、RDBMSレベルでのデータの一意性を保証する仕組みはない
    • ほぼ同時に同内容のPOSTリクエストを受け取った場合など、重複してはならないカラムに重複が発生するおそれがある

「これらを踏まえた上で、相応の対策が必要となる」ということです。…恐れずに行ってみましょう!

重複するメールアドレスを拒否する(Rails側で)

テストを書く

一意性をテストするためには、「実際にレコードをRDBMSに保存する」という操作が必要になります。ここで使うのは、モデルオブジェクトのsaveメソッドです。テストコードに登場するのは初めてですね。

test/models/user_test.rb
  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
+     @user.save
+     assert_not duplicate_user.valid?
+   end
  end

テストは通らない

この時点でテストは通りません。assert_notの時点で、RDBMS上にduplicate_userと同一内容のmail属性値を持つレコードが存在するのに、duplicate_uservalid?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モデルにおけるemailのバリデーションにuniqueness: trueというオプションを追加すればOKです。

app/models/user.rb
  class 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.rb
  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
  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.rb
  class 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 skips

RDBMSレベルでメールアドレスの一意性を保証する

現時点で、Rails側でメールアドレスの重複を排除する仕組みは実装できています。しかし、前述「メールアドレスの一意性についての重要注意」で言及したように、RDBMSレベルでメールアドレスの一意性を保証する仕組みは未だ実装されていません。そのため、現時点では「重複するメールアドレスを持つ複数レコードがRDBMSに登録されてしまう」という事態の発生を排除しきれません。Railsチュートリアルでは、「Submitボタンを素早く2回クリックしてしまった場合」を例として、そうした事態が発生しうることについて言及しています。

というわけで、RDBMS側にもメールアドレスの一意性を保証する仕組みを実装する必要があります。具体的には以下の手順を踏みます。

  1. RDBMS上のemailカラムにインデックスを追加する
  2. 追加したインデックスについて、一意性制約を追加する

変更を加えるのは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テーブルのemailカラムにインデックスを追加している
  • インデックスは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.comEraSErMotOrPhaNTom@ExAMPle.comは同一の文字列と解釈してほしいです。そのため、インデックスにおいて大文字と小文字を区別するRDBMSへの対処が必要となります。

今回は、「データベースに保存される直前に、すべての文字列を小文字に変換する」という方法をとります。例えば、"EraSErMotOrPhaNTom@ExAMPle.com"という文字列が渡されたら、保存直前に"erasermotorphantom@example.com"に変換してしまうわけです。

Active Recordのコールバック(callback)メソッドとは

RailsのActive Recordライブラリでは、オブジェクトのライフサイクル中の特定タイミングに実行されるメソッドを定義することができます。例えば、作成時・保存時・更新時・削除時・検索時・検証時・データベースからの読み込み時などがそのタイミングです。こうした特定タイミングに実行されるメソッドを「コールバック」といいます。

ここで「特定タイミング」と言いました。より厳密には、特定タイミングの「前」「後」というのも指定することができます。例えば、作成前・作成後。保存前・保存後…というような指定も可能です。

コールバックメソッドの実装

繰り返しますと、今回実装するのは、「データベースに保存される直前に、email属性のすべての文字列を小文字に変換する」という処理です。「データベースに保存される直前」に実行されるコールバックは、before_saveという名前で定義されています。

では実装していきましょう。対象ファイルはapp/models/user.rbです。

app/models/user.rb
  class 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"という名前のテストが、設問内容に対応するテストですね。以下の処理を行っています。

  1. 大文字小文字が混ざったメールアドレスの例をmixed_case_emailオブジェクトとして定義する
  2. テストで使うユーザーオブジェクトのEmail属性に、mixed_case_emailを代入する
  3. テストで使うユーザーオブジェクトをRDBMS上に保存する
  4. mixed_case_emailの値を全て小文字にしたものと、改めてRDBMSから読み込んだユーザーオブジェクトのemail属性の値が一致していればテスト成功

1.2. リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトしてredになることを確認してみましょう。

app/models/user.rb
  class 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.rb
  class 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.rb
  class 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

無事完了しました。

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

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#destroy

flash.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/063957

sessionメソッドの使い方

session情報の追加

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id  # ← session情報にユーザIDを追加
  end
end

session情報の削除

deleteメソッドを使う。

session.delete(:user_id)

分からなかったこと、今回はスルーしたこと

  • テスト全般
  • アプリケーションの仕様の詳細
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

中間テーブルが更新されない

解決方法

optional: trueをつける!

参考: http://soccer1356abc.hatenablog.com/entry/2018/09/22/210221

user.rb
  has_many :talent_users
  has_many :talents, through: :talent_users
talent.rb
  has_many :talent_users
  has_many :users, dependent: :destroy, through: :talent_users
talent_users.rb
  belongs_to :user
  belongs_to :talent
  belongs_to :shop, optional: true

user登録時にtalentも紐づけてcreateしたかった
この時shopは関係ないからoptional: trueをつける

ネストさせる

route.rb
    resources :users do
      resource :talent_users, only: [:create]
    end

パラメーターに登録したいタレントのidが入るようにしておけばOK!

users_controller.rb
  def user_params
    params.require(:user).permit(
      :email, :password, :tel ,:sex,
      :first_name, :last_name, :first_name_kana, :last_name_kana, { :talent_ids=> [] })
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初学者]rootメソッド

目的

学習の備忘録と初学者の参考資料として投稿

ルートの設定

ルートへアクセスした場合に特定のアクションを実行させたい時に設定。

設定方法

config/routes.rb
 root to: コントローラー名#アクション名

ブラウザで

 http://localhost:3000/と記入

結果、設定したアクションのviewが表示されます。

まとめ

最初に表示したいトップページがあるならば、ルート設定すればいい。

今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします:bow_tone1:

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

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 ? ?

うまくいったようです。
でも何が原因だったかわかりません。
どなたか教えてくださいませー。

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

[初学者]ルーティングについて

目的

学習の備忘録と初学者の参考資料として投稿

ルーティング

ルーティングは、ブラウザから届いたリクエスト(HTTPメソッド+URL)に対して、コントローラーで定義したアクションを結びつけるルールです。
884f611b6e8d4a5af88d3211daa4bc1b.png

上記は参考例です。

ルーティングの確認

ターミナルで

$ rails routes

あるいは...ブラウザで

http://localhost:3000/rails/info/routes と入力

どちらでも確認出来ます。

HTTPメソッド

HTTPメソッドとは、「クライアントがサーバーにしてほしいことを依頼するための手段」のこと。
主に使うのは『GET』『POST』『PUT』『DELETE』の4つぐらいです。
それぞれの働きは

『GET』  ・・・データを取得するときに利用する。

『POST』  ・・・サーバーにデータを送信する時に利用する。アカウント作成や投稿するなど新規作成で使われる。

『PUT』  ・・・サーバーにデータを送信する時に利用する。既存データの更新などで使われる。

『DELETE』・・・既存データを削除するときに利用する。

任意のアクションを呼び出したい時は

 http://(ホスト名)/コントロール名/アクション名

で呼び出すことが可能です。

まとめ

今回は簡単なさわりだけを書いています。今後さらに深掘りして書いていきます。

今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします:bow_tone1:

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

Rails で CSRF トークンの検証を制御する

特定のコントローラで検証を無効化する

skip_forgery_protection を使います。
これは skip_before_action :verify_authenticity_token のラッパーなので only: :create 等のオプションを指定できます。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  skip_forgery_protection
end

大本のコントローラで指定して継承先のコントローラで重複して使用はできないので、その場合は protect_from_forgery with: :exception 等を明示的に指定します。

app/controllers/article_controller.rb
class ArticleController < ApplicationController
  protect_from_forgery with: :exception, only: :create

  def create
    # ...
  end
end

検証を完全に無効化する

検証そのものを行わないようにします。

config/application.rb
config.action_controller.allow_forgery_protection = false

警告だけを消す

検証は行って WARN のログは出さないようにします。

config/application.rb
config.action_controller.log_warning_on_csrf_failure = false

Rails 5.2 や 6 以降で null_session

rails 5.2 以降はデフォルトで protect_from_forgery with: :exception となるような変更(https://github.com/rails/rails/commit/ec4a836919c021c0a5cf9ebeebb4db5e02104a55)が入り、ApplicationController からは protect_from_forgery の記述が消されました。

ApplicationControllerprotect_from_forgery null: :session としても反映されないので以前のように with: :null_session を使いたい場合は以下のようにします。

config/application.rb
config.action_controller.default_protect_from_forgery = false
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # protect_from_forgery に with オプションを渡さない場合は with: :null_session と同等
  protect_from_forgery
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

graphql-ruby×nuxt.js 画像アップロード

走り書きすみません。綺麗にします。

Rails

carrierwaveuploader/carrierwaveを設定した後のお話。
ModelとUploaderを設定してね。

GraphqlController

jaydenseric/apollo-upload-clientコレを使うと、ActionController::Parametersに入ってくる値が変化する。(リクエストを変更している。)

~/app/controllers/graphql_controller.rb
class 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
end

ImageType

~/app/graphql/scalar_types/image_type.rb
module 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.rb
module 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.rb
module 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
end

Nuxt

Apollo設定

以下のコードは使いません。

~/apollo/client-configs/default.js
import { 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.js
import { 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.gql
mutation UpdateUserProfileImage($file: ImageType!) {
  updateUserProfileImage(profileImage: $file) {
    results
  } 
}

残った疑問

  • ↓調べる。
ActionController::Parameters
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】世界のナベアツのあのギャグで遊んでみよう

目次

  • 1. この記事の狙い
  • 2. ターゲット層
  • 3. 実行環境
  • 4. 世界のナベアツについて
  • 5. Rubyコード
    • 5-1. 期待する挙動
    • 5-2. Comedianクラス実装
    • 5-3. 実際に動かしてみよう

1. この記事の狙い

世の中には伊藤淳一さんチェリー本(私も大変おせわになりました:bow: )をはじめとした入門書、オンライン上でも無料の教材が揃っているので、インプットで不便することはない。
しかし、アウトプットをしないと自分の力に変換出来ないし忘れてしまう。

なるべくなら肩の力を抜いて遊びながら基礎をおさらい出来たらあまり疲れないで済むで良い。
現実世界にはそんな遊びに適した材料が転がってるので、自分のリフレッシュも兼ねて思いつく限りで記事に書き起こしてみようというのが狙い。

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="吉本興業">

違う芸人ではギャグは発動しない。

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

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 : 数字にマッチ

  • \ddは数字を表す。数字と表す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}/)
=> nil

i : 大文字・小文字を区別しない検索

  • iオプションを加えることで大文字・小文字を区別しないで検索する。
  • iオプションをつけずに[a-z]と小文字で記述すると大文字にマッチしなくなる。
  • 大文字・小文字の区別
ターミナル
irb(main):003:0> 'Cat'.match(/cat/)
=> nil

irb(main):004:0> 'Cat'.match(/cat/i)
=> #<MatchData "Cat">

実践的な使用例

irb
pass = '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 回以上の繰り返しにマッチ

まだまだ奥深い正規表現ですが以上のことだけは最低限おさえて置きたいと思います。。。

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

Railsで既存モデルをポリモーフィック化したら大変だった話

前提

社内でやろうとしてたことそのまま書けないので、
例えを探しましたが・・・わかりにくいかもですw
ポリモーフィックについては、こちらの記事がわかりやすかったです!感謝感激。

モデルは下記とします。
モデル図 *()の中がモデル名
普通自動車(Car)-<部品(Part)>-人(Owner)

Car
has_many :parts
has_many :owners, through: :parts

Part
belongs_to :cars
belongs_to :owners

Owner
has_many :parts
has_many :cars, through: :parts

目的は、
「ある人が所持している、ある普通自動車に搭載されている部品達をDBで管理できる」
とします。

もうちょっと具体的にいうと、
「AさんのIDから次郎号という自動車を特定できて、
    かつ次郎号に搭載されている部品(前輪・後輪とか)も把握できる」

みたいな感じです。

異なるモデルも同一のモデルで管理したい

上記モデルをポリモーフィック可したらどうなるかというと
モデルはこんな感じになります。*()の中がモデル名
モデル図
なんかの乗り物-<部品(Part)>-人(Owner)

なんか乗り物(ここではCarとShipとしておきます)
has_many :parts
has_many :owners, as: :vehicle, through: :parts

Part
belongs_to :vehicles, polymorphic: true
belongs_to :owners

Owner
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モデルが入ろうが吸収してくれます。

参考

【初心者向け】Railsのポリモーフィック関連付けを理解しよう

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

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 モデルを作る

titlepublished の2つの属性を持つ Book モデルを作ります。

$ bin/rails g model Book title published:boolean

seed データを作る

seed データを作ります。

db/seeds.rb
Book.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

参考情報

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

クラスメソッドはリファクタリングしにくい

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

GoogleのOpenIDを使ったログインの実装

概要

OpenIDについて調べたので、実際にGoogleのOpenIDを利用してのログインの実装方法を調べました。

採用技術

実装

クライアント

クライアントは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をキャッシュしておいたりなど改善点はいっぱいあるとは思います。
ですがとりあえずかなり便利そうだし、わりと簡単に使えるということはわかりました。

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

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.rb
class UsersController < ApplicationController
  before_action :set_blog
  ・・・
  def set_blog
    @blog = "before_actionの学習中"
  end
end
show.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の値が表示されていますね。
これによりアクションの前に処理を差し込むことができました。

スクリーンショット 2019-10-10 23.04.29.png

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オプションを使うことで条件付きでコールバックを使用できる

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