背景・目的
- 背景・目的
- Ruby on Railsでアプリケーション開発を行っている
- なぜDB層で
default: ''を指定しない方針をとったのか?という質問をされて回答ができなかったので言語化しておく
前提
- モデルケース
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :bio
t.string :password_digest
t.timestamps
end
end
end
-
ユーザーの情報を管理するUserモデルが、名前、メールアドレス、自己紹介(bio)の3つのカラムを持っている想定
-
新規登録からログインのフローについて
- 新規登録
- ユーザーは新規登録画面から、名前、メールアドレス、パスワードを入力する
- ログイン
- ユーザーはログイン画面から、メールアドレス、パスワードを入力する
- プロフィールについて
- ユーザーはログイン後に任意のタイミングで、名前、メールアドレス、自己紹介(bio)の入力、編集をすることができる
- 新規登録
-
その他
- パスワードは
has_secure_passwordで実装(この記事では解説しない) - https://api.rubyonrails.org/v8.0.0/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password
- パスワードは
nameカラムについて考える
nullを許容する、空文字を許容する
# migration
t.string :name
# model
# バリデーションなし
nameがnullまたは空文字を許容する
どんな状態か
-
DB層ではnull: true(デフォルト)なので、nameカラムにnullが入る - アプリケーション層ではバリデーションがないので、空文字も登録できる
-
nameがnilのユーザーと、nameが""のユーザーが混在する
具体的に困るケース
- ユーザー一覧画面で
nameを表示しようとすると、nilの場合と空文字の場合の両方をチェックする必要がある -
@user.nameがnilの可能性があるため、@user.name.upcaseのようなメソッドを呼ぶとエラーになる - ユーザーを名前で検索する機能が実質使えない
メリット
- 思い浮かばなかったです
デメリット
- 表示時に常に
nilチェックと空文字チェックが必要 - メールアドレスでしか、ユーザーを識別できない
採用するかどうか
しない。メリットがない
nullを許容する、空文字を禁止する
# migration
t.string :name
# model
validates :name, presence: true
nameがnullは許容するが、空文字は禁止する
どんな状態か
-
DB層ではnull: true(デフォルト)なので、nameカラムにnullが入る可能性がある - アプリケーション層では
presence: trueで空文字を弾くが、DB層の制約はない -
Railsのバリデーションを通さずにDBに直接INSERT(例:生のSQLや他のツール)すると、nullが入る
具体的に困るケース
-
Railsコンソールでバリデーションをスキップして作成するとnameがnullのまま保存される -
DBマイグレーションやバッチ処理でバリデーションをスキップすると、nullが入る可能性がある -
@user.name.upcaseのようなメソッド呼び出しでNoMethodErrorが発生する可能性がある - バリデーション抜けたら、空文字が入る
メリット
- 思い浮かばなかったです
デメリット
-
DBにはnullが入る可能性があるため、コード上でnilチェックが必要 - バリデーションで空文字を弾いても、
DB層ではnullが入る可能性がある -
DB層とアプリケーション層で一貫性がない
採用するかどうか
しない。データの統合性がなくなる可能性がある
nullを禁止する、空文字を許容する
# migration
t.string :name, null: false, default: ''
# model
# バリデーションなし
nameがnullは禁止だが、空文字は許容する
どんな状態か
-
DB層ではnull: false, default: ''なので、nameは常に文字列型(nullにはならない) - アプリケーション層ではバリデーションがないので、空文字が登録できる
-
nameが空文字""のユーザーが存在する
具体的に困るケース
- ユーザー一覧画面で名前が空欄のユーザーが表示される
- ユーザーを名前で識別することができない
- 後からユーザーが意図的に名前を空にしたのか、それとも入力していないだけなのかを判定できない
メリット
-
nilチェックは不要(常に文字列)
デメリット
- 空文字の
nameが登録される可能性がある - ユーザーを識別できない
採用するかどうか
完全になしではないが、採用しない
usersテーブルは現実のユーザー(人間)を管理するテーブルである。そのテーブルの中でnameが空文字(名前がない)というのは現実的に考えられないケースであるため。
nullを禁止する、空文字を禁止する
# migration
t.string :name, null: false
# model
validates :name, presence: true
nameがnullも空文字も禁止
どんな状態か
-
DB層ではnull: falseなので、nameカラムにnullが入らない - アプリケーション層では
presence: trueで空文字を弾く -
nameは必ず何か意味のある文字列が入っている状態が保証される
具体的に困るケース
-
nameには必ず何かの値を入れる必要があるため、名前が存在しないという状態を表現するのに工夫がいる。(要件次第)
メリット
-
DB層でデータ整合性を保証できる - コード上で
nilチェックが不要 - 必ず意味のあるユーザー名が登録される
デメリット
-
nameに値は必ず入れなければならない
採用するかどうか
採用する
- 必ず意味のあるユーザー名が登録されることで、業務要件を満たすため。
emailカラムについて考える
nullを許容する、空文字を許容する
# migration
t.string :email
# model
# バリデーションなし
emailがnullまたは空文字を許容する
どんな状態か
-
DB層ではnull: true(デフォルト)なので、emailカラムにnullが入る - アプリケーション層ではバリデーションがないので、空文字も登録できる
-
emailがnilのユーザーと、emailが""のユーザーが混在する
具体的に困るケース
- ログイン機能が動作しない(
emailがnilや空文字では認証できない) - ユーザーに通知メールを送る機能が使えない
-
@user.email.downcaseのようなメソッド呼び出しでNoMethodErrorが発生する
メリット
- 思い浮かばなかったです
デメリット
- 認証機能が動作しない
- データ整合性が保証されない
- 業務要件上、不適切
採用するかどうか
しない。メリットがない
nullを許容する、空文字を禁止する
# migration
t.string :email
# model
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
emailがnullは許容するが、空文字は禁止する
どんな状態か
-
DB層ではnull: true(デフォルト)なので、emailカラムにnullが入る可能性がある - アプリケーション層では
presence: trueで空文字を弾くが、DB層の制約はない -
Railsのバリデーションを通さずにDBに直接INSERTすると、nullが入る
具体的に困るケース
-
Railsコンソールでバリデーションをスキップして作成するとemailがnullのまま保存される -
DBマイグレーションでバリデーションをスキップすると、nullが入る可能性がある -
@user.email.downcaseのようなメソッド呼び出しでNoMethodErrorが発生する可能性がある - ログイン機能が動作しない
- バリデーション抜けたら、空文字が入る
メリット
- 思い浮かばなかったです
デメリット
-
DBにはnullが入る可能性があるため、コード上でnilチェックが必要 - バリデーションで空文字を弾いても、
DB層ではnullが入る可能性がある -
DB層とアプリケーション層で一貫性がない
採用するかどうか
しない。メリットがない
nullを禁止する、空文字を許容する
# migration
t.string :email, null: false, default: ''
# model
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
emailがnullは禁止だが、空文字は許容する
どんな状態か
-
DB層ではnull: false, default: ''なので、emailは常に文字列型(nullにはならない) - アプリケーション層では
formatバリデーションのみで、presenceがない -
emailが空文字""のユーザーが存在する可能性がある
具体的に困るケース
- ログイン画面で
emailが空文字のユーザーではログインできない - メール送信機能が動作しない(空文字のメールアドレスには送信できない)
メリット
-
nilチェックは不要(常に文字列)
デメリット
- 空文字の
emailが登録される可能性がある - 認証機能が動作しない
- 業務要件上、不適切
採用するかどうか
採用しない
- メールが空文字だとログインができない
- ユーザーを一意で判定する値がなくなる
nullを禁止する、空文字を禁止する
# migration
t.string :email, null: false
# model
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
emailがnullも空文字も禁止
どんな状態か
-
DB層ではnull: falseなので、emailカラムにnullが入らない - アプリケーション層では
presence: trueとformatで空文字と無効な形式を弾く -
emailは必ず有効なメールアドレスが入っている状態が保証される
具体的に困るケース
- メールアドレスを空にできない→そんなこと必要?って感じですが
メリット
-
DB層でデータ整合性を保証できる - コード上で
nilチェックが不要 - 必ず有効なメールアドレスが登録される
- 認証機能が安定する
デメリット
- 思い浮かばなかったです
採用するかどうか
採用する
- emailは認証に必須のカラムなので、nullも空文字も許容できない
- 要件を満たす
bioカラムについて考える
nullを許容する、空文字を許容する
# migration
t.text :bio
# model
# バリデーションなし
bioがnullまたは空文字を許容する
どんな状態か
-
DB層ではnull: true(デフォルト)なので、bioカラムにnullが入る - 空文字も許容する
-
bioがnilのユーザーと、bioが""のユーザーが混在する
具体的に困るケース
- 表示などでは常に
nilをチェックする必要がある - データ分析で「自己紹介を登録しているユーザー数」を集計する際、
WHERE bio IS NOT NULL AND bio != ''のように条件が複雑になる
メリット
- 「まだ未入力」と「意図的に空にした」を区別できるかも(
nullと空文字で区別)
デメリット
- コード上で
nilチェックと空文字チェックの両方が必要
採用するかどうか
採用しない
- nilと空文字の両方をチェックする必要があり、コードが煩雑になる
nullを許容する、空文字を禁止する
# migration
t.text :bio
# model
validates :bio, presence: true
bioがnullは許容するが、空文字は禁止する
どんな状態か
-
DB層ではnull: true(デフォルト)なので、bioカラムにnullが入る可能性がある - アプリケーション層では
presence: trueで空文字を弾くが、DB層の制約はない -
DB層ではnull: trueなので、レコード作成時にbioがnullでも保存できる - 一度値を入れたら、その後は空にできない
具体的に困るケース
- ユーザーが画面から操作するときに、後から自己紹介を空で登録することができない
- バリデーションが抜けたら空文字が入る
メリット
- 思い浮かばなかったです
デメリット
-
DBにはnullが入る可能性があるため、コード上でnilチェックが必要 - バリデーションで空文字を弾いても、
DB層ではnullが入る可能性がある -
DB層とアプリケーション層で一貫性がない
採用するかどうか
採用しない
- 一度値を入れたら空にできないという柔軟性のなさがある
- オプショナルな項目としては不適切
nullを禁止する、空文字を許容する
# migration
t.text :bio, null: false, default: ''
# model
validates :bio
bioがnullは禁止だが、空文字は許容する
どんな状態か
-
DB層ではnull: false, default: ''なので、bioは常に文字列型(nullにはならない) - アプリケーション層では長さのバリデーションのみで、空文字は許容する
具体的に困るケース
- 困るケースはなし(業務要件を満たす設計)
- プロフィール画面では
@user.bio.present?で簡単に判定できる - 自己紹介が「まだ未入力」なのか「後から空にした」のか、判定ができない
メリット
- シンプル(
NULLを考えなくていい) - 常に文字列として扱える
- ユーザーは自由に入力/削除できる
- コードがシンプルになる
- 実務で十分
デメリット
- 「未入力」と「意図的に空にした」を区別できない
- ただし、ユーザーから見たら同じ状態なので実務上問題ない
採用するかどうか
採用する
- オプショナルな項目として適切
- nilチェックが不要でコードがシンプルになる
- ユーザーが自由に入力/削除できる柔軟性がある
- 実務上、「未入力」と「意図的に空にした」を区別する必要性は低い
nullを禁止する、空文字を禁止する
# migration
t.text :bio, null: false
# model
validates :bio, presence: true
bioがnullも空文字も禁止
どんな状態か
-
DB層ではnull: falseなので、bioカラムにnullが入らない - アプリケーション層では
presence: trueで空文字を弾く -
bioは必ず何か入力されている状態が強制される
具体的に困るケース
- 新規登録時に自己紹介を入力しないと登録できない
- ユーザーが自己紹介を削除(空にする)ことができない
- 「あとで自己紹介を書く」という運用ができない
メリット
- 必ず自己紹介が登録される
デメリット
- ユーザーが自由に削除できなくなる
採用するかどうか
採用しない
- オプショナルな項目を必須にしてしまうのは、ユーザー体験を損なう
- 新規登録時に自己紹介を強制するのは柔軟性がない
- 後から自己紹介を削除できないのは不便
感想
- データベースにおいて
nullを許容するのか、空文字を許容するのかを整理する良い機会になりました。 - どちらが正解というよりかは、要件にあった適切な選択肢が取れるのか、それを人に説明できるのか?みたいなところを言語化して整理したかったのでよかった。