第5回 Ruby on Rails〜タスク管理アプリケーション 機能追加①(バリデーション)〜

Bootstrap

今回からは、前回CRUD機能を実装したタスク管理アプリケーションをより使いやすくするために、様々な機能を追加していきます。

その第一弾として今回は「バリデーション」を追加します。

バリデーション

初めにバリデーションを実装します。

データには、「必ず値を入れたい」「必ず数値が入るようにしたい」など想定される内容の範囲があります。

こうした想定される内容に沿った正しいデータだけをデータベースに保存するための検証機能をバリデーションといいます。

Railsのモデルには、Rubyコードによってデータの内容をチェックする検証の仕組みが用意されています。
モデルの検証を使うことで自由度の高いチェックを行うことができる上、ユーザーにわかりやすいエラーメッセージを伝えることが可能です。

しかし、他のシステムからデータベースを利用する場合や、プログラム上の不備などでモデルの検証が想定どうりに動かない場合もあります。
モデルの検証とデータベース側でのデータの制限を行います。

データベース側の制限

初めにデータベース側の定義について見ていきます。

データ型

データベースのカラムにはデータ型を指定します。
格納される予定のデータの種類に応じたデータ型を利用します。

データ型には主に以下の型があります。

NOT NULL制約

カラム値としてNULLを格納する必要がない場合、NOT NULL制約をつけることで、物理的にNULLを保存できないようにできます。
アプリケーションにおいて、あるモデルの属性の値が必ず入ることを期待している場合はNOT NULL制約をつけます。

前回までで作成したタスクアプリケーションのTaskモデルの属性の中にnameがありました。
この属性の入力を必須にするためにtasksテーブルのnameカラムにNOT NULL制約をつけます。

まずは以下のコマンドを実行して「tasksテーブルのnameをnull不可に変える」マイグレーションファイルを作成します。
ファイル名はChangeTasksNameNotNullとします。

$ bin/rails g migration ChangeTasksNameNotNull
Running via Spring preloader in process 53231
      invoke  active_record
      create   db/migrate/20210307200412_change_tasks_name_not_null.rb

生成したファイルの中身を以下のように変更します。

db/migrate/20210307200412_change_tasks_name_not_null.rb

class ChangeTasksNameNotNull < ActiveRecord::Migration[6.1]
  def change
    change_column_null :tasks, :name, false
  end
end

change_column_nullを使うと既存テーブルのカラムのNOT NULL制約の付け外しができます。
引数にはテーブル名、カラム名、NULLを許容するか否かをそれぞれ指定します。

編集後、マイグレーションをデータベースに適用します。

$ bin/rails db:migrate

このようなNOT NULL制約は、テーブル作成時から必要だとわかっている場合、作成時にnull: falseオプションをつけるように編集してから適用したとしても同じ効果を得ることができます。

db/migrate/20201225015314_create_tasks.rb

class CreateTasks < ActiveRecord::Migration[6.1]
  def change
    create_table :tasks do |t|
      t.string :name, null: false
      t.text :description
      t.timestamps
    end
  end
end

以上でNOT NULL制約をつけることができました。

文字列カラムの長さの指定

文字列カラムに対して文字列の長さを指定するには、limitオプションを使います。
これにより必要以上に長いデータが保存されることを防ぐことができます。

tasksテーブルのnameカラムを30文字以内に制限するために、先ほど同様ChangeTasksNameLimit30マイグレーションファイルを作成・適用します。

$ bin/rails g migration ChangeTasksNameLimit30
Running via Spring preloader in process 54035
      invoke  active_record
      create    db/migrate/20210307203024_change_tasks_name_limit30.rb

db/migrate/20210307203024_change_tasks_name_limit30.rb

class ChangeTasksNameLimit30 < ActiveRecord::Migration[6.1]
  def up
    change_column :tasks, :name, :string, limit: 30
  end
  def down
    change_column :tasks, :name, :string
  end
end

ファイルを編集したら以下のコマンドで適用します。

$ bin/rails db:migrate

今回のようにchange_columnはadd_columnと異なり、バージョンを戻す際の処理を、バージョンを上げる処理から自動生成できません。
そのためdownメソッド内にバージョンを下げる処理を記述します。
upメソッドのみ記述した場合は、このマイグレーションの適用を取り消す際にnot automatically reversibleという例外が発生します。

ユニークインデックスの作成

テーブルのあるカラムのデータが、全レコード内で一意データベースにユニークインデックスを作成することで、一意性が壊されるのを防ぎます。

例えばTaskモデルのname属性を一意にしたい場合、tasksテーブルのnameカラムに対してユニークインデックスを追加します。
※ ただし本当にnameカラムにユニークインデックスを追加すると後々不便になりそうなので、実際に変更は行いません。

$ bin/rails g migration AddNameIndexToTasks
class AddNameIndexToTasks < ActiveRecord::Migration[6.1]
  def change
    add_index :tasks, :name, unique: true
  end
end

なお、ユニークインデックスを作成しているカラムの値でも、NULLは複数存在することができます。

このようにデータの内容の範囲をデータベースに定義することで、想定にそぐわない値が格納できないように予防できました。

モデルの検証

前節で、データの内容の範囲をデータベースで定義し制限しました。
しかし、より自由度の高い検証や、ユーザーへの丁寧でわかりやすいエラーメッセージを実現するために、モデルでの検証を実装します。

モデルの検証の仕組み

Railsの登録・更新のためのメソッドはsaveであり、データベースに登録・更新する前に自動的に検証を行います。

エラーがあれば登録・更新をしないで差し戻し、falseを返します。
エラーがなければデータベースへ登録・更新を行ってtrueを返します。
検証失敗以外の予期せぬエラーがあれば、例外を発生させます。

また、valid?メソッドを呼ぶことで検証処理を単独で呼ぶことができます。

検証の書き方

モデルの検証コードの書き方は大きく分けて2つあります。

  • ①検証用のヘルパーを利用する
  • ②自身で任意の検証コードを記述する

Railsでは以下のように様々な検証用のヘルパーが用意されています。

実際にヘルパーの使い方を見ていきます。

必須属性の検証

例としてTaskモデルのname属性の値が入っていなければ検証エラーになるように、検証を追加します。

task.rvを以下のように変更します。

app/models/task.rb

class Task < ApplicationRecord
  validates :name, presence: true
end

これでname属性を指定せずTaskデータをsaveすると、falseが返され検証エラーになります。

空文字列””や半角スペースのみの文字列” “など内容がない文字列は、データを指定していないのと同様に扱われます。

検証エラーメッセージの表示

必須属性の検証を実装できましたが、タスクアプリケーションの全体的な要件としては、Taskモデルが検証に引っかかった時に、ユーザーに原因をわかりやすく表示して再入力を促す必要があります。

コントローラの変更

app/controllers/tasks_controller.rbを以下のように変更します。

app/controllers/tasks_controller.rb

def create
  @task = Task.new(task_params)

  if @task.save
    redirect_to tasks_url, notice: "タスク 「#{@task.name}」を登録しました。"
  else
    render :new
  end
end

変更点は3つあります。

1点目は登録に用いるメソッドをsave!からsaveに変更したことです。
saveを使うことで戻り値で制御を変えるようにしています。

2点目は検証エラー時の処理の追加です。検証エラー時に、render :newによって登録画面を再び表示して、ユーザーに再入力を促しています。

3点目はTaskオブジェクトをインスタンス変数@taskに代入したことです。
検証エラーで登録画面を再度表示する際に@taskをビューに伝えることで、検証を行った現物のTaskオブジェクトを渡します。
これにより前回操作したままの値をフォーム内に引き継ぐことができる上、検証エラーの内容をユーザーに対して表示できます。

ビューの変更

検証エラーメッセージを表示するようビューを変更します。
app/views/tasks/_form.html.slimを以下のように変更します。

app/views/tasks/_form.html.slim

- if task.errors.present?
  ul#error_explanation
    - task.errors.full_messages.each do |message|
      li= message

= form_with model: @task, local: true do |f|
 ・・・

error.present?でエラーの有無を調べ、エラーがある場合はエラーメッセージを表示します。

文字列長の検証

次に文字列の長さを制限する検証を追加します。

データベースの定義でnameカラムの文字列を30文字以下に制限していますが、このままだと30文字を超える文字列が入力された場合、登録・更新の際に例外が発生します。
これではユーザーが問題の原因に気づきづらいため、検証エラーを出すようにします。

Taskモデル(app/models/task.rb)二以下を追記します。

validates :name, length: { maximum: 30 }

複数の検証を1行で書くこともできます。

validates :name, presence: true, length: { maximum: 30 }

これでname属性の文字列の長さを制限出来ました。

オリジナルの検証

Railsのヘルパーだけで要件を満たせない場合は自分で検証コードを書くこともあります。

自分で検証コードを書く場合の書き方は2つあります。

  • ①検証用のメソッドを追加する
  • ②自前のValidatorを作って利用する

使い分けとしては、あるモデル専用の処理を書く場合は①、複数のモデルで共通利用する場合は②が適しています。

今回は①の方法でTaskモデルに「name属性の値にはカンマが含まれていないかどうか」の検証を追加します。

検証用のメソッドの追加

検証を追加するには以下の手順で進めます。

  • ・検証用のメソッドをモデルクラスに登録する
  • ・検証用のメソッドを実装する

まずはapp/models/task.rbに以下のコードを追加します。

app/models/task.rb

validate :validate_name_not_including_comma

次にこの名前の検証用メソッドを追加します。
このメソッドはTaskモデルでしか使用しない想定なのでprivateに定義します。

app/models/task.rb

private
  
  def validate_name_not_including_comma
    errors.add(:name, 'にカンマを含めることはできません') if name&.include?(',')
  end

nameにカンマが含まれていた場合、nameに関する検証エラーの詳細をerrors.addで格納します。

なお、nameがnilの場合に例外が発生しないように&.include?としています。nameがnilの時はこの検証を通り、必須検証で引っかかります。

これでオリジナルの「name属性の値にはカンマが含まれていないかどうか」の検証を実装できました。

自前のValidatorの利用

自前のValidatorを作るには、ActiveModel::Validatorを継承してvalidateメソッドを定義します。

定義したメソッドはvalidates_withを使って呼び出します。

例えば以下は、生年月日が未来の日付を設定していないか検証するValidatorを定義しています。

class BirthdayValidation < ActiveModel::Validator
  def validate(record)
    record.errors[:birthday] << 'は未来の日付を選択できません' if record.birthday.present? && record.birthday > Date.today.to_date
  end
end

今日より後の日付を選択した場合、エラーの詳細が格納されます。

このValidatorを呼び出すにはmodelに以下のように記述します。

validates_with BirthdayValidation

これでBirthdayValidationクラスにレコードが渡され、条件に基づいてエラーが追加されます。

終わりに

今回は以前作成したタスク管理アプリケーションに新たにバリデーションを追加しました。バリデーションを実装することで、想定に沿った正しいデータだけを格納することができるようになりました。

次回からは、今回に続いてコールバックやログイン機能などを追加していきます。

コメント

タイトルとURLをコピーしました