はじめに
概要
この章では、Ruby on Railsでのアプリケーション開発において必要な「バリデーション」と「アソシエーション」という2つの基本的な概念について学びます。簡単に言うと、バリデーションはデータが正しいかどうかをチェックする機能で、アソシエーションは異なるデータ(モデル)がどのように関連しているかを定義する機能です。
この章を通して、Railsアプリケーションでデータをうまく管理し、整理する方法を学びます。これらの概念はRailsでの開発において非常に重要なので、しっかりと理解しておくことが大切です。
目標
- Active Recordのバリデーション機能を理解し、データの整合性を保証する方法を学ぶ。
- アソシエーションを定義して、異なるモデル間の関連付けを実装する。
- データの関連性を活用し、Railsアプリケーションのデータ構造をより効果的に管理するスキルを習得する。
必要な前提条件・事前準備
この章では、Railsの更に高度なデータベース操作について学習します。しかし、始める前にいくつかの前提条件があります。新しいことを学ぶ上でとても大切な土台になります。
- 「Railsでのデータベース設計と操作を学ぼう」を完了していること
- 「Railsでデータ操作の基本(CRUD)をマスターしよう」を完了していること
これらの前提条件を満たしていることを確認した上で、次に進んでください。
バリデーション(検証)
バリデーション(validation)とは、入力されたデータが有効であるかを検証する仕組みのことです。Webアプリケーションでは、さまざまな場所からデータを受け取りますが、バリデーションはデータベースに書き込む前に「データが有効かどうか」を検証します。
例えば、お問い合わせフォームの必須項目に対して、データの存在性(データがあるべき箇所に存在しているか)を検証することができます。1項目でも未入力があった場合は、以下のように送信されず、データベースに保存されません。
このような検証処理を1から実装するのは大変です。Active Recordのバリデーション機能を利用することで、シンプルなコードで検証を定義することができます。
Active Recordのバリデーション機能
CRUDで学習しましたが、RailsではActiveRecordが提供するメソッドを利用することで、オブジェクトを操作するようにデータ操作が可能でしたよね。
1
INSERT INTO users(name, age) VALUES('山田花子', 34);
1
2
user = User.new(name: "山田花子", age: 34)
user.save
saveメソッドは、モデルのインスタンスをデータベースに保存するメソッドですが、Active Recordのバリデーション機能を活用すると、モデルのインスタンスがデータベースに書き込まれる前に、インスタンスの状態を検証することができます。
saveメソッドを実行する際に、自動的にインスタンスの状態が検証されて、インスタンスが有効な場合のみデータベースに保存されます。
バリデーションが設定されている場合、saveメソッドはインスタンスの状態を検証し、有効な場合にのみデータベースへの保存が行われるようにできるんだね!
例えば、Userモデルクラスのname
属性とage
属性に対して「値が空ではないか」という存在性のバリデーションを設定しているとします。
saveメソッドを実行する際、以下のようにインスタンスの状態が検証されます。インスタンスのname
属性とage
属性の値が空ではない場合、インスタンスが有効となり、データベースに保存されます。
インスタンスのname
属性とage
属性の値が空の場合、バリデーションは失敗となり、インスタンスは無効となります。無効なインスタンスは、データベースに保存されません。
例えば、インスタンス生成時にname
属性とage
属性に対して、空であるnil
をセットした場合は、以下のようにデータベースにインスタンスは保存されません。
バリデーションが設定される場合は、save
メソッドを実行する際に自動的にインスタンスの状態が検証されます。save
メソッドの他にも、create
メソッドやupdate
メソッドを実行する際に行われます。
バリデーションを行う主なメソッド | 無効な場合の挙動 |
---|---|
saveメソッド | INSERT操作を行わず、false を返す |
createメソッド | INSERT操作を行わず、そのインスタンスを返す |
updateメソッド | UPDATE操作を行わず、false を返す |
それでは、バリデーションを設定する方法を学んでいきましょう
バリデーションの定義
バリデーションは、モデルのクラスに定義します。
モデルクラス内にvalidates
メソッドを利用して、「検証対象の属性」と「検証内容」を設定することができます。
1
2
3
class クラス名 < ApplicationRecord
validates(:属性名, 検証名: true)
end
validatesメソッドの検証名: true
のように、メソッドの最後の引数がハッシュの場合は、括弧を付けなくても問題ありません。
多くの場合は、以下のように括弧を外して記述します。
1
2
3
class クラス名 < ApplicationRecord
validates :属性名, 検証名: true
end
先ほどの具体例のように、Userモデルのname
属性とage
属性に対して「値が空ではないか」という存在性(presence)の検証を行う場合は、以下のように定義します。
1
2
3
4
class User < ApplicationRecord
validates :name, presence: true
validates :age, presence: true
end
上記のバリデーションをUserモデルのクラスに定義すると、以下のようにsave
メソッドを実行する際、自動的にインスタンスのname
属性とage
属性の値が空ではないか検証を行うことができます。
Userモデルのクラスに存在性のバリデーションを定義していない場合、save
メソッドを実行したとしても、インスタンスの属性に対して値が空ではないかの検証は行われません。
1
2
class User < ApplicationRecord
end
よく使われる「存在性」「一意性」「長さ」の検証を設定します。
存在性の検証
存在性(presence)は、指定した属性の値が空ではないことを検証します。
Departmentモデルのname
属性、Employeeモデルのname
属性に設定してみます。
属性には、以下のいずれかの値がセットされる場合にインスタンスが無効になります。
nil
- 空文字(
''
) - 空白文字(
' '
)
rails c
コマンドで起動させましょう以下のコマンドを実行して、コンソールを起動してください。
1
cd ~/environment/employee_management
1
rails c
上記のコマンドを実行すると、以下のように表示されます。
Departmentモデルにバリデーションを設定する前でも、name
属性にnil
をセットした場合、データベースに保存されません。
以下のコードをコンソールに記述し、実行しましょう。
1
2
department = Department.new(name: nil)
department.save
上記を実行すると、以下のようなエラーが発生します。
departmentsテーブルのname
カラムには「非NULL制約」が設定されるので、そもそもNULL
を格納することができません。
非NULL制約を設定してるのに、モデルのバリデーションって必要なの?
非NULL制約はNULL
を禁止するけど、空文字や空白文字は保存されてしまうんだ。だからモデルのバリデーションは必要だよ!
以下のコードをコンソールに記述し、実行しましょう。
1
2
department = Department.new(name: " ")
department.save
上記を実行すると、以下のようにINSERT操作が行われてtrue
が返ります。
phpMyAdminを再読み込みして、departments
テーブルにname
カラムの値が空白文字のレコードが1件挿入されていることを確認しましょう。
presence: true
を設定しましょう空文字や空白文字もデータベースに保存できないように、Departmentモデルのname属性にpresence: true
を設定しましょう。
1
2
3
class Department < ApplicationRecord
validates :name, presence: true
end
reload!
と打ち込み、モデルの最新情報を反映させましょうコンソールを立ち上げている最中にモデルの内容を変更した場合、コンソールにはすぐに反映されません。コンソールにreload!
と打ち込むことで、再起動せずに最新のモデル情報を反映させることができます。
コンソールにreload!
と打ち込み、実行しましょう。
1
reload!
以下のようにtrue
を返せば、再読み込みできています。
以下のコードをコンソールに記述し、実行しましょう。
1
2
department = Department.new(name: " ")
department.save
上記を実行すると、以下のようにINSERT操作が行われず、false
が返ります。
Departmentモデルのname
属性にpresence: true
を設定したことで、saveメソッドが実行される際に自動でインスタンスのname属性の値が空ではないかを検証します。
name属性には空白文字をセットしたので、インスタンスは無効となり、データベースに保存されません。
presence: true
を設定しましょう1
2
3
class Employee < ApplicationRecord
validates :name, presence: true
end
起動中のコンソールにEmployeeモデルの最新情報を反映させるために、reload!
と打ち込み、実行しましょう。
1
reload!
以下のコードをコンソールに記述し、実行しましょう。
1
2
employee = Employee.new(name: '')
employee.save
Employeeモデルのインスタンスには、name属性の値を空文字(''
)でセットしたので、インスタンスは無効となり、INSERT操作が行われずfalse
が返ります。
一意性の検証
一意性(uniqueness)は、指定した属性の値が一意で重複しないことを検証します。このバリデーションは、「指定した属性と同じ値を持つ既存のレコード」がモデルに対応するテーブルにあるかどうかを調べてくれます。
一意性の検証は、validatesメソッドの引数にuniqueness: true
を指定します。
1
2
3
class クラス名 < ApplicationRecord
validates :属性名, uniqueness: true
end
それでは、Departmentモデルの部署名に対して、一意性の検証が行われるように設定してみます。
name
属性にuniqueness: true
を追加しましょうまずは、以下のコードをコンソールで実行し、部署名が'営業'
の新しいレコードをdepartmentsテーブルに挿入しましょう。
1
2
department = Department.new(name: '営業')
department.save
次にDepartmentモデルのname
属性にuniqueness: true
を追加し、一意性の検証が行われるように設定しましょう。
1
2
3
class Department < ApplicationRecord
validates :name, presence: true, uniqueness: true
end
起動中のコンソールにDepartmentモデルの最新情報を反映させるために、reload!
と打ち込み、実行しましょう。
1
reload!
最後に部署名の一意性の検証が行われるか確認してみます。
departmentsテーブルには、部署名が'営業'
のレコードがすでに存在するので、同じ値を保存しようとすると、一意性の検証により、インスタンスは無効になり、データベースには保存されないはずです。
以下のコードを実行し、一意性の検証が行われるか確かめてみましょう。
1
2
department = Department.new(name: '営業')
department.save
上記を実行すると、以下のように裏側では「departmentsテーブルに同じ値を持つレコードがあるかどうか」を調べるSQL文が発行されます。
すでに'営業'
を持つレコードは存在しているので、インスタンスは無効となり、データベースに保存されずに、false
を返します。
長さの検証
長さ(length)の検証は、指定した属性の値の長さを検証します。
長さの検証は、validatesメソッド
の引数にlength: {オプション}
を指定します。
1
2
3
class クラス名 < ApplicationRecord
validates :属性名, length: { オプション }
end
オプションは、長さの制限を設けることができます。
オプション | 説明 | 例 |
---|---|---|
:minimum | 指定した値より小さな値を取れない | length: { minimum: 2 } 2より小さな値を取れない(1は取れない) |
:maximum | 指定した値より大きな値を取れない | length: { maximum: 50 } 50より大きな値を取れない(51は取れない) |
is | 指定した値と等しいこと | length: { is: 15 } 15と等しいこと(16は取れない) |
departmentsテーブルとemployeesテーブルのnameカラムには、文字列の最大幅として30
が指定されています。各モデルのname属性にlength: { maximum: 30 }
を追加し、長さの検証が行われるように設定してみます。
length: { maximum: 30 }
を追加しましょうDepartmentモデルのname属性にlength: { maximum: 30 }
を追加し、長さの検証が行われるように設定しましょう。
1
2
3
class Department < ApplicationRecord
validates :name, presence: true, uniqueness: true, length: { maximum: 30 }
end
起動中のコンソールにDepartmentモデルの最新情報を反映させるため、reload!
と打ち込み、実行しましょう。
1
reload!
31
文字の部署の名前が長すぎることを検証しましょう31文字の文字列は、'a' * 31
で作成します。
length
メソッドを呼び出して、'a' * 31
が31文字の文字列を作ることを確認してみます。コンソールで以下のコードを記述し、実行しましょう。
1
2
'a' * 31
('a' * 31).length
上記を実行すると、以下のように'a' * 31
が31文字の文字列の長さであることが確認できます。
Departmentモデルのインスタンスに対して、name
属性の値を31
文字の文字列をセットして、検証してみましょう。
コンソールで以下のコードを記述し、実行しましょう。
1
2
3
exceed_max_validation_length = 'a' * 31
department = Department.new(name: exceed_max_validation_length)
department.save
上記を実行すると、31文字の部署の名前が長すぎることが検証されます。インスタンスは無効となり、データベースに保存されずに、false
を返します。
length: { maximum: 30 }
を追加しましょうEmployeeモデルのname属性にもlength: { maximum: 30 }
を追加し、長さの検証が行われるように設定しましょう。
1
2
3
class Employee < ApplicationRecord
validates :name, presence: true, length: { maximum: 30 }
end
Employeeモデルの最新情報を反映させるため、コンソールにreload!
と打ち込み、実行しましょう。
1
reload!
Employeeモデルのインスタンスに対して、name
属性の値を31
文字の文字列をセットして、検証してみましょう。
コンソールで以下のコードを実行し、false
を返すことを確かめましょう。
1
2
3
exceed_max_validation_length = 'a' * 31
employee = Employee.new(name: exceed_max_validation_length)
employee.save
エラーメッセージ
バリデーションが行われた後にerrors
メソッドを利用すると、検証エラーに関する情報を取得することができます。
バリデーションが行われてインスタンスが無効だった場合、そのモデルのインスタンスに対してerrors.full_messages
メソッドを呼び出すと、全てのエラーメッセージを配列で取得できます。
1
モデルクラスのインスタンス.errors.full_messages
例えば、Userモデルのname属性とage属性に「値が空ではないか」という存在性の検証を定義していたとします。
1
2
3
4
5
class User < ApplicationRecord
#name属性とage属性の値が空ではないか検証するように設定
validates :name, presence: true
validates :age, presence: true
end
Userモデルのインスタンスに対して、name属性とage属性の値をnil
にセットし、saveメソッドを呼び出すとバリデーションが自動で実行されてfalse
を返します。
インスタンスは無効なので、以下のようにerrors.full_messages
メソッドを呼び出すと、エラーメッセージを取得できます。
1
2
3
4
5
6
user = User.new(name: nil, age: nil) #nilでインスタンスを生成する
user.save #バリデーションが自動で実行される
=> false #インスタンスが無効なので、エラーメッセージが追加される
user.errors.full_messages #エラーメッセージを取得する
=> ["Name can't be blank", "Age can't be blank"]
有効なインスタンスに対してerrors.full_messages
メソッドを呼び出した場合、以下のようにエラーメッセージがない空の配列([]
)を返します。
1
2
3
4
5
6
user = User.new(name: '山田花子', age: 34)
user.save #バリデーションが自動で実行される
=> true #インスタンスは有効
user.errors.full_messages #エラーメッセージを取得する
=> [] #インスタンスは有効なので、空配列が返る
それでは、エラーメッセージを取得してみましょう!
Departmentモデルのname
属性では、以下のバリデーションを設定していましたね。
1
2
3
class Department < ApplicationRecord
validates :name, presence: true, uniqueness: true, length: { maximum: 30 }
end
まずは、有効になるインスタンスをデータベースに保存し、エラーメッセージが[]
を返すことを確認しましょう。
以下のコードをコンソールで実行してください。
1
2
3
department = Department.new(name: '開発')
department.save
department.errors.full_messages
上記のdepartment
は有効なのでdepartment.save
を実行すると、以下のようにtrue
を返します。有効なインスタンスでは、エラーメッセージがなく[]
を返します。
無効なインスタンスに対してerrors.full_messages
メソッドを呼び出した場合、エラーメッセージを含む配列を返します。
以下のコードをコンソールで実行して、エラーメッセージを取得しましょう。
1
2
3
department = Department.new(name: nil)
department.save
department.errors.full_messages
上記のdepartmentのname
属性はnil
で、インスタンスは無効です。department.save
を実行すると、以下のようにfalse
を返します。無効なインスタンスでは、エラーメッセージを含む配列を返します。
アソシエーション(関連付け)
Active Recordには、「アソシエーション」と呼ばれる機能があります。アソシエーションとは、モデル同士の関連付けを行う機能のことです。
テーブル同士の関連付けは「外部キー制約」の設定によって、すでに定義されています。
このデータベース側のテーブル同士の関連付けは、Railsのモデル側には対応していません。
テーブル同士の関連付けだけではなく、アソシエーションでモデル同士の関連付けまで行うと、複数のテーブルにまたがる操作が直感的なコードで記述できるようになります。
開発
の部署に所属している社員の情報を取得する場合で比較してみます。
アソシエーションでモデル同士の関連付けは行わず、テーブル同士の関連付けだけの場合は、以下のようにdepartmentsテーブルに対応するDepartmentモデルでfind
メソッドを利用して、id
が1
の開発のレコードを取得します。
department.id
は、開発のレコードの主キーの値(1
)を取得できます。whereメソッドに外部キーを指定してid
が1
の開発部に属している社員を取得します。
1
2
department = Department.find(1)
employees = Employee.where(department_id: department.id)
アソシエーションでモデル同士の関連付けを行うと、以下のように外部キーを機にすることなくdepartment.employees
だけで、開発の部署に所属している社員を全て取得することができます。
1
2
department = Department.find(1)
employees = department.employees
アソシエーションの定義
アソシエーションは、モデルのクラスに定義します。モデル同士の関連付けの種類はいくつかありますが、belongs_to
とhas_many
がよく使われます。
has_many
の場合、:関連モデル名
は複数形にします。
1
2
3
class クラス名 < ApplicationRecord
belongs_to :関連モデル名
end
1
2
3
class クラス名 < ApplicationRecord
has_many :関連モデル名
end
アソシエーションを定義する場合、テーブル同士の関係性を整理しておく必要があります。
belongs_toの関係性
employeesテーブルとdepartmentsテーブルの関係性は、以下のように複数の社員が各部署に必ず属しています。社員は部署がなければ存在しません。
社員は必ずどれか1つの部署に属している(Employee belongs to a Department)関係性です。このような関係性の場合、Employeeモデルのクラスにbelongs_to
を使います。belong toは「〜に属する」という意味です。
belong to
を利用して、EmployeeモデルとDepartmentモデルを関連付けましょうEmployeeモデルは、Departmentモデルに属す関係です。この関係性でモデル同士を関連付けるには、Employeeモデルのクラスにbelongs_to :department
を指定します。
Employeeモデルのクラスにbelongs_to :department
を追加しましょう。
1
2
3
4
class Employee < ApplicationRecord
belongs_to :department
validates :name, presence: true, length: { maximum: 30 }
end
has_manyの関係性
belongs_to
のときは、複数の社員は1つの部署に属すという関係でしたが、部署側からはどのようにみえるでしょうか。
部署側側から社員をみると、1つの部署は複数の社員を持つ(Department has many Employees)関係性です。このような関係を1:n
と呼びます。部署1つに対して社員は0以上あるので、n
と表現しています。
1:n
の関係性の場合、Department
モデルのクラスにhas_many
を使います。
has_many
を利用して、DepartmentモデルとEmployeeモデルを関連付けましょう1つのDepartmentモデルは複数のEmployeeモデルを持つ関係です。この関係性でモデル同士を関連付けは、Departmentモデルのクラスにhas_many :employees
を指定します。
Departmentモデルのクラスにhas_many :employees
を追加しましょう。
1
2
3
4
class Department < ApplicationRecord
has_many :employees
validates :name, presence: true, uniqueness: true, length: { maximum: 30 }
end
has_many
の関連モデル名は、複数形になるので注意してください。
Departmentモデル経由でEmployeeモデルの情報を取得
アソシエーションで、DepartmentモデルとEmployeeモデルの関連付けをしたことで、Departmentモデル経由でEmployeeモデルの情報を取得することができます。
1
2
3
4
5
6
7
class Department < ApplicationRecord
has_many :employees
end
class Employee < ApplicationRecord
belongs_to :department
end
rails db:migrate:reset
を利用して、データベースを作り直しましょうターミナルで以下のコマンドを実行して、テーブルを作り直しましょう。
※ rails c
で起動したコンソールではなく、ターミナルで実行します。
1
cd ~/environment/employee_management
1
rails db:migrate:reset
上記を実行すると、以下のようにデータベースの削除、作成、マイグレーション実行までされます。
コンソールに最新情報を反映させるため、起動中のコンソールにreload!
と打ち込み、実行しましょう。
※ターミナルではなく、rails cコマンドで起動させたコンソールで行います。
1
reload!
departmentsテーブルに挿入するデータは、以下の通りです。
id | name |
---|---|
1 |
開発 |
2 |
営業 |
まずは、以下のコードをコンソールで実行し、departmentsテーブルに上記のレコードを挿入しましょう。
※ターミナルではなく、rails c
コマンドで起動させたコンソールで行います。
1
2
3
4
Department.create([
{ name: '開発' },
{ name: '営業' }
])
employeesテーブルに挿入するデータは、以下の通りです。
id | name | birthday | department_id |
---|---|---|---|
1 |
田中太郎 |
1980-10-22 |
1 |
2 |
山田花子 |
1983-08-20 |
1 |
3 |
高橋一朗 |
1986-06-16 |
2 |
4 |
伊藤晴子 |
1987-10-01 |
1 |
5 |
鈴木二郎 |
1990-01-17 |
2 |
6 |
山口冬子 |
1993-05-12 |
2 |
employeesテーブルに複数レコードを挿入するために、以下のコードをコンソールに記述し、実行しましょう。
1
2
3
4
5
6
7
8
Employee.create([
{ name: '田中太郎', birthday: '1980-10-22', department_id: 1 },
{ name: '山田花子', birthday: '1983-08-20', department_id: 1 },
{ name: '高橋一朗', birthday: '1986-06-16', department_id: 2 },
{ name: '伊藤晴子', birthday: '1987-10-01', department_id: 1 },
{ name: '鈴木二郎', birthday: '1990-01-17', department_id: 2 },
{ name: '山口冬子', birthday: '1993-05-12', department_id: 2 }
])
コンソールで以下のコードを実行し、Employeeモデルの情報を取得しましょう。
1
2
department = Department.find(1)
department.employees
上記を実行すると、以下のように開発に所属する社員の情報が取得することができます。
開発に所属する社員は、以下のように田中太郎さん、山田花子さん、伊藤晴子さんです。
この章のまとめ
この章では、Ruby on Railsの重要な概念である「バリデーション」と「アソシエーション」について学びました。
バリデーションによって、アプリケーションが受け取るデータの正確性と有効性を保証する方法を理解しました。アソシエーションの部分では、異なるモデル間の関連を定義する方法を学びました。これにより、アプリケーション内でデータ間の複雑な関係を効率的に扱うことができるようになります。
この章をもう一度学習したい方のみ、以下のコマンドを実行して環境をリセットし、改めて「バリデーション(検証)」から取り組んでみてください。
1
cd ~/employee_management
1
rails db:migrate:reset
この記事のまとめ
- バリデーションとは、入力されたデータが有効であるかを検証する仕組みのこと
- アソシエーションとは、モデル同士の関連付けを行う機能のこと
この記事で学んだことをTwitterに投稿して、アウトプットしよう!
Twitterの投稿画面に遷移します