読者です 読者をやめる 読者になる 読者になる

【Rails】Scaffoldされたコントローラを読んでみる

こんにちは、MUGENUPの倉成です。 僕はRailsを使い始めてあと少しで丸2年になるのですが、初めの頃は覚えることがたくさんあって、とても大変だった記憶があります。

今回はそんな中でも、Railsで初めてWebプログラミングをする人向けに、コントローラのアクションであるnew,create,showについて解説してみたいと思います。

この記事ではソースコードは説明のため、実際にscaffoldされたコードを抜粋、もしくは省略されているコードを挿入して取り上げます。 なお、使用するコードは

rails g scaffold user name:string email:string postal_code:string address:string

で生成されたコードを元にしており、モデル・ビュー・コントローラはこれを使うことにします。

それではnewから見て行きましょう。

def new : 新規登録フォームを作成

def new
  # (1) Userオブジェクトを生成
  @user = User.new
  # (2) フォームを生成(Rails4では省略されている)
  render :new
end

このアクションでは(1)@userを作成し、(2)では(1)で生成したオブジェクト(@user)とテンプレート(new.html.erb)を使ってhtmlを生成しています。

図にすると以下の様なイメージです。

f:id:mgnup:20140828233234p:plain

今回は@userの全ての属性の初期状態がnilなので、空のinputフォームが生成されています。

さて、次はcreateアクションを見て行きましょう。

def create : 登録 or 差戻し

scaffoldされたcontrollerは複雑で悩むポイントはたくさんあると思うのですが、特に難しいのが行数の多いcreateだと思います。 今回は簡単のためformat.htmlformat.jsonそしてnoticeは一旦脇に置いておいて

def create
  # (1) フォームから渡されたパラメータを使用し、Userオブジェクトを生成
  @user = User.new(user_params)
  # (2) DBへ保存(を試みる)
  if @user.save
    # (3) 新規登録されたユーザーページヘリダイレクト
    redirect_to @user
  else
    # (3') @userを使って、もう一度フォームを表示
    render :new
  end
end

のコードを追いましょう。

(1) @userを生成

@userならnewするときに作ったよ」と思うかもしれません。

しかし、ブラウザからリクエストとして送られてくるのは、フォームに入力されたテキストデータのみのため、改めて@userを生成する必要があります。 同じコントローラでも「一回ごとのリクエストでインスタンス変数が共有化されない」というのはWebアプリが初めてだと取っ付きづらく感じる原因かもしれないですね。

f:id:mgnup:20140828234344p:plain

create内で、フォームからの情報を使って、@userを作ります。

f:id:mgnup:20140828234945p:plain

(2) DBへ保存

(1)で生成した@userの値を検証し、DBへの保存が成功した場合には、(3)redirect_to、失敗した時には(3')renderをします。 saveが失敗するケースとしては「ユーザー名が空白です」「郵便番号は7桁で入力して下さい」のように、バリデーションに引っかかることが多いかと思います。

(3)登録に成功: 登録内容を表示する

ユーザーの登録に成功したら、そのユーザーの登録内容を表示します。 ここでは登録に成功した@userのページを表示するため

redirect_to @user
# 以下と同じ意味
# redirect_to user_path(@user)
# redirect_to /users/#{@user.id}

としています。

しかしnewにあったrenderと違い、redirect_toはテンプレートを使用してHTMLを生成するわけではありませんredirect_toはブラウザに「localhost:3000/users/1にアクセスしなおしてね」と教えるだけです(urlはドメインlocalhost:3000@user.id=1の場合)。

ブラウザはサーバーからのredirectの指示に従ってlocalhost:3000/users/1にアクセスすることで、詳細ページが表示されることになります。

f:id:mgnup:20140829012717p:plain

renderじゃないのはなぜ?

さて、ここでredirect_toを使用していて、render :showとしないのはなぜでしょう? 上の図でも真ん中の2本の矢印は不要に思えますし、create内で@userが既に存在しているので、再度DBから読み直すのは無駄な気がします。

実は、saveに成功時にrender :showとしてしまうと、ページをリロードするたび、新規ユーザー登録処理が発生してしまうという問題があります。 Railsの視点から説明すると、ブラウザをリロードした時には、最後にrenderを呼び出したアクション、今回の場合はcreateアクションが呼ばれてしまうことになります。*1

下はcreateアクションでrender :showをした後、ブラウザをリロードさせた図です。多重登録を防ぐため「フォーム再送信の確認」ダイアログが表示されているのが分かるかと思います。

f:id:mgnup:20140829004303p:plain

今回はscaffoldのコードに習いshowにリダイレクトしましたが、もちろん一覧ページであるindexに飛んでもいいですし、ホームのページ(localhost:3000/)に飛ぶことも多いと思います。 DBの更新が発生した後には、ページを表示するだけのアクションにリダイレクトするということを覚えておきましょう。

また、このpost → redirect → getの一連の流れはその頭文字を取ってPRGパターンと呼ばれているようです。この記事の説明でしっくりこないなぁという方は、ぜひPRGパターンで検索してみてください。

(3')登録に失敗: もう一度入力フォームを表示する

入力内容に不備があった場合、もう一度フォームを表示して、再度情報を入力してもらうため、フォームのテンプレートであるnew.html.erbrenderします。createアクションでrender :newをすると、保存に失敗した@userインスタンス変数をそのまま使えるため不正な項目のみを再度入力してもらうことが出来ます。

f:id:mgnup:20140828235346p:plain

redirect_toじゃないのはなぜ?

一方、再度フォーム画面を表示さるためにredirect_to "users/new"を使ってしまうとフォームは表示させることが出来るのですが、ユーザーがこれまでに入力した内容が全て消え、再度全ての項目を入力してもらわなければなりません。

newの処理を思い出すと、@userの生成時に@user = User.newとしているので、白紙のフォームが表示されてしまうのは納得できるかと思います。

ユーザー登録時に「一箇所ミスがあるくらいで、全ての内容を再入力させるなー!」という経験は一度くらいあるはずですが、このようなシステムを作らないためにはredirect_toではなくrenderを使う必要があるのです。

def show : 登録情報を表示

ユーザーの情報を表示するのがshowの役割です。

createからredirectしてくる場合でも、createで使用した@userは使用できないため、改めてDBからデータを取得し、@userを生成する必要があります。

  def show
    # Rails4ではbefore_actionで呼ばれる
    # (1) params[:id]のユーザーを取得
    @user = User.find(params[:id])
    # (2)Viewを生成(Rails4では省略されている)
    render :show
  end

f:id:mgnup:20140829011625p:plain

@userを作って、View(show.html.erb)のhtmlを生成するということで、フォームなのかテキストなのかという違いがあれど、図はnewと似たものになります。

最後に

さて、今回は僕がRails初心者だった頃に「renderredirect_toは何が違うの?」とか「ここの@userって、こっちのアクションで使えないの?」とか、よく混乱していた部分を中心にRailsのコントローラを解説してみました。 この記事を読んで少しでも理解が深まれば嬉しいです。

この記事はDavid Griffiths著『Head First Rails』の3章を参考にさせていただきました。 この本はRails2系が対象と、やや古くRails3/4では、写経したコードが動かないことが多々あるのですが、Railsの仕組みを知る上ではかなり良書だと思います。 リファレンス的には使用できず、回り道に感じるHead First Railsですが、Railsで初めてWebプログラミングをするという方はこの本から始めてみるとよいかもしれません。

*1:HTTP的な視点で見ると、localhost:3000/usersに対し、再度POSTが発行されている状態です