💪

DBでnullを許容するのか、空文字を許容するのかの備忘録

に公開

背景・目的

  • 背景・目的
    • Ruby on Railsでアプリケーション開発を行っている
    • なぜDB層でdefault: ''を指定しない方針をとったのか?という質問をされて回答ができなかったので言語化しておく

前提

  • 開発環境

    • Ruby on Rails バージョン 8.0.3
    • Ruby バージョン 3.4.4
    • PostgreSQL 15.13
  • モデルケース


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)の入力、編集をすることができる
  • その他

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を許容するのか、空文字を許容するのかを整理する良い機会になりました。
  • どちらが正解というよりかは、要件にあった適切な選択肢が取れるのか、それを人に説明できるのか?みたいなところを言語化して整理したかったのでよかった。

Discussion