今日のエラー
Spring Boot + MyBatis + PostgreSQL で「ログインフォームから送信 → DB 照合」を実装した直後、正しいメール・パスワードを入れてもログインが通らない現象が出ました。
何が起きたか
renderでのデプロイOK、ブラウザでのアクセスOK、見た目OK、バリデーションチェックOK、新規登録画面の実装は大丈夫そう。
今登録したアドレスとパスワードでログイン…あれ、パスワード間違えたのかな?
(数回やり直し、別のアカウントを新規登録してみたりした)やっぱりログインできない!!
と、他の部分は何も問題なさそうに動いているかと思いきや、何かが間違っているようでした。
ログインの判定はこんな感じです。
User user = userMapper.findByEmail(form.getEmail());
if (user == null || !passwordEncoder.matches(form.getPassword(), user.getPasswordHash())) {
// 失敗扱い
}
ユーザー自体は取得できていそうなのに、毎回 else ではなく失敗側に落ちる。
=> 「user.getPasswordHash() が null になっている」のではないか、と当たりを付けました。
原因の一言まとめ
DB の列名(snake_case)と Java のプロパティ名(camelCase)の対応付けが崩れていた。
- DB 列:
password_hash - Java プロパティ:
passwordHash
INSERT 側は #{passwordHash} のように MyBatis が Java 側の名前で参照していたため問題なく動いていました。
ところが SELECT の findByEmail では、結果セットの 列名(password_hash) を Java 側プロパティ(passwordHash) へ自動でマッピングする想定が、設定上効いていなかった。
結果、User 型は返ってくるのに passwordHash だけ null という、いちばん気付きにくい状態になっていました。
どうやって原因にたどり着いたか(手順)
「動かない!」となったときに、思い込みで触り回らずに済む順番です。
-
失敗している地点を特定する
今回はログイン判定のifの中。明らかにここでコケている。 -
その地点の入力値が想定どおりかを確認する
-
userは本当に取れているか?(nullなら DB 検索が失敗) -
user.getPasswordHash()は値が入っているか?(nullなら照合は必ず false)
-
-
「処理ロジック」だけでなく「データの受け渡し」も疑う
- 入力 → コントローラ → サービス → Mapper → SQL → DB → Mapper → Java オブジェクト
- 今回の怪しい所は最後の戻り:SQL 結果 → Java オブジェクトへのマッピング
-
値の流れを「逆向き」に辿る
-
getPasswordHash()←findByEmailの戻り値 ← SQL の結果列 - DB 側で
SELECT password_hash FROM usersを直接打って、値自体は存在することを確認。
=> DB ではなくマッピング側が怪しい、と確定。
-
-
MyBatis のマッピング規約を疑う
- 列
password_hash↔ プロパティpasswordHash - スネーク↔キャメルの自動変換が効いていない可能性。
- 列
ここまで来ると、直す場所が一気に絞れます。
どう直したか
UserMapper.xml の findByEmail で、列名にエイリアスを付けて Java プロパティ名と完全一致させました。
<select id="findByEmail" parameterType="string"
resultType="com.example.demo.domain.User">
SELECT
id,
name,
email,
password_hash AS passwordHash,
introduction,
avatar_image_url AS avatarImageUrl,
created_at AS createdAt,
updated_at AS updatedAt
FROM users
WHERE email = #{email}
LIMIT 1
</select>
ポイントは 1 行だけ:
-
password_hash AS passwordHash
これで MyBatis が 「この列はこの Java プロパティ」 と迷わなくなり、getPasswordHash()が正しい値を返すようになった。
これでログインは通るようになりました。
なぜ MyBatis ではこれが起きやすいのか
MyBatis は「SQL 結果のカラム名」と「Java の setter 名」を見て値を埋めます。
列名とプロパティ名が一致しないとき、いくつかの解決策があります。
-
SQL 側でエイリアスを付ける(今回の方法)
SELECT password_hash AS passwordHash ...- 一番直感的で、後から読み返したときも分かりやすい。
-
<resultMap>を定義する- 列とプロパティの対応を XML 側で表で書いておく方法。
- 関連テーブルや複雑なマッピングがあるなら、こちらの方が拡張しやすい。
-
mybatis.configuration.map-underscore-to-camel-case=trueを設定する-
application.propertiesなどで一括設定。 - 「
snake_caseの列をcamelCaseに自動変換」してくれる。 - 設定したつもりが効いていない/プロジェクトに入っていない、というのが今回のような事故の典型パターン。
-
つまり、password_hash → passwordHash を どこで責任もって変換するか を決めていないと、片側(INSERT は明示)だけ動いて、もう片側(SELECT)は静かに null を返す、という現象になります。
実務目線での今後の対策
-
SELECT には基本エイリアスを付ける癖をつける
created_at AS createdAtのように 1 行追加するだけで、将来の自分や他人を救う。 -
「自動変換に頼る方針」と「手動で書く方針」をプロジェクトで決めておく
迷うとバグになる。map-underscore-to-camel-caseを使うかどうかは、入った時点で確認。 -
nullバグは「結果オブジェクト全体」でなく「フィールド単位」で疑う
「ユーザーは取れているのにパスワードだけnull」のような部分欠損が起きる。 -
DB と Java の名前規約を最初に揃えておく
どうしても揃わない箇所だけ、エイリアスやresultMapで受け止める設計にする。 -
デバッグのときは「SQL ログを表示する設定」を一時的に有効にする
実際に発行されている SQL と、戻ってきている列名を見るのが一番早い。
学び(自分への指差し確認)
- 動かないとき、まず 「処理」ではなく「データの受け渡し」 を疑う。
- マッピング層のバグは エラーが派手に出ないので、
nullを見たら一旦止まる。 -
INSERTで動いていることは、SELECTで動く保証にはならない。 - 1 行のエイリアスで、再現性のある修正ができる。
このパターンは MyBatis を使っている限り何度も出会うので、見つけ方の手順を体に入れておくと、次の課題では一瞬で直せます。