railsチュートリアル学び直しアウトプットです。
記憶の定着を図るためのもので、超個人向けとなります。
また、思い出せるだけとりあえず記載する、というスタイルなので本家と順番が違うこともあります。悪しからず。
Railsチュートリアル 第7章
Usersリソース
以前の章でも登場した、リソースについて。
routes.rbに resources users
と記載することで、usersリソースを利用できるようになる。
具体的には、以下。
url | httpメソッド | 対応controllerメソッド | 対応view | ヘルパー |
---|---|---|---|---|
/users | get | #index | index.html.erb | users_path |
/users | post | #create | - | users_path |
/users/new | get | #new | new.html.erb | new_user_path |
/users/#{id}/edit | get | #edit | edit.html.erb | edit_user_path |
/users/#{id} | get | #show | show.html.erb | user_path(user) |
/users/#{id} | patch | #update | - | user_path(user) |
/users/#{id} | delete | #destroy | - | user_path(user) |
ここで少し不思議なのが、postがusers_pathと複数系であること。(作成しているのはひとりなのに、という違和感)
予想は、user一人を作成しているのではなく、users全体に追加しているというイメージ。
-> 大体あってそう。
debug
本チュートリアルでは、デバッグ向けに2つのツールが紹介されている。
1. debug ビューヘルパー
railsに標準搭載されているヘルパーで、viewにそのまま追加できる。
本チュートリアルでは以下のように記載した。
<%= debug(params) if Rails.env.development? %>
envがdevelopmentの場合のみ、ということであるが、特筆すべきはRailsというオブジェクトがある、ということと、development?のようなメソッドとして環境確認ができることかなと思う。
(env == "development"
のような記述ではなく、?というrubyメソッドっぽくかける)
余談だが、Rails6以前ではyaml形式だったこともちゃんと記載されている。
私が最初に学習した時はそっちだったので、変わっているんだなぁとふと思った。
debugger メソッド
そして、多少混同しがちなのは続けて紹介されているdebuggerメソッドである。
これは、debug
というgemをインストールしているため、rails標準ではなくそちらのメソッドを利用していることに注意。
これは、controllerなにdebugger
を差し込むことにより、受け取っているパラメーターなどをcliで確認できる + 対話型による確認(@user.name
など)が可能になる。
何か問題が起こった時に利用する、またはちゃんとパラメータきてるよね、という確認をするなど。
要追記
新規登録formを作成する
form_withヘルパー
erbでは、form_withという便利なヘルパーを利用することにより、作成したモデル(今回はUser)のattributeに沿ったformを作成することができる。
<%= form_with(model: @user) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :email %>
<%= f.email_field :name %>
<%= f.label :password %>
<%= f.passwrod_field :password %>
<%= f.label :password_confirmation, 'Confirmation' %>
<%= f.passwrod_field :password_confirmation %>
<%= f.sumbit 'Create' %>
<% end %>
引数には、modelとして@userを渡している。これは、controllerのnewメソッドで作成した@userを渡している。form_withヘルパーはそれを受け取り、それぞれのフィールドとuserのattributeを関連づけ、paramsに入れることができるようなformを生成してくれる。
ここで、email_field
やpassword_field
といったフィールドがあるが、
- email_field: モバイルで表示すると、専用のキーボードが表示される
- password_field: 入力した値が黒丸で表示される
といった利点がある。
また、確認しておきたい点として、実際に生成されているhtmlのname属性である。
下記の通り、<model>[属性]
という形でinputを生成してくれていることがわかる。これを利用して、railsはそれぞれのattributeごとに値を設定したparamsをハッシュ形式で作成してくれるらしい。
ほか特筆すべき点として、hiddenで謎のinputが生成されていることである。
これは、csrf(Cross-Site Request Forgery)対策のためのトークンが含まれている。
<要確認> そもそもcsrfとな何か、csrf対策のメタタグとの関連、どうやって対策をしているのか。について。
paramsに格納される形式
先ほどの通り、inputのname属性でモデルそれぞれのattributeに対応した値をparamsに格納してくれる。
今回の場合は大体以下のようになる。
見ての通り、二重のハッシュ + キーはシンボルとなっていることがわかる。
※paramsは単なるハッシュではないようなのですが、同じように取り出せるので以下のように記載しています。
params ... { users: { name: "name", email: "email",
password: "password",
password_confirmation: "confirmation" } }
createメソッド
newページを作り、formの形も作成したのでsubmitした際のメソッド(createメソッド)をusers_controllerに定義していく。
手順は以下だった。
- パラメータを絞り、任意の属性をシャットアウトする
- 失敗した時の動作およびテストを実装
- 成功時の動作およびテストを実装
まず、一番最初のcreateメソッドは大体以下のような感じ。
(ちょっと見直した。)
def create
@user = User.new(params[:user]) # これは未完成
if @user.save
# 成功時の処理
else
render 'users/new'
end
end
1. パラメータの制限
記載の通り、@user = User.new(params[:user])
というのは未完成である。
先ほどの通り、二重のハッシュとして受け取れるので値の受け取り自体はできているが、これだと任意の属性を含めることができてしまう。
※少し抽象的だが、curlなど直接httpリクエストをする際に含めてしまえば、paramsとして受け取ってしまえたりする。
それがどうしてダメなのかというと、もしUserモデルに admin: boolean
というような権限属性があったとして、それを任意の値で設定することができてしまうのである。
よって、受け取れるparamsには制限をかけたい。そこで、以下のように修正する。
def create
@user = User.new(user_params)
if @user.save
# 成功時の処理
else
render 'new' # 少し修正する
end
end
# ここの記載方法は忘れており、見直した。
private
def user_params
params.require(:user).permit(:name, :email,
:password,
:password_confirmation)
end
インデントが深いのは、privateと分かりやすくするための慣習だそう。
requireで必須項目、その中身はpermitで許可するものを列挙という形。
これで、指定された属性のみを受け付けるようになった。
が、ここでどうしても看過できない記載がある。
どうしてparamsというものをこちらで定義せず、また引数などにも取らずコントローラー内では利用できているのだろう?という点だ。
急に降ってきたparamsにどうしてかパラメータが入っていて、しかもメソッドまで呼び出せている。
とても便利だが、どうも不安にもなる記述である。
すごく簡単に調べたところ、paramsは ActionController::Base
で定義されているメソッドだそう。だから、それを継承しているUsersController classでも利用でき。実際はself.paramsと呼び出している(インスタンス自体から呼び出している)ということらしい。
よって、魔法のようにそれを利用できているように見えるとのこと。
確かに、クラス内で定義したメソッドはそのまま呼び出せるな、と思うと割としっくりくる気がする。
2. 失敗時の処理
次に、失敗時の処理 + テストである。
コントローラーは以下のように修正する。
def create
@user = User.new(user_params)
if @user.save
# 成功時の処理
else
render 'new', status: :unprocessable_entity # このstatusは忘れていた
end
end
statusというのはhttpステータスで、今回はunprocessable_entityを利用した。
entityとの不整合、といったところと思われ、ステータスコードは422だそう。
さてここで、integration_test(統合テスト)を追加していく。
実際の動作に沿ってテストを書くことができるので、毎回表示を自分で確認して、、、とせずとも良くなる。
まず、テストをgeneratorで作成する。
rails g integration_test users_signup # このusers_signupは任意の名前
これをすると、test/integrationにuser_signup_test.rbというファイルが生成される。
そこに、失敗時のテストを書いていく。
ざっくり以下のような形(<見直しあり>)
test "invalid params (のようなテスト名)" do
get signup_path
assert_no_difference 'User.count' do
post users_path, params: { user: { name: "",
email: "invalid@test..com",
password: "pass"
password_confirmation: "pass" } }
end
assert_response :unprocessable_entity
assert_template 'users/new'
end
postのみでも問題ないが、よりリアルな動きを想定したいため signup_pathから始めている。
特筆すべきは、assert_no_difference
メソッドと、それに渡すUser.count
という文字列だろうか。
assert_no_differenceは、渡したものがブロックの実行前と後で変化のないことを確認するものである。そして、それに文字列でUser.count
を渡しているのに驚いた。
確かに、Userモデルのインポートなどはしていないため直接かけない気もするが、文字列で渡すことで実行してくれるのは面白い。
また、既出かもしれないが、User自体に.countというメソッドが呼び出せるのもまたRubyっぽいところであろう。便利。
そして、responseでは先ほど渡したものとなっているか確認し、newがレンダリングされたままかを確認して終了といったところ。
なのだが、ここでもう一手間加える。
現状だと、どうして登録に失敗したのかわからない。そのため、それをユーザーにわかりやすく表示する。
ここは、パーシャルの作成等をしていくが、そこでのerrorの取り出し方がおさらいである。
そこだけ抜き出すと以下みたいな感じ。
いかにもrubyっぽい。
これを、newページに差し込めば、errorがある場合のみその内容を表示してくれる。
<% if @user.errors.any? %>
<% @user.error.full_messages.each do |msg| %>
msg
<% end %>
<% end %>
※本当はもっとタグやclassなどあります。
そして、custom.scssにerrorのstyleを書いて完了でした。
3.成功時の挙動
さて、失敗時の挙動はどうやら良さそうなので、最後に成功時の処理を書いていく。
コントローラーを以下のように修正する。
def create
@user = User.new(user_params)
if @user.save
flass[:success] = "flashメッセージ"
redirect_to @user
else
render 'new', status: :unprocessable_entity
end
end
flashは後々追加したものだが、今回は最終系まで書きました。
redirect
まず、flashメッセージは置いておき、redirect_to @user という、また魔法のような、呪文のような記述になっている。
これは、redirect_to user_url(@user)
という記述と全く同じ意味、だそう。
チュートリアルでも記載されていた以下の記事が本当に丁寧に解説してくださっていた。
あと、どうしてredirectするのか?というところは、restの慣習的に、となっていた気がする(?)これは要確認である。
今回はuserの詳細ページに遷移しているが、必要であれば一覧ページなど、ユースケースによってredirect場所は変えて良い。
余談だが、redirectすると嬉しいのはdbのデータなどがちゃんと最新のものになる、ということも一つなのではと思う。
render系だと、再度getするというよりは、そのテンプレートを表示するだけという意味になる気がしており、なんらかのキャッシュデータ等が残ってしまっているといけてない。
というのも、現在react + nestjsという環境で、frontにてtanstackQueryを利用してクエリキー・キャッシュなどを管理している。
その際、refetch(再取得)とinvalidate(キャッシュの無効化)という二つのメソッドがあり、どのように利用すべきかわからなかったため調べたところ、
- refetch
- 即座に実行される
- 該当のキャッシュのみ変更される
- 一覧ページで、詳細はコラプス等の中にあり、開いた際にfetchするなど、一部情報をgetするなどに適している
- invalidate
- その名の通り、キャッシュデータを無効化する
- create, update, deleteなど、dbに変更がある = キャッシュは古くなるので、invalidateが妥当
- ほか、階層構造の下にあるクエリキーのキャッシュも同時にクリアしてくれるため、他のコンポーネントなどにも影響が及んでくれる
という感じであった。言われてみるとその通りだが、ぱっと見どちらも再取得しているだけ、、?なんて思うのもまあ普通かもと思いたい。
flash
さて、ここでもどこから登場したのやらというflashオブジェクトに、flash[:success] = "メッセージ"
と何やらメッセージを入れている。
これも先ほどのparamsと同じく、ActionController::Base
クラスに定義されているインスタンスメソッドらしい。ActionController::Base
を一回みてみる必要があるかも。
というわけで、特に宣言せずともflashメソッドはcontroller内で自由に呼び出せるということだ。
また、flash[:success]
という記述からわかる通りこれもハッシュのように利用できる。:success
というシンボルキーにメッセージを入れているが、ここはsuccessでなくても、任意の値で大丈夫である。
ただ、bootstrapと組み合わせて利用する際、success, info, danger, cautionなどでスタイルが用意されているので、慣習的にsuccessを利用することが多いらしい。
本チュートリアルでもbootstrapの実装を利用したいために、successを利用している。
そして、flashの特徴は「そのリクエストが終了したら削除される」というところであり、一時的にメッセージを表示するのに適している。
また、flashメッセージはUserの作成にとどまらず広く利用するため、application.html.erb
にそれを表示する記述を追加した。
大体下記のような感じだった気がする。
<% if flash.any? %>
<% flash.each do |type, msg| %>
<%= content_tag (:div, msg, class: alert alert-#{type}) %>
<% end %>
<% end %>
content_tagの箇所は、演習でdivタグから修正したものになります。
any?で、もし中身があればそれぞれを表示するというもの。非常にシンプル。
で、content_tagというものに目を向けると、最初に利用したいhtmlタグをシンボルで渡し、その中身を第二引数、最後にclassという感じ。
reactなどで考えると、(雑ですが)以下のような雰囲気という感じがする。
msgでなくてchildren要素として渡すかもしれないですが。
type Props = {
msg: string
class: string
}
// ~色々略~
<div class={class}>
msg
</div>
というわけで、無事flassメッセージの表示も完了し、userのcreateメソッドが完了しました。
最後に、テストを書いていきます。
先ほど生成したusers_signupテストに追記します。
test `valid params のようなテスト名` do
get signup_path
assert_difference 'User.count', 1 do
post users_path params = { user: { name: "valid",
email: "valid@test.com",
password: "password",
password_confirmation: "password"} }
end
follow_redirect!
assert_template 'users/show'
assert_not flash.empty? # 演習で追記した
end
さて、先ほどと流れはほとんど同じであるが、assert_difference
の箇所とfollow_redirect!
が違う箇所になる。
assert_differenceは、失敗時のno_differenceと違い実行後に「結果が変わること」を確認したい時に利用するメソッドである。
そして、幾つ差が出るのかを数字で渡すことで、単に違うかどうか、ではなく変更された数も確認できる。
また、follow_redirect!
というのは、controllerでリダイレクト処理を書いた際にこれを記載しないとリクエストをしてくれないようだ。<要確認>
そして、ユーザー詳細ページへ移動しflashがあることを確認している。といったところ。
デプロイ
最後に、この章の鬼門であるデプロイだ。
ここまではローカルで動くのでそこまで問題にならないが、やはりデプロイとなると外部ツールを利用するため難しくなる気がする。
ここでやりたいことは以下2点。
- TLS(旧SSl)通信を利用する形に変える
- 本番環境で利用するRDBをmySqlからPostgreSQLに変える
1について、私はrailsチュートリアルで用意されているテンプレートを利用せずやってみているので、もともとTLS利用になっていた。
変更箇所は、environment/producion.rb
の config.force_ssl = true
のコメントアウトをはずす。
2. 本番環境で利用するRDBをmySqlからPostgreSQLに変える
さて、こちらが少し問題だった。二つ、チュートリアルどおりではうまくいかない部分があった。
- pg gemのインストールが失敗する
- rails db:migrateが通らなかった
まあ、dockerFileなども含め修正してくれていると思うが、そうはなっていないので自分で変える必要があるといったところ。それぞれの解決策について。
1. pg gemのインストールが失敗する
チュートリアルに記載されている手順に沿って、renderでのpostgresql作成・アプリケーションへDBのurlを登録等を済ませたが、どうやらdockerのビルドで落ちているらしかった。
こちらは、デフォルトのdockerファイルでインストールしているものに、pg gem を installするために必要なものが不足していたため、エラーとなっていた。
Dockerファイルを以下のように修正したところ、問題なくビルドが完了し、本番環境へのデプロイができた。
# Install packages needed to build gems
RUN apt-get update -qq && \
- apt-get install --no-install-recommends -y build-essential git libvips pkg-config
# libpq-devを追加
+ apt-get install --no-install-recommends -y build-essential git libvips pkg-config libpq-dev
# Install packages needed for deployment
RUN apt-get update -qq && \
- apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \
# libpq5を追加
+ apt-get install --no-install-recommends -y curl libsqlite3-0 libvips libpq5 && \
libpqというのは、C言語にて作成されたライブラリで、postgresql DB とアプリケーション(サーバー)のやり取りを取り持ってくれるインターフェースだということだった。
よって、pg gemをインストールする前にこれらがないとダメだった、ということだ。
では、build gemではlibpq-dev
を、デプロイにはlibpq5
をそれぞれインストールしているが、どうして違うものをそれぞれインストールしているのか?
AIに聞いてみたことを、自分の理解の範囲で記載します。
pg gemはその一部で、libpqを利用したC言語で作成さている。
よって、それをコンパイルするためにlibpq-dev
が必要となる。
また、libpq
は実行時ライブラリであり、実際にアプリケーションがDBアクセス等を実行する際にこれを利用している。5はバージョン。
みたいな感じ。ざっくりいうと、pg は外部で用意さているlibpqを利用し、postgresqlの操作を可能にしてくれる。
よって、pgはlibpq-dev
やlibpq5
に依存しているため、それらがないとインストールができないようになっているのである。なるほど。
2. rails db:migrateが通らなかった
次は、migrationエラーが出るようになった。
以下の修正で動作するようになった。
- # Run db migration
- RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails db:migrate
#!/bin/bash -e
# If running the rails server then create or migrate existing database
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
./bin/rails db:prepare
fi
+ bundle exec rails db:migrate
exec "${@}"
修正をそのまま言えば、Dockerfileに記載されていたmigrationコマンドを削除し、bin/docker-entrypoint
にmigrateを移動した。
※Dockerfileの記載は、前回の章で自分で追記していたものです。
どうしてこれでうまくいくのか?
今回のデプロイでは、本番dbをpostgresqlに変更し、dbのアクセス先(url)は環境変数としてコンソールで設定した。
そして、コンテナの環境変数が利用できるのは起動したタイミングであり、ビルド時にはまだ利用できない。※Dockerfileにも変数は記載もできるが、セキュリティなどがあるのでできない。
よって、DBのurlにアクセスできず、マイグレーション反映は不可能であったということのよう。
そこで、docker-entrypoint
にその実行を移すことで解消した。
ここに記載されたコマンドは、dockerfileのCMDが実行される前に実行してくれる。
よって、コンテナ起動 -> entrypoint実行(migration含む)-> CMDでアプリケーション起動(Pumaサーバー起動)という、素直な実行ができていたということだった。
なるほど。
最後に
ひとまず、思い出せること(忘れていたこともあったけど)をざっと書いてみた。
思っているよりも1章1章がボリューミーだということに気付かされるなぁと。
「この章から難しくなるから、2回やったりするのもあり」みたいな記載があったと思うが、確かにそう思うのと、最初に学習した時はわかった気がしていただけの部分も多かったなと思う。
ひとまず完了!また復習と、次の章をやっていく