はじめに
前回は
Deviseのビューをカスタム・フラッシュメッセージ として
- Deviseのビュー
- Bootstrapの適用
- フラッシュメッセージ
について学びました。
今回は
バリデーションメッセージのテンプレート化 として
- Railsコンソール
- バリデーション
- errors
- バリデーションメッセージ
について学びます。
では、はじめていきましょう。
1. コンソールを使ってみる
最初にちょっと脱線します。
rails c
ロジックの検証に是非とも使いたいのが、Railsコンソールです。
モデルのロジックやDBの接続も可能なので、データーの入力や削除にも使えますし、クエリの結果を調べたりもできます。
最近では使えるメソッドの候補等も表示されてとても親切なツールになりました。
では、早速ターミナルから作業を始めます。
一旦データーを初期状態に戻したいので、
todo_app % rails db:migrate:reset
でテーブルを初期化します。
todo_app % rails db:migrate:reset
Dropped database 'storage/development.sqlite3'
Dropped database 'storage/test.sqlite3'
Created database 'storage/development.sqlite3'
Created database 'storage/test.sqlite3'
== 20231105023538 CreateTasks: migrating ======================================
-- create_table(:tasks)
-> 0.0008s
== 20231105023538 CreateTasks: migrated (0.0008s) =============================
== 20231108012222 DeviseCreateUsers: migrating ================================
-- create_table(:users)
-> 0.0007s
-- add_index(:users, :email, {:unique=>true})
-> 0.0003s
-- add_index(:users, :reset_password_token, {:unique=>true})
-> 0.0003s
== 20231108012222 DeviseCreateUsers: migrated (0.0013s) =======================
このようなログが流れたら次に、
todo_app % rails c
でコンソールを開きましょう。
todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001>
ここから、 データーを作成していきます。
new
Railsは、モデルとなるclass
を使ってインスタンスを作成します。
新しいインスタンスなので、
irb(main):001> Task.new
このように書きます。
すると以下のように結果が返ってきます。
irb(main):001> Task.new
=> #<Task:0x0000000105dd8730 id: nil, title: nil, description: nil, start_time: nil, end_time: nil, status: "not_started", created_at: nil, updated_at: nil>
この時点では、enumを設定したデフォルト値のある status
以外の入力がありません。
続いて値を入力していきますが、方法は2通りあり
引数で渡す
irb(main):002> Task.new(title: "test")
=>
#<Task:0x00000001088c4250
id: nil,
title: "test",
description: nil,
start_time: nil,
end_time: nil,
status: "not_started",
created_at: nil,
updated_at: nil>
一度変数に代入して後で足す
irb(main):003> task = Task.new(title: "test")
=>
#<Task:0x00000001088cb910
...
irb(main):004> task.description = "Hello World!"
=> "Hello World!"
irb(main):005> task
=>
#<Task:0x00000001088cb910
id: nil,
title: "test",
description: "Hello World!",
start_time: nil,
end_time: nil,
status: "not_started",
created_at: nil,
updated_at: nil>
このようになります。ですので、インスタンスの作成後も値を入れ直す事が可能です。
モデルからインスタンスを作って値を代入する所まで来ましたが、このままではただのインスタンスですので、DBとの接続は行われていません。
save
インスタンスは、 save
メソッドを実行する事で、DBの各テーブルにインスタンスの値をレコードします。
しかし、ここまでの状態のものを保存してみると、
irb(main):006> task.save
=> false
falseとだけ出て終わりました。これは、保存出来ていない事を示します。
ただ、これだけでは何があったか分からないですね。
その場合、
irb(main):007> task.save!
/opt/local/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.1.1/lib/active_record/validations.rb:84:in `raise_validation_error': バリデー失敗しました: 開始日を入力してください (ActiveRecord::RecordInvalid)
このようなエラーとなります。また、エラーが出た原因として、 バリデーションメッセージが表示されています。
通常バリデーションに引っかかっても何も表示されないのですが、!
を付ける事で、バリデーションエラーを例外エラー(赤い画面のやつ)として拾うようになります。
バリデー失敗しました: 開始日を入力してください
となっていますので、データーを入れてみましょう。
irb(main):008> task.start_time = Time.current
=> Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00
値の代入後は、戻り値である現在の日時しか表示されませんので、次で現状確認をしています。
irb(main):009> task
=>
#<Task:0x00000001088cb910
id: nil,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: nil,
updated_at: nil>
日時が入りました。
では、これを save
してみます。
現在インスタンスは、変数 task
の中に入っていますので、
irb(main):010> task.save!
TRANSACTION (0.2ms) begin transaction
Task Create (2.6ms) INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id" [["title", "test"], ["description", "Hello World!"], ["start_time", "2023-11-17 05:53:53.809442"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 06:00:30.446569"], ["updated_at", "2023-11-17 06:00:30.446569"]]
TRANSACTION (1.6ms) commit transaction
=> true
saveがうまくいった時の戻り値は true
となり、
INSERT INTO "tasks"
のようにSQLがレコードされるログが出てきますので、ここでようやくDBにアクセスしたことになります。
また、 TRANSACTION (1.6ms) commit transaction
というログで、DB上でも問題なくレコードが終了したことが分かります。
create
create
は new
と save
を一緒に行います。
irb(main):011> Task.create(title: "test2", description: "荷物を送る", start_time: Time.current)
TRANSACTION (0.1ms) begin transaction
Task Create (0.5ms) INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id" [["title", "test2"], ["description", "荷物を送る"], ["start_time", "2023-11-17 06:51:40.954905"], ["end_time", nil], ["status", 0created_at", "2023-11-17 06:51:40.955624"], ["updated_at", "2023-11-17 06:51:40.955624"]]
TRANSACTION (1.0ms) commit transaction
=>
#<Task:0x0000000108b26908
id: 2,
title: "test2",
description: "荷物を送る",
start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>
save
が成功した時の戻り値は true
でしたが、 create
が成功した時の戻り値はモデルオブジェクトそのものとなります。
first・last
次に登録したレコードを呼び出してみます。
irb(main):012> Task.last
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT ? [["LIMIT", 1]]
=>
#<Task:0x0000000108c45280
id: 2,
title: "test2",
description: "荷物を送る",
start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>
last
は直前に登録したレコードを呼び出します。
逆に最初のレコードを呼び出す為には、 first
を使います。
irb(main):013> Task.first
Task Load (0.2ms) SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT ? [["LIMIT", 1]]
=>
#<Task:0x00000001088c7810
id: 1,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00>
find
find
は 登録された id
からレコードを呼び出します。
irb(main):014> Task.find(1)
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=>
#<Task:0x0000000108c08d58
id: 1,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>
レコードが存在しない場合は例外エラーとなります。
irb(main):015> task = Task.find(100)
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]]
/opt/local/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.1.1/lib/active_record/core.rb:252:in `find': Couldn't find Task with 'id'=100 (ActiveRecord::RecordNotFound)
find_by
find_by
は 呼び出すレコードをカラムを指定して行なえます。
irb(main):016> Task.find_by(title: "test")
Task Load (0.2ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."title" = ? LIMIT ? [["title", "test"], ["LIMIT", 1]]
=>
#<Task:0x0000000108aa0f10
id: 1,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>
レコードが存在しない場合は nil
となります。
irb(main):017> Task.find_by(id: 100)
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]]
=> nil
update
次に登録済みのレコードを編集します。
end_time
にも値を入れてみましょう。
今回は、先に変数 task
に Task.first
を代入します。
irb(main):018> task = Task.first
Task Load (0.2ms) SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" ASC LIMIT ? [["LIMIT", 1]]
=>
#<Task:0x0000000108be5650
...
現在時刻から一日後の時間を入れますので、 Time.current.tomorrow
とします。
irb(main):019> task.update(end_time: Time.current.tomorrow)
TRANSACTION (0.1ms) begin transaction
Task Update (0.6ms) UPDATE "tasks" SET "end_time" = ?, "updated_at" = ? WHERE "tasks"."id" = ? [["end_time", "2023-11-18 07:02:38.622984"], ["updated_at", "2023-11-17 07:02:38.625491"], ["id", 1]]
TRANSACTION (0.3ms) commit transaction
=> true
この時の戻り地は true
となります。
all
全てのレコードを取得するには、 all
を使います。
irb(main):020> Task.all
Task Load (0.2ms) SELECT "tasks".* FROM "tasks" /* loading for pp */ LIMIT ? [["LIMIT", 11]]
=>
[#<Task:0x0000000108c4c080
id: 1,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>,
#<Task:0x0000000108c4bf40
id: 2,
title: "test2",
description: "荷物を送る",
start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>]
複数のレコードが配列 []
の中に入っています。
where
where
は複数のレコードを探す事が出来ます。
初期ステータス not_started
のレコードを探すなら
irb(main):021> Task.where(status: "not_started")
Task Load (0.2ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."status" = ? /* loading for pp */ LIMIT ? [["status", 0], ["LIMIT", 11]]
=>
[#<Task:0x0000000108c46f40
id: 1,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>,
#<Task:0x0000000108c46e00
id: 2,
title: "test2",
description: "荷物を送る",
start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>]
このようになりますし、 end_time
に値が入っているものなら、
irb(main):022> Task.where.not(end_time: nil)
Task Load (0.2ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."end_time" IS NOT NULL /* loading for pp */ LIMIT ? [["LIMIT", 11]]
=>
[#<Task:0x0000000108c4b180
id: 1,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>]
のように nil
でないものと書く事ができます。
destroy
レコードの削除は、 destroy
を使います。
変数 task
にインスタンスを入れて、 destroy
しています。
irb(main):023> task = Task.last
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT ? [["LIMIT", 1]]
=>
#<Task:0x0000000108bc0dc8
...
irb(main):024> task.destroy
TRANSACTION (0.1ms) begin transaction
Task Destroy (0.4ms) DELETE FROM "tasks" WHERE "tasks"."id" = ? [["id", 2]]
TRANSACTION (0.7ms) commit transaction
=>
#<Task:0x0000000108bc0dc8
id: 2,
title: "test2",
description: "荷物を送る",
start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>
これは、下記のように直接書いてもOKです。
irb(main):025> Task.last.destroy
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT ? [["LIMIT", 1]]
TRANSACTION (0.1ms) begin transaction
Task Destroy (0.5ms) DELETE FROM "tasks" WHERE "tasks"."id" = ? [["id", 1]]
TRANSACTION (0.1ms) commit transaction
=>
#<Task:0x0000000108c42300
id: 1,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>
ただし、レコードがない場合は
irb(main):026> Task.find(100).destroy
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]]
/opt/local/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.1.1/lib/active_record/core.rb:252:in `find': Couldn't find Task with 'id'=100 (ActiveRecord::RecordNotFound)
例外エラーとなります。
destroy_all
テーブルのレコード全て削除する時は destroy_all
を使います。
先に、レコードを10件作ります。 each
を使ってレコードの作成を10回繰り返します。
irb(main):027> (1..10).each{|i| Task.create!(title: "test#{i}", start_time: Time.current) }
TRANSACTION (0.1ms) begin transaction
Task Create (2.0ms) INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id" [["title", "test1"], ["description", nil], ["start_time", "2023-11-17 07:40:35.348580"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 07:40:35.349055"], ["updated_at", "2023-11-17 07:40:35.349055"]]
TRANSACTION (0.3ms) commit transaction
TRANSACTION (0.0ms) begin transaction
Task Create (0.2ms) INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id" [["title", "test2"], ["description", nil], ["start_time", "2023-11-17 07:40:35.352315"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 07:40:35.352533"], ["updated_at", "2023-11-17 07:40:35.352533"]]
TRANSACTION (0.1ms) commit transaction
(略)
, ?, ?) RETURNING "id" [["title", "test10"], ["description", nil], ["start_time", "2023-11-17 07:40:35.361751"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 07:40:35.361886"], ["updated_at", "2023-11-17 07:40:35.361886"]]
TRANSACTION (0.1ms) commit transaction
=> 1..10
現在10件のレコードがあります。
irb(main):028> Task.count
Task Count (0.2ms) SELECT COUNT(*) FROM "tasks"
=> 10
これを削除します。
irb(main):029> Task.destroy_all
Task Load (0.2ms) SELECT "tasks".* FROM "tasks"
TRANSACTION (0.0ms) begin transaction
Task Destroy (0.2ms) DELETE FROM "tasks" WHERE "tasks"."id" = ? [["id", 3]]
TRANSACTION (0.1ms) commit transaction
TRANSACTION (0.0ms) begin transaction
Task Destroy (0.2ms) DELETE FROM "tasks" WHERE "tasks"."id" = ? [["id", 4]]
TRANSACTION (0.1ms) commit transaction
TRANSACTION (0.0ms) begin transaction
(略)
#<Task:0x0000000108aaa010
id: 12,
title: "test10",
description: nil,
start_time: Fri, 17 Nov 2023 07:40:35.361751000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 07:40:35.361886000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:40:35.361886000 UTC +00:00>]
削除できました。数も確認します。
irb(main):030> Task.count
Task Count (0.2ms) SELECT COUNT(*) FROM "tasks"
=> 0
ここで、削除したものはテーブルを落とした訳ではないので、
irb(main):031> Task.create(title: "test", start_time: Time.current)
TRANSACTION (0.1ms) begin transaction
Task Create (0.6ms) INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id" [["title", "test"], ["description", nil], ["start_time", "2023-11-17 07:49:17.172420"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 07:49:17.173004"], ["updated_at", "2023-11-17 07:49:17.173004"]]
TRANSACTION (0.3ms) commit transaction
=>
#<Task:0x0000000108b8d4a0
id: 13,
title: "test",
description: nil,
start_time: Fri, 17 Nov 2023 07:49:17.172420000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 07:49:17.173004000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:49:17.173004000 UTC +00:00>
id
自体は初期化されず、続きの id
(上の例だと 13) が採番されています。
これを初期化するには、一旦
irb(main):032> exit
で、コンソールを脱出したあと、 rails db:migrate:reset
をすれば戻ります。
todo_app % rails db:migrate:reset
Dropped database 'storage/development.sqlite3'
Dropped database 'storage/test.sqlite3'
Created database 'storage/development.sqlite3'
Created database 'storage/test.sqlite3'
== 20231105023538 CreateTasks: migrating ======================================
-- create_table(:tasks)
-> 0.0008s
== 20231105023538 CreateTasks: migrated (0.0008s) =============================
== 20231108012222 DeviseCreateUsers: migrating ================================
-- create_table(:users)
-> 0.0007s
-- add_index(:users, :email, {:unique=>true})
-> 0.0003s
-- add_index(:users, :reset_password_token, {:unique=>true})
-> 0.0003s
== 20231108012222 DeviseCreateUsers: migrated (0.0014s) =======================
todo_app %
コンソールでの操作はここまでです。
2. バリデーションとは
バリデーションは、モデル側で扱うデーターの制約を掛けていく仕組みで、意図しないデーターの入力を防ぐ為のものです。
すでに、↓こちらで少し記述はしてあるのですが、
ここではもう少し掘り下げて、動きの確認をしてみましょう。
validates
最初に、以前記述したバリデーションの記述を確認します。
app/models/task.rb【確認】
class Task < ApplicationRecord
enum status: { not_started: 0, in_progress: 1, closed: 2 }
validates :title, presence: true, length: { minimum: 3, maximum: 20 }
validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
validates :start_time, presence: true
end
現在は、 title
description
start_time
に対して書かれています。
簡単に今の仕様を書き出すと、
カラム名 | 仕様 | 使うオプション |
---|---|---|
title | 入力は必須 3文字以上20文字以内 |
presence length |
description | 入力は任意 3文字以上500文字以内 |
allow_blank length |
start_time | 入力は必須 | presence |
となっています。
この検証を先程のRailsコンソールで行ってみましょう。
3. errorsとは
errors
とは、バリデーションが実行されたタイミングでインスタンスに記録される項目です。 valid?
メソッド実行時にに記録されます。
valid?
では、コンソールを開きます。
todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001>
新しくインスタンスを作ります。
irb(main):001> Task.new
=>
#<Task:0x00000001085dbef8
id: nil,
title: nil,
description: nil,
start_time: nil,
end_time: nil,
status: "not_started",
created_at: nil,
updated_at: nil>
コンソールは ↑
で履歴を呼び出せますので、呼び出して書き足す感じで進めるとスムーズです。
呼び出して、 valid?
を付け加えます。
irb(main):002> Task.new.valid?
=> false
false
が返ってきました。 この場合、バリデーションに不備があるという事です。
では、その不備を確認していきましょう。
errors
errors
メソッドを使います。 そのために一旦インスタンスを変数に入れてから valid?
errors
を実行していきます。
irb(main):003> task = Task.new
=>
#<Task:0x0000000102e03708
...
irb(main):004> task.valid?
=> false
irb(main):005> task.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=title, type=blank, options={}>, #<ActiveModel::Error attribute=title, type=too_short, options={:count=>3}>, #<ActiveModel::Error attribute=start_time, type=blank, options={}>]>
ActiveModel::Errors
というオブジェクトが表示されています。この中に対象のエラーの情報が入ってきます。
内容は
type=too_short, options={:count=>3}>, #<ActiveModel::Error attribute=start_time, type=blank, options={}>
と、なんとなく分からなくもないですが、ちょっと見にくいですね。
これをもう少し見やすくしましょう。
full_messages
エラーを整形して表示させるには、 モデル名.errore.full_messages
と書きますので、
irb(main):006> task.errors.full_messages
=> ["タイトルを入力してください", "タイトルは3文字以上で入力してください", "開始日を入力してください"]
と、このように書けば、 I18nも適用された状態でバリデーションメッセージを見る事が出来ます。
errors.add
また、この errors
はエラーを追加する事もできます。
irb(main):007> task.errors.add(:title, "ほげほげ")
=> #<ActiveModel::Error attribute=title, type=ほげほげ, options={}>
irb(main):008> task.errors.full_messages
=> ["タイトルを入力してください", "タイトルは3文字以上で入力してください", "開始日を入力してください", "タイトルほげほげ"]
これは、Railsが用意している以外で、カスタムバリデーションの記述などに使えます。(今は使いません)
では、次に指摘された title
と start_time
にデーターを入れていきましょう。
すでに変数にインスタンスを代入していますので、
task.title = "test"
task.start_time = Time.current
のように一つずつ追加しても良いですし、複数項目追加するなら assign_attributes
を使うとすっきり書けます。
irb(main):008> task.assign_attributes(title: "test", start_time: Time.current)
=> nil
これで、再度 valid?
を使うと
irb(main):009> task.valid?
=> true
true
が返ってきました。
これで save
が可能です。
irb(main):010> task.save
TRANSACTION (0.2ms) begin transaction
Task Create (2.2ms) INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id" [["title", "test"], ["description", nil], ["start_time", "2023-11-18 02:06:16.141775"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-18 02:19:40.641577"], ["updated_at", "2023-11-18 02:19:40.641577"]]
TRANSACTION (0.8ms) commit transaction
=> true
検証はここまでです。 exit
でコンソールを終了しておきましょう。
4. エラーメッセージ
ここまでを踏まえて、フォームの入力に対して、バリデーションメッセージを表示する記述をしていきましょう。
エラーメッセージのテンプレート
エラーメッセージをテンプレート化したいので、_error_messages.html.erb
として作っておきましょう。
app/views/layouts/_error_messages.html.erb
<% if model.errors.any? %>
<ul class="alert alert-warning" style="list-style:none;">
<% model.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
インスタンスを model
という変数で渡してくる前提で、errors
でエラー情報に配列の要素が存在すれば、 バリデーションに引っかかっているという事ですので、
errors.any?
でバリデーションエラーが存在する時に full_messages
を取得し、 each
で1つずつ取り出してリスト表示させます。
エラーメッセージの呼び出し側
tasks/_form.html.erb
Taskモデルのフォーム側にこれを呼び出す記述を書いていきましょう。
app/views/tasks/_form.html.erb【確認】
<div class="row">
<div class="col-md-6">
<%= form_with(model: task) do |form| %>
<!-- ここから -->
<% if task.errors.any? %>
<div style="color: red">
<h2><%= pluralize(task.errors.count, "error") %> prohibited this task from being saved:</h2>
<ul>
<% task.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<!-- ここまで -->
<% if params[:action] == "new" || params[:action] == "create" %>
<div class="mb-3">
<%= form.label :title, style: "display: block" %>
<%= form.text_field :title, class: "form-control" %>
</div>
<div class="mb-3">
<%= form.label :description, style: "display: block" %>
<%= form.text_area :description, class: "form-control" %>
</div>
<div class="mb-3">
<%= form.label :start_time, style: "display: block" %>
<%= form.datetime_field :start_time, class: "form-control" %>
</div>
<div class="mb-3">
<%= form.label :end_time, style: "display: block" %>
<%= form.datetime_field :end_time, class: "form-control" %>
</div>
<% end %>
<% if params[:action] == "edit" %>
<div class="mb-3">
<%= form.label :status, style: "display: block" %>
<%= form.select :status, Task.statuses.keys.map {|k| [I18n.t("enums.task.status.#{k}"), k]}, {}, { class: "form-control" } %>
</div>
<% end %>
<div class="mb-3">
<%= form.submit class: "btn btn-primary" %>
</div>
<% end %>
</div>
</div>
この中でもすでに scaffold
が用意している該当コードがあり、
<% if task.errors.any? %>
<div style="color: red">
<h2><%= pluralize(task.errors.count, "error") %> prohibited this task from being saved:</h2>
<ul>
<% task.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
この部分を先程作ったテンプレートの呼び出しに置き換えていきましょう。
上記のコードを、以下のコードに置き換えます。
<%= render "layouts/error_messages", model: form.object %>
引数に model: form.object
と書いています。
model:
で呼び出すテンプレート側で使いたい変数 model
をキーとして渡して、その中身である 該当するインスタンス(@task) をインスタンス変数で渡さずに、
form_with
で使うヘルパーのブロック変数 form
からモデルのインスタンスを取り出す object
で呼び出しています。
一見無駄のような呼び出し方をしていますが、これは、呼び出し元の変数名に依存させなくする事で、テンプレートを汎用的に使うために行うための方法です。
編集後は以下のようになります。
app/views/tasks/_form.html.erb【編集】
<div class="row">
<div class="col-md-6">
<%= form_with(model: task) do |form| %>
<%= render "layouts/error_messages", model: form.object %><!-- こうする -->
<% if params[:action] == "new" || params[:action] == "create" %>
<div class="mb-3">
<%= form.label :title, style: "display: block" %>
<%= form.text_field :title, class: "form-control" %>
</div>
<div class="mb-3">
<%= form.label :description, style: "display: block" %>
<%= form.text_area :description, class: "form-control" %>
</div>
<div class="mb-3">
<%= form.label :start_time, style: "display: block" %>
<%= form.datetime_field :start_time, class: "form-control" %>
</div>
<div class="mb-3">
<%= form.label :end_time, style: "display: block" %>
<%= form.datetime_field :end_time, class: "form-control" %>
</div>
<% end %>
<% if params[:action] == "edit" %>
<div class="mb-3">
<%= form.label :status, style: "display: block" %>
<%= form.select :status, Task.statuses.keys.map {|k| [I18n.t("enums.task.status.#{k}"), k]}, {}, { class: "form-control" } %>
</div>
<% end %>
<div class="mb-3">
<%= form.submit class: "btn btn-primary" %>
</div>
<% end %>
</div>
</div>
では、他のフォームにも適用しておきましょう。
当然 Devise
のテンプレートにも使えますので、
users/registrations/new.html.erb
app/views/users/registrations/new.html.erb【確認】
<h2>Sign up</h2>
<div class="row">
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 col-sm-8 offset-sm-2">
<div class="card">
<div class="card-body">
<%= form_with(model: @user, url: registration_path(resource_name)) do |f| %>
<%= render "users/shared/error_messages", resource: resource %><!-- ここ -->
<div class="mb-3">
<%= f.label :email, class: "form-label" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :password, class: "form-label" %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :password_confirmation, class: "form-label" %>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
</div>
<div class="mb-3">
<%= f.submit "Sign up", class: "btn btn-primary w-100" %>
</div>
<% end %>
</div>
</div>
</div>
</div>
<%= render "users/shared/links" %>
すでに Devise
のエラーメッセージを呼びにいっていますが、これを今作ったものを見るように変更しましょう。
以下のコードに変更します。
<%= render "layouts/error_messages", model: f.object %>
渡す変数のキー model
に対する値は、 form
ではなく f
となっていますね。これは、フォームのテンプレートの問題で scaffold
は form
としているのに対して、 Devise
は f
としてテンプレートを生成しているためです。
scaffoldを使わず書く場合などは、通常 f
としている事が多いように思います。
書き換え後は以下のようになります。
app/views/users/registrations/new.html.erb【編集】
<h2>Sign up</h2>
<div class="row">
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 col-sm-8 offset-sm-2">
<div class="card">
<div class="card-body">
<%= form_with(model: @user, url: registration_path(resource_name)) do |f| %>
<%= render "layouts/error_messages", model: f.object %><!-- こうする -->
<div class="mb-3">
<%= f.label :email, class: "form-label" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :password, class: "form-label" %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :password_confirmation, class: "form-label" %>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
</div>
<div class="mb-3">
<%= f.submit "Sign up", class: "btn btn-primary w-100" %>
</div>
<% end %>
</div>
</div>
</div>
</div>
<%= render "users/shared/links" %>
編集箇所は以上です。
todo_app % bin/dev
でサーバーを起動して確認すると、以下のようになっているはずです。
これで、バリデーションエラーを1箇所で管理出来るようになりました。
5. git
git
ここまでを保存しておきましょう。
todo_app % git add .
todo_app % git commit -m "バリデーションメッセージのテンプレート化"
GitHub
Githubの設定ができているのであれば、同名のリポジトリを作成し、pushしておきましょう。
おわりに
本チャプターはここまでとなります。
次回は
- Enumのバリデーション
- カスタムバリデーション
- 日時のバリデーション
- タイムゾーン
を学びます。
ここまでお疲れ様でした。