1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【NULLの恐怖】Javaの NullPointerException と SQLの NULL に挟まれて爆破した話

1
Posted at

プログラミングを学ぶ中で、誰もが一度は叩き落される絶望の罠、「NULL(ヌル)」。

  • Javaでおなじみの全エンジニアの敵 NullPointerException (NPE)
  • SQLにおける独特な「三値論理」の NULL
  • JavaScriptの undefined と null の曖昧な境界

それぞれのレイヤーで「値が存在しない状態」の扱い方が異なるため、バックエンドとデータベースの連携部分で意図しないデータの消失やバグが連発する大事故を経験しました。

今回は、私が過去にやらかした「NULLにまつわる不具合の連発劇」と、そこから学んだ各レイヤーでのNULLとの正しい付き合い方についての考察をまとめます。


事故現場1:SQLの「三値論理」を理解しておらず、データが消失した

状況

会員テーブル(users)から、「退会フラグ(deleted_at)が立っていないアクティブなユーザー」を取得するSQLを書いた時のことです。

-- 退会日時が入っていないユーザーだけを取得したい
SELECT * FROM users 
WHERE deleted_at <> '2026-01-01 00:00:00';

「よし、退会していないユーザー(deleted_at が NULL のユーザー)が取れてるな!」と思い込み、そのまま本番へ投入。

発生した悲劇

いざ検索クエリを実行すると、**アクティブユーザーが1件も取得できない(結果が0件になる)**という事態が発生しました。

SQLの世界は TRUE / FALSE の二値ではなく、**TRUE / FALSE / UNKNOWN の「三値論理」**で動いています。
NULL は「値が存在しない(不明)」であるため、<>(等しくない)や =(等しい)などの比較演算子を使った瞬間、評価結果がすべて UNKNOWN になり、WHERE 句の条件から除外されてしまっていたのです。

考察と反省

  • SQLの NULL は値ではない: 「空文字」や「0」とは違い、「判定不能」であることを忘れていた。
  • IS NULL / IS NOT NULL の徹底: 比較演算子ではなく、専用の述語を使わなければならない。

事故現場2:Javaの String.valueOf(null) が生み出した「文字列 "null"」の罠

状況

Javaのバックエンド処理で、DBから取得した値(NULL の可能性があるフィールド)を文字列に変換して外部APIへ送信、またはフロントへ渡す処理を実装しました。

「NPE(NullPointerException)を起こしたくないから、安全に文字列変換しよう!」と考え、String.valueOf() を使いました。

// DBから取得した値(nullが入っている)
String categoryCode = dbResult.getCategoryCode(); 

// NPEを防ぐために String.valueOf を使用
String safeCategory = String.valueOf(categoryCode); 

if (safeCategory != null) {
    // nullじゃないから処理を実行!
    processCategory(safeCategory); 
}

発生した悲劇

NPEは出ず、一見正常に動いているように見えました。
しかし、データベース側で categoryCode が NULL だったデータに対して、なぜか**「存在しないカテゴリ "null" が指定されました」**という謎のエラーが多発。

実は String.valueOf(null) は、null チェックを行って空文字や null を返すのではなく、文字列の "null"(4文字のString)を返してしまうという罠仕様があったのです。

結果として、safeCategory には文字列の "null" が入り、if (safeCategory != null) のチェックを悠々とスルーして後続の処理を破壊していました。


なぜこの悲劇は繰り返されるのか?(全体の考察)

システム全体(フロント〜バックエンド〜DB)を見渡したとき、「値が存在しない」という状態の表現方法が分散していることが混乱の根本原因です。

各レイヤーにおける「値がない状態」の解像度

レイヤー 表現 特徴・罠
SQL (DB) NULL 三値論理。= や <> で比較すると壊れる。IS NULL が必須。
Java (Backend) null 参照がない状態。触った瞬間に NullPointerException が発生。
JavaScript (Frontend) null / undefined 定義されていない (undefined) と 空である (null) が混在。

開発者が「たぶん空だろう」という感覚値で各レイヤーを跨いでデータを流すと、どこかで意味論(セマンティクス)の不一致が起きてバグが跳ね返ってきます。


失敗から得た「NULLと戦うためのアクションプラン」

同じ失敗を繰り返さないために、私は以下の設計指針を徹底するようにしました。

1. DB設計レベルで可能な限り NOT NULL 制約をつける

根本的な解決策として、**「そもそもテーブルに NULL を入れない」**というアプローチです。

  • デフォルト値(DEFAULT '' や DEFAULT 0)を設定できるカラムは、原則 NOT NULL にする。
  • 「NULL が入る余地」を減らすことで、SQLの三値論理の罠を回避する。

2. Java側では Optional や Objects.requireNonNullElse を使う

null を生のまま持ち回すのをやめ、Java 8以降の標準機能を活用します。

// String.valueOf(null) は危険。"null" という文字列になる
// 代わりに Objects.toString を使うとデフォルト値を指定できる
String category = Objects.toString(dbResult.getCategoryCode(), "");

// Optionalを使って明示的に値の有無を扱う
Optional.ofNullable(dbResult.getCategoryCode())
        .ifPresent(code -> processCategory(code));

3. フロントに渡す前にレスポンス(DTO)で揺らぎを吸収する

DBから取れた NULL をそのままJSON化してフロントに投げると、JS側で null になったり "null" になったりして混ざります。
バックエンドの境界線(Controller / DTO)で、「空文字 "" に統一する」あるいは「キー自体を含めない」といったルールを明確に統一するようにしました。


おわりに

「NULL」は、一見単純に見えてプログラミング言語やDBの根本思想が色濃く出るテーマです。

  • SQLは IS NULL で判定する
  • Javaで安易に String.valueOf() を信じない
  • レイヤーの境界線で「空」の定義を揃える

この基本を守るだけで、システム全体の堅牢性は劇的に上がると痛感しました。

皆さんも「NULLに弄ばれて夜を明かした」思い出があれば、ぜひコメントで教えてください!

株式会社ONE WEDGE

【ITエンジニアに、IT業界に貢献する企業】
株式会社ONE WEDGEは、Webシステム開発・SES・AI/DX支援を行うIT企業です。生成AIを活用した業務効率化や次世代システム開発にも注力しており、企業の課題解決だけでなく、エンジニア一人ひとりの成長にも本気で向き合っています。また、技術は「一人で学ぶもの」ではなく「仲間と成長するもの」と考え、社内外でのコミュニティづくりにも力を入れています。
https://onewedge.co.jp/

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?