Rails7 モデルとデータベース

今回はRailsにおけるデータの作成と保存、そして変更について説明します。 そのベースになるモデルとデータベースの話から始め、appendとchangeの動作について詳しく説明します。

MVC

MVCはモデル、ビュー、コントローラを指すことばで、Railsの構成を指しています。

  • M(Model): モデルは保存対象のデータをオブジェクトにしたもの。 データベースに保存する
  • V(View): クライアントに送るHTML文書を作成する
  • C(Controller): モデルやビューの働きを調整しコントロールする

ビューについては前回の記事で扱ったので、イメージはつかめると思います。 今回はモデルを定義し、さらに全体を調整するコントローラも作成します。

モデルとデータベース

WordBookでは保存するデータは「英単語と日本語訳」のペアです。 ブラウザ画面から単語をRails側に送信すると、そのモデルが作成されデータベースに登録されます。 モデルはデータベースのレコードに1対1に対応するものです。 モデルはデータベースの1レコードに対応するものなので、単数形でクラス定義をします。 「class Word」というわけです。 コントローラが複数形「class Words」だったのと比べてみてください。

Railsのgenerateコマンドでモデルを作成できます。 引数にデータベースのフィールドを付けておきます。 今回は

  • 英単語=>フィールド名en、タイプは文字列(string)
  • 日本語訳=>フィールド名jp、タイプは文字列(string)

とします。

$ bundle exec rails generate model Word en:string jp:string 
      invoke  active_record
      create    db/migrate/20221020235423_create_words.rb
      create    app/models/word.rb
      invoke    test_unit
      create      test/models/word_test.rb
      create      test/fixtures/words.yml

表示されたパスのうち、最初の2つがモデルを扱う上で重要なファイルです。 1つ目のファイルは後ほどdb:migrateのところで説明します。 2つ目のファイルのapp/models/word.rbはモデルの設定をするファイルです。 このファイルにはWordクラスの定義が書かれており、その中身は空になっています。 ですが、このクラスはApplicationRecordクラスのサブクラスとして定義されていますので、スーパークラスの様々なメソッドを使うことができます。

ファイルに以下のようにvalidatesメソッドを追加します。

class Word < ApplicationRecord
  validates :en, presence: true, format: {with: /[a-zA-Z]+/}, uniqueness: true
  validates :jp, presence: true
end

validatesはデータをチェックするメソッドです。 データベースに書き込む直前にこのチェックが働き、チェックを通らない場合は書き込みをしません。 また、エラーの内容がモデルに書き込まれます。

  • enはデータが存在しなければデータベースに保存できない(空欄にはできない)
  • enは英文字の文字列でなければならない(formatのwithには正規表現を使える)
  • enはユニークである(すでに同名のデータが存在するならば、データベースに保存できない)。 英語には同じスペルで異なる単語が存在するのだが、この単語帳ではそれらを別のレコードとして登録できない。 そのため1つのレコードに日本語訳を2つ書くことで問題を解決する。 これは誤って同一の単語を二重に登録するのを防ぐための措置
  • jpはデータが存在しなければデータベースに保存できない(空欄にはできない)

db/migrate/20221020235423_create_words.rbは「データベースにアクセスするための設定」をするファイルです。 なお、RailsはデフォルトでSQlite3をデータベースに使います。

class CreateWords < ActiveRecord::Migration[7.0]
  def change
    create_table :words do |t|
      t.string :en
      t.string :jp

      t.timestamps
    end
  end
end

CreateWordsクラスがActiveRecord::Migration[7.0]のサブクラスとして定義されています。 その中でchangeメソッドが定義されています。 create_tableメソッドはwordsという名前のデータベース・テーブルを作るものです。 そのテーブルには、文字列フィールドの「en」と「jp」、そしてタイムスタンプ(作成日時と更新日時)のフィールドが作られます。 このメソッドのブロックには現れませんが、idフィールドも自動的に作られ、1、2、3・・・という整数がレコードに割り振られます。

このファイルを用いてデータベース・テーブルを作ります。 db:migrateコマンドをrailsで実行します。

$ bundle exec rails db:migrate
== 20221020235423 CreateWords: migrating ======================================
-- create_table(:words)
   -> 0.0019s
== 20221020235423 CreateWords: migrated (0.0020s) =============================

コマンド名が「migrate」となっていますが、これは英語で「移住」という意味を表します。 データベースでマイグレート(動詞)、あるいはマイグレーション(名詞)というのは、データを「異なるフィールド構成のデータベース」に移すことをいいます。 同じデータベースで、データを保ったまま「フィールド構成を変更する」ときもマイグレーションといいます。 今回はデータベースをはじめて作成したので、既存のデータはなく、上記の意味でのマイグレーションではありません。

「rails db:migrate」の名前から想像できると思いますが、このコマンドはデータベースのマイグレーションにも使えます。 その場合は「create_table」だけでなく「change_table」などのメソッドも使います。

コンソール

ここで、モデルに備わっているいくつかのメソッドを紹介します。 先程定義を書いたWordクラスを例にとります。

  • @word = Word.new => Wordクラスのオブジェクト(モデル)を生成し、インスタンス変数@wordに代入する
  • @word.en= => @wordの指すオブジェクトのインスタンス変数@enに値を代入する
  • @word.en => @wordのインスタンス変数@enの指すオブジェクトの値を取得する
  • @word = Word.find_by(en: “good”) => データベースから、enフィールドの値が”good”であるデータを取り出し、@wordに代入する
  • @word.save => @wordの指すオブジェクトをデータベースに保存する
  • @word.delete => @wordの指すオブジェクトをデータベースから削除する

これらのメソッドを試すのにはRailsのコンソールを使うのが良いです。 少々回り道になりますが、コンソールを使ってみましょう。 上記のメソッドを試すと、その式の値が表示されるので何をしているのかがよく分かります。 次の例を丁寧にたどってみてください。

$ bundle exec rails console
Loading development environment (Rails 7.0.4)
irb(main):001:0> @word = Word.new
=> #<Word:0x00007f7759446f88 id: nil, en: nil, jp: nil, created_at: nil, updated_at: nil>
irb(main):002:0> @word.en="good"
=> "good"
irb(main):003:0> @word.jp="良い"
=> "良い"
irb(main):004:0> @word
=> #<Word:0x00007f7759446f88 id: nil, en: "good", jp: "良い", created_at: nil, updated_at: nil>
irb(main):005:0> @word.save
  TRANSACTION (0.7ms)  begin transaction
  Word Create (0.8ms)  INSERT INTO "words" ("en", "jp", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["en", "good"], ["jp", "良い"], ["created_at", "2022-10-21 04:38:51.334385"], ["updated_at", "2022-10-21 04:38:51.334385"]]
  TRANSACTION (14.4ms)  commit transaction
=> true
irb(main):006:0> @word2=Word.new
=> #<Word:0x00007f7758452578 id: nil, en: nil, jp: nil, created_at: nil, updated_at: nil>
irb(main):007:0> @word2=Word.find_by en: "good"
  Word Load (0.4ms)  SELECT "words".* FROM "words" WHERE "words"."en" = ? LIMIT ?  [["en", "good"], ["LIMIT", 1]]
=> #<Word:0x00007f7759505e10 id: 1, en: "good", jp: "良い", created_at: Fri, 21 Oct 2022 04:38:51.334385000 UTC +00:00, updated_at: Fri, 21 Oct 2022 04:38:51.334385000 UTC +00:00>
irb(main):008:0> @word.en == @word2.en
=> true
irb(main):011:0> @word.jp == @word2.jp
=> true
irb(main):012:0> @word.delete
  Word Destroy (43.0ms)  DELETE FROM "words" WHERE "words"."id" = ?  [["id", 1]]
=> #<Word:0x00007f7759446f88 id: 1, en: "good", jp: "良い", created_at: Fri, 21 Oct 2022 04:38:51.334385000 UTC +00:00, updated_at: Fri, 21 Oct 2022 04:38:51.334385000 UTC +00:00>
irb(main):013:0> @word = Word.find_by en: "good"
  Word Load (0.2ms)  SELECT "words".* FROM "words" WHERE "words"."en" = ? LIMIT ?  [["en", "good"], ["LIMIT", 1]]
=> nil
irb(main):014:0> exit
$

データベースとのやりとり(save、find_by、delete)ではSQLコマンドがデータベースに対して発行されているのがわかります。 RailsがSQLを発行してくれるので、プログラマーはSQLを知らなくてもデータベースを使うことができます。 これはRailsを使うアドバンテージのひとつです。

Railsのコンソールはプロンプトがirb(main)となっています。 irbはRubyのgemで単独で使うこともできます。 単にコマンドラインから「irb」と打ち込めばOKです。 Rubyのドキュメントを参照してください。

単語の新規作成

ナビゲーションバーのAppendをクリックしてから単語登録を完了するまでの一連の流れを確認します。 以下では(C)がクライアント(ブラウザ)側、(S)がサーバ(Rails)側とします。

  • (C)ナビゲーションバーのAppendをクリック⇒/words/appendへGETメソッドでアクセス
  • (S)Wordsコントローラのappendアクション(Wordsクラスのappendメソッド)へルーティング
  • (S)ビューで英単語と日本語訳のデータ入力枠、送信ボタンのあるHTMLを組み立て、送信
  • (C)単語と日本語訳を入力して送信ボタンをクリック⇒/words/createへPOSTメソッドでアクセス
  • (S)Wordsコントローラのcreateアクションへルーティング
  • (S)送られた単語と日本語訳をWordモデル(Wordクラスのインスタンス)にセットしデータベースに保存
  • (S)保存成功の場合⇒成功のメッセージと保存した英単語を表示するために/words/show/:idへリダイレクトする。ただし:idは登録したモデルのid
  • (S)保存失敗の場合⇒失敗のメッセージと入力枠を再表示(レスポンスのステータスコードを「unprocessable_entity」にする)

(注)「unprocessable_entity」は「クライアントのリクエストが正しかったが、その指示が処理できなかった」ことを表します。 サーバはクライアントのリクエストに対してレスポンスを返すときにステータスコードを常に送りますが、 railsが自動的にコードを作ってくれることが多く、プログラムに明示する必要がありませんでした。 Rails7ではTurboという仕組みが使われていて、Turboではformタグでクライアントがデータを送った後のサーバーの対応が決められています。 詳しいことは「Turbo Handbook」を参照してください。 成功⇒リダイレクト、失敗⇒「unprocessable_entity」ステータスコードで入力画面を再送信、という流れでほぼ良さそうです。

上記の流れではサーバとクライアントの間を2から3往復します。 一度の操作では完了しないことに注意してください。

なお、ナビゲーションバーのchangeをクリックしたときの流れもこれに似ています。 そこで、今回の記事ではappendとchangeの両方を説明します。

appendとchange共用のパーシャル

append(単語の追加)とchange(単語の変更)は処理が似ています。 両者ともformタグを使ってデータをサーバに送信します。 そこで、フォーム部分を共通に使えるパーシャルにしておきます。 パーシャルはプログラムのサブルーチンのようなもので、他のビューから呼び出し、埋め込むことができます。 パーシャルはファイル名の先頭をアンダースコアにします。 以下に_form.html.erbのリストを示します。

<%= form_with model: @word, url: @path, method: :post do |form| %>
  <div>
    <%= form.label :en, "英単語", class: "form-label" %>
    <%= form.text_field :en, value: @en, class: "form-control" %>
    <% @word && @word.errors.full_messages_for(:en).each do |message| %>
      <div class="text-danger"><%= "DB: 英単語は空欄にできません" if message == "En can't be blank" %></div>
      <div class="text-danger"><%= "DB: 英文字を入力してください" if message == "En is invalid" %></div>
      <div class="text-danger"><%= "DB: すでに同名の単語が登録されています" if message == "En has already been taken" %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :jp, "日本語訳", class: "form-label" %>
    <%= form.text_field :jp, value: @jp, class: "form-control" %>
    <% @word && @word.errors.full_messages_for(:jp).each do |message| %>
      <div class="text-danger"><%= "DB: 日本語訳は空欄にできません" if message == "Jp can't be blank" %></div>
    <% end %>
  </div>

  <div class="my-2">
    <%= form.submit @submit,  class: "btn btn-primary" %>
  </div>
<% end %>
  • form_withがformタグを生成するためのRailsのメソッド。
    • モデルを@wordとする。@wordはあらかじめコントローラで生成しておく
    • 送信先のURLを@pathで与える。@pathはコントローラで生成する
    • 送信メソッドはPOSTにする。 append(作成)ではPOST、change(変更)ではPATCHやPUTを用いるのがRailsの標準だが、ここでは両者ともPOSTを使うことにする
    • ブロックパラメータformにはFormBuilderオブジェクトが代入されており、そのオブジェクトにはlabeltext_fieldメソッドがある。 詳しくはAPIリファランスを参照
  • form.labelメソッドで、inputタグ上部にラベル(文字列)を表示する。 class: "form-label"はBootstrapのクラスclass="form-label"をタグの属性に指定する。
  • form.text_fieldで、type=”text”のinputタグが作成される。 タグの名前を”en”に初期状態での枠内の文字列を@enにする。 @enはコントローラで定義する。 class: "form-control"はBootstrapのクラス
  • 次の5行は、@wordがnilでなく、@word.errors.full_messages_for(:en)が空でない場合にメッセージを表示する。 この「空でない」場合とは、@wordをデータベースに保存しようとしてエラーになった場合である。 もちろん、最初のフォーム送信ではエラーはないが、送信後データ保存時にエラーが発生し、フォームが再送信される場合にメッセージが表示される。 したがって、ここのエラーメッセージはモデルのデータベース登録時のバリデーション・メッセージである。
  • jp(日本語訳)の入力フォームも同様
  • form.submitで送信ボタンを表示する。 ボタンに現れる文字列は@submitになるが、これはコントローラで設定する。 class: "btn btn-primary"はBootstrapのボタンデザインのクラス

appendアクション

データ(英単語・日本語訳)の新規作成はAppendメニューのクリックによって始まります。 このメニューのリンクは/words/appendに飛びます。 スラッシュから始まるリンクは、完全な形で書くとhttp://localhost:3000/words/appendになります。

このリンク先は/config/routes.rbによって、Wordsコントローラのappendアクション(appendメソッド)にルーティングされます。

Rails.application.routes.draw do
  get 'words/index'
  get 'words/show/:id', to: "words#show"
  get 'words/append'
  post 'words/create'
  get 'words/change'
  post 'words/update'
  get 'words/delete'
  delete 'words/exec_delete'
  get 'words/search'
  get 'words/list'
  root "words#index"
end

ブロックの上から3番め、get 'words/append'がそのルーティングです。 HTTPのGETメソッドで/words/appendをアクセスすると、このルーティングにマッチします。 行き先が省略されているときは、そのアドレス通りWordsコントローラのappendアクションに移動します。

Wordsコントローラ(Wordsクラスの定義)の中でappendアクションだけを取り出すと次のようになります。

class WordsController < ApplicationController
... ... ...
... ... ...
  def append
    @en = @jp = ""
    set_append
  end
... ... ...
... ... ...
  private

  def set_append
    @word = Word.new unless @word
    @path = words_create_path
    @submit = "作成"
  end
... ... ...
... ... ...
end

formパーシャルに必要なインスタンス変数はここで設定されます。

  • @wordにはWordモデルから新規作成されたオブジェクトが代入される。 クライアントからデータが送信された後にエラーが発生し、データの再送信画面を送る場合(後述)は@wordにはすでにデータが入っており、その@wordをそのまま使う。
  • @pathにはwords_create_pathメソッドの返り値が入る。 このメソッドはroutes.rbからRailsが自動生成するもので、このルーティングでは’/words/create’が代入される。 なお、このようなメソッドを確認するにはbundle exec rails routesとコマンドラインから入力する。 これによってルーティングの一覧が表示され、その一番左の列にメソッドが書かれている。 メソッドのないルーティングのルールもある。
  • @enと@jpには空文字列が代入される
  • @submitには”作成”が代入される

コントローラの処理の中で特に出力コマンドがない場合は対応するビューが呼ばれます。 /app/views/append.html.erbの内容は次のようになります。

<h1 class="my-2">単語登録</h1>
<%= render "form" %>
  • タイトルとして「単語登録」を表示。 `class=”my-2”はBootstrapのクラスでm(マージン)y(上下)を2(デフォルトで2rem)に設定する。 少し上下に隙間ができる
  • renderメソッドにより、formパーシャルを呼び出してここに埋め込む。 パーシャルのファイル名にはアンダースコアがつくが(/app/views/_form.html.erb)renderの引数ではアンダースコア、拡張子なしで呼び出す

パーシャルのおかげで、非常に短くてすみました。

Append画面

このような画面が表示されます。 サブミットボタンには@submitの内容である「作成」が表示されています。 このボタンを押すとPOSTメソッドで/words/createに送信します。

createアクション

送信ボタンがクリックされると、/words/createにPOSTメソッドでリクエストが届き、それはルーティングにより、createアクションを呼びだします。

... ... ...
post 'words/create'
... ... ...

コントローラのcreateアクションの部分を示します。

class WordsController < ApplicationController
... ... ...
... ... ...
  def create
    @en = params[:word][:en]
    @jp = params[:word][:jp]
    @word = Word.new(en: @en, jp: @jp)
    if @word.save
      flash[:success] = "単語を保存しました"
      redirect_to "/words/show/#{@word.id}", status: :see_other
    else
      set_append
      flash.now[:alert] = "単語を保存できませんでした"
      render :append, status: :unprocessable_entity
    end
  end
... ... ...
... ... ...
end

送信されたデータをparamsメソッドで取り出すことができます。 そのときデータにつけられた名前(例えば「:wordの:en」)はappendビューで作成したフォームタグに書かれたものです。 form_withでモデルを指定した場合は、モデル名が名前の1番めになります。 form.text_fieldの最初の引数が名前の2番めになります。 モデルを指定しない場合は、params[:en]のようにモデル名なしになります。

  • 英単語はparams[:word][:en]で取り出せる。
  • 日本語訳はparams[:word][:jp]で取り出せる。
  • @wordを新規作成する。 そのときenフィールドを@en、jpフィールドを@jpにする。 つまり送信された英単語と日本語訳にする
  • saveメソッドにより@wordをデータベースに登録する。 このとき、モデル定義(/app/models/word.rbファイル)に書いたバリデーションが行われる。 すでに書いたように、バリデーションとはデータのチェックのこと。 改めてどのようなバリデーションだったかを書くと
    • enはデータが存在しなければデータベースに保存できない
    • enは英文字の文字列でなければならない
    • enはユニークである(すでに同名のデータが存在するならば、データベースに保存できない)
    • jpはデータが存在しなければデータベースに保存できない
  • バリデーションががOKならば保存が実行され@word.saveは真になる。 バリデーションを通らなければ保存されず、@wordにエラーメッセージがセットされ、@word.saveは偽になる。
  • 無事保存された場合は、フラッシュに「単語を保存しました」を代入し、showアクションにリダイレクトする。 リダイレクトとは、HTTPステータスコードの300番台とリダイレクト先アドレスをクライアントに送り、クライアントからそのアドレスにアクセスしてもらうことをいう。 なお、Turbo Handbookを見るとTurboではフォームのPOST送信後は303(see other)ステータスコードを期待しているとのことである。 それに沿ってプログラムでは、redirectメソッドの最後にstatus: :see_otherを付け加えた。 303リダイレクトではブラウザはGETメソッドでリダイレクト先にアクセスすることになっている。 仮にステータスコードの指定を省略すると302(Found)がステータスコードになる。 302リダイレクトは「元のページが一時的に別アドレスになっている」ときに使うもので、POSTでアクセスして302リダイレクトが返されたときはブラウザは再びPOSTでリダイレクト先にアクセスする可能性がある。 今回のリダイレクト先のshowは「作成されたデータを表示」するアクションでGETでアクセスすべきものである。 以上から、303リダイレクトが妥当なリダイレクトであるといえる。 なお、歴史的な理由によりブラウザの中にはPOSTアクセス後の302リダイレクトに対してGETを用いている実装もかなりある。 Rails Guideのサンプルがstatus: :see_otherを書いていないのは、そのような背景があるものと思われる。 このような曖昧さがあることから、POSTのリダイレクトを再びPOSTでやることを明確にしたい場合は307リダイレクト(Temporary redirect)を用いる。
  • 保存に失敗した場合は、ステータスコードunprocessable_entity(データは受け取ったがサーバ側は想定された作業を遂行できなかった)を返し、リダイレクトせずにデータを送信する。 set_appendメソッドにより、@path送信先アドレス、@submitをセットする。 @wordはnilではないのでそのままになる。 このとき保存に失敗したバリデーションのエラー情報が@wordには含まれている。 renderメソッドでappendビューをクライアントに送る。 appendビューはformパーシャルを呼び出す。 このときformパーシャルの中で@wordのエラーメッセージが取り出され、それに対応した日本語メッセージがHTMLに加えられる。 このあとはクライアントからのPOST送信が繰り返されることになる

フラッシュ

フラッシュはリダイレクト後の画面でメッセージを表示するための仕組みです。 フラッシュはHashに似てキーと値を定義できます。

  • 「flash[キー]=メッセージ」でキーとメッセージを登録できる
  • 「flash[キー]」でメッセージを取り出せる

createアクションでデータが無事に保存された場合はflash[:success] = "単語を保存しました"でフラッシュに登録します。 これは次のshowアクションで取り出されて表示されることになります。

フラッシュはどのアクションでも使う可能性があるので、レイアウトの部分に入れてあります。 レイアウト/app/views/application.html.erbを以下に再掲します。

<!DOCTYPE html>
<html>
... ... ...
... ... ...
  <body>
    <div class="container">
      <div class="col-sm-10 col-lg-8 col-xl-6 mx-auto">
        <%= render "navbar" %>
        <%= render "flash" %>
        <%= yield %>
      </div>
    </div>
  </body>
</html>

ナビゲーションバーを表示するパーシャルnavbarを呼び出した後に、フラッシュを表示するパーシャルflashを呼び出します。 flashのコードは次の通りです。

<% flash.each do |name, msg| %>
  <% if name == "success" %>
    <%= content_tag :div, msg, class: "text-success" %>
  <% elsif name == "alert" %>
    <%= content_tag :div, msg, class: "text-danger" %>
  <% end %>
<% end %>

これを/app/views/_flash.html.erbというファイル名で保存します。 フラッシュはHashと同じようにeachコマンドを使うことができます。

  • successキーの場合は、text-successクラス(緑色)でメッセージを表示
  • alertキーの場合はtext-danger(赤色)でメッセージを表示

content_tagメソッドは、ここではdivタグの中にメッセージを入れたHTMLを埋め込みます。

フラッシュはリダイレクトで用いるものですから、次回の送信で反映されます。

  • リダイレクト(303ステータスコード+リダイレクト先のアドレス)をクライアントに送信する(送信1回め)
  • クライアントはそれに基づいてリダイレクト先にGETメソッドでアクセスする
  • サーバがデータを送信する(送信2回め)=>このときフラッシュが有効になる

しかし、リダイレクト無しでフラッシュを使いたいときもあります。 そのときはflash.nowを用います。 flash.nowでは1回めの送信でフラッシュが有効になります。 createアクションの中の次のコードを比べてみてください

if @word.save
  flash[:success] = "単語を保存しました"
  redirect_to "/words/show/#{@word.id}", status: :see_other #<=リダイレクトがあるのでflashを使う
else
  set_append
  flash.now[:alert] = "単語を保存できませんでした"
  render :append, status: :unprocessable_entity #<=リダイレクト無しなのでflash.nowを使う
end

エラーが起こったときはリダイレクトせず、appendビューを送るので、flashではなくflash.nowを使っています。

単語の変更

単語の変更はchange、updateアクションで行います。 この2つはappend、createに似ています。

change アクション

appendとの違いは@pathと@submitだけです。

class WordsController < ApplicationController
... ... ...
... ... ...
  def change
    @en = @jp = ""
    set_change
  end
... ... ...
... ... ...
  def set_change
    @word = Word.new unless @word
    @path = words_update_path
    @submit = "変更"
  end
end
  • @pathによるPOSTメソッドの送信先のアドレスにはwotds_update_pathメソッドを使う。 このメソッドは’/words/update’を返す
  • @submitは「変更」にする。 これがボタンの文字になる

対応するビューもほとんど同じです。 /app/views/change.html.erbの内容は次のとおりです。

<h1 class="my-2">単語の変更</h1>
<%= render "form" %>

appendのビューとの違いはタイトルだけです。 画面のスクリーンショットはほぼ同じなので省略します。 ボタンが押されると、POSTメソッドで先程の送信先にデータが送られます。

なお、Railsでは変更、編集、アップデートではPATCHまたはPUTメソッドが使われます。 そのようにプログラムを変更することはもちろん可能で、@pathで送信先をappendとchangeで区別したのと同様に@method変数に”post”、”patch”を入れて区別すればOKです。 その方がベターかもしれません。

update アクション

POSTメソッドで送信されたデータはroutes.rbによってWordsコントローラのupdateアクションにルーティングされます。

post 'words/update'

コントローラからupdateアクションの部分を取り出してみましょう。

class WordsController < ApplicationController
... ... ...
... ... ...
  def update
    @en = params[:word][:en]
    @jp = params[:word][:jp]
    @word = Word.find_by(en: @en)
    if @word == nil
      set_change
      flash.now[:alert] = "単語「#{@en}」は未登録のため変更できません"
      render :change, status: :unprocessable_entity
    elsif @word.update jp: @jp
      flash[:success] = "単語を変更しました"
      redirect_to "/words/show/#{@word.id}", status: :see_other
    else
      set_change
      flash.now[:alert] = "変更した単語を保存できませんでした"
      render :change, status: :unprocessable_entity
    end
  end
... ... ...
... ... ...
  def set_change
    @word = Word.new unless @word
    @path = words_update_path
    @submit = "変更"
  end
end
  • @enと@jpに送られてきた英単語と日本語訳を代入する
  • その英単語をもつWordオブジェクトをデータベースから探し出し@wordに代入する。 そのようなデータがなければ@wordにはnilが代入される
  • @wordがnilならば
    • set_changeメソッドで@wordに新規作成したWordオブジェクトを代入、@pathと@submitを初期化
    • フラッシュに「単語・・・は未登録のため変更できません」をセット。 今回の送信でフラッシュを使うのでflash.nowを使う
    • 再びchangeビューを作成し、unprocessable_entityのステータスコードとともに送信
  • そうでなければ@wordのjp部分のデータを用いてデータベースを書き換える(update)。 その書き換えが成功した場合は
    • フラッシュに「単語を変更しました」をセット
    • showアクションへリダイレクト
  • データ書き換えに失敗した場合は
    • set_changeメソッドで@pathと@submitを初期化。 @wordはnilではないので変更されない。 この@wordには書き換え時のエラーメッセージが保持されている
    • フラッシュに「変更した単語を保存できませんでした」をセット。
    • 再びchangeビューを作成し、unprocessable_entityのステータスコードとともに送信

やっていることはcreateアクションと似ています。 違いはsaveの代わりにupdateを用いているところです。

  • saveは@wordのデータ全部を作成または更新する
  • updateは@wordの指定されたフィールドのデータのみを更新する

saveは更新にも使えるのでこちらをupdateの代わりに使うこともできます。

show アクション

createとupdateのリダイレクト先のshowアクションについて説明します。 showアクションは特定の1つの単語について表示します。 どの単語かの指定はアドレスの中に書きます。

  • words/show/1=>データベースからid=1の要素を取り出して表示
  • words/show/2=>データベースからid=2の要素を取り出して表示

このような形になります。 createやupdateでは

redirect_to "/words/show/#{@word.id}", status: :see_other

とい形でshowメソッドにリダイレクトしていました。 #{@word.id}の部分が取り出したいデータのidになります。 すなわち、createやupdateで作成または変更したデータのidです。

showメソッドへのルーティングは次のようになります。

get 'words/show/:id', to: "words#show"

GETメソッドでwords/show/:idのアドレスで呼ばれた場合、Wordsコントローラのshowアクションを呼び出します そのとき、:idの部分はパラメータになり、params[:id]でその値を読み出すことができます。 例えば/words/show/10でアクセスするとparams[:id]は10になります。

コントローラにおけるアクションは次のようになります。

def show
  begin
    @word = Word.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    @word = nil
    flash.now[:alert] = "データベースにはid=#{params[:id]}のデータは登録されていません"
  end
end    

findメソッドは引数がidの値になるようなデータを探して取り出します。 データベースからparams[:id]の値がidフィールドに書かれているレコードを取り出し、@wordに代入します。 データが取り出せないときには例外が発生しますので、それを捕捉し、@wordをnilにしてフラッシュを作成します。

ここで、beginとrescueの使い方を説明します。 Rubyは例外が発生するとそこでプログラムを停止し、例外の内容を表示します。 プログラムを停止したくない場合はbeginとrescueを使い「例外の捕捉(つかまえること)」をします。 beginとrescueの間に例外が発生する可能性のあるステートメントを書きます。 rescueの次に想定される例外の名前を書きます。 その例外が発生するとrescue節に実行が移るので、そこで例外に対する処置を行います。

ビューは次のようになります。

<% if @word %>
  <ul class="list-group">
    <li class="list-group-item"><%= @word.en %></li>
    <li class="list-group-item"><%= @word.jp %></li>
  </ul>
<% end %>

@wordがnilのときは何も表示されません。 それでもフラッシュだけは表示されます。

2023

単項マイナスと構文解析

1 minute read

単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合

Raccライブラリと構文解析

3 minute read

パーサ・ジェネレータとは 少し複雑な文法 四則(加減乗除)計算のBNF Racc で実装 クラス定義、BNFの記述部分 ヘッダー、インナー、フッター コンパイルと実行 演算子の優先順位と結合における左右の優先順位 まとめ

StrScanライブラリと字句解析

less than 1 minute read

StrScanライブラリのドキュメント 字句解析とは StrScanライブラリ StrScanライブラリを使った字句解析 実例

Gem

1 minute read

lbtというgemを作って公開してみた lbtはどんなgemか ファイルの配置 lbt.gemspec Rakefile gemのビルド RubyGems.orgへのアップロード 補足・・rake/gempackagetaskサブライブラリについて

Encoding

1 minute read

文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。

Thread

less than 1 minute read

Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。

Fiber

1 minute read

Fiberは「ノンプリエンプティブな軽量スレッド」とRubyのマニュアルに記載されています。

RDoc

less than 1 minute read

今回はRubyプログラムから自動的にドキュメントを作成するRDocについて書きたいと思います。 私はこのことについて、エキスパートではありません。 この記事も、初心者の体験談だと考えてください。

Back to Top ↑

2022

Ruby/GTK4

5 minute read

Ruby/Gtkの記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Railsで作った単語帳プログラム)のGTK 4版を作りました。 プログラムは「徒然なるままにRuby」のGitHubレポジトリに置いてあります。 レポジトリをダウンロードし、ディレクトリ_example/...

Shoes – Rubyとグラフィック

5 minute read

Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。

Rails7 テスト

5 minute read

前回作ったWordbook(リソースフル)のテストを書いてみます。 RailsのテストはminitestをRails用に拡張したものです。

Rails7 モデルとデータベース

5 minute read

今回はRailsにおけるデータの作成と保存、そして変更について説明します。 そのベースになるモデルとデータベースの話から始め、appendとchangeの動作について詳しく説明します。

Rails7とBootstrap

3 minute read

一般に、HTMLは文書の構造を表し、CSSはその体裁(見栄え)を表します。 Railsは最終的にCSSを含むHTML文書を出力するので、この2つについての理解が必須です。 この記事ではとくにCSSの人気ライブラリであるBootstrapを紹介します。 BootstrapはJavascriptも含んでいます。

Rails7のインストール

2 minute read

Rubyの最も人気のあるアプリケーションであるRuby on Railsを取り上げようと思い、書き始めました。 予想してはいましたが、相当な分量になってしまいました。 そのため、何回かに分けて記事にすることにします。 また、対象となる読者のレベルをどうしようかと考えましたが、「徒然Ruby」が基礎的な内容から始ま...

GemとBundler

1 minute read

Rubyのライブラリ管理システムのRubygemsとコマンドgemおよびbundlerについて説明します。

minitest(3)モックの詳細

1 minute read

minitestについて連続して2回書いてきました。 「minitestはドキュメントが少ない」という人がいますが、私も同感です。 例えば、モックとスタブの説明も少ないです。 そこで、今回はmock.rbのソースコードを参考に、モックの私的ドキュメントを書いてみました。 あくまで私個人の考えであり、minites...

minitest(1)テストとは

2 minute read

アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。

public、private、protected

2 minute read

今回はメソッドの呼び出し制限ついて説明します。 呼び出し制限にはpublic、private、protectedの3つがあります。

アプリ制作、インストール、テスト

1 minute read

2023/10/29 追記:この記事は新しく書き直しました。 古い記事で使っていたGitHubのCalcが大幅にアップデートされたためです。 そこで、この記事に合うようなプログラムsimple_calcを新たに作りました。 このプログラムは本レポジトリの_example/simple_calcにあります。

case文

2 minute read

if〜elsif〜・・・〜else〜endは皆さん良く使うでしょうか? これは場合分けで良く使われる方法です。 これと同様の制御構造にcase文があります。 Cのswitch文に似ていますが、より強力な機能を持っています。 if-else-endよりも高い能力があるといえます。

Lambda

2 minute read

Procオブジェクトを生成するメソッドlambdaについて説明します。

Proc オブジェクト

2 minute read

今回はブロックを一般化したオブジェクトProcを説明します。

モジュール

1 minute read

モジュールには名前空間とミックスイン(Mix-in)の2つの機能があります。 ここではミックスインについて説明します。

Kernelモジュール

less than 1 minute read

Kernelモジュールのメソッドはどこでも使うことができます。 そのメソッドの中には便利で有用なものが多いです。

便利なメソッド

1 minute read

ここでは私が便利だと思ったメソッドを紹介します。

文字列と正規表現

3 minute read

文字列は最も使うオブジェクトのひとつです。 特にウェブ・アプリケーションでは、コンテンツだけでなくHTMLのタグやCSSを含めすべてが文字列です。 Rubyは文字列オブジェクトのメソッドが充実しており、またパターンマッチのための正規表現も充実しています。

配列

2 minute read

配列は、どのプログラミング言語にもあると思います。 複数の要素を一括して扱うことができるのが配列です。 Rubyの配列はメソッドが充実しているので、プログラムを効率的、機能的に書くのに役立ちます。

トップレベルのメソッド

1 minute read

今回はメソッド定義です。 メソッド定義はRubyの核心ですが、今回はトップレベルに限って説明します。 この限定によって、内容はかなり易しくなっています。

ブロックとイテレータ

less than 1 minute read

ブロックはRubyの特長です。 ブロックのおかげで記述が非常にすっきりと分かりやすくなります。 今回はブロックをイテレータの本体として使う方法を説明します。

整数

less than 1 minute read

ここではRubyの最も基本的なオブジェクトである整数について説明します。

Hello world

less than 1 minute read

「徒然なるままに」をネットで調べてみると、「することもなく、手持無沙汰なのにまかせてという意味」とありました。 まさに、自分の現状を言い当てた言葉。 しかも、ブログに書くネタもなかなか思いつかない日々。

Back to Top ↑