YovStudio

article 読むトレ #06:いつのまに?汚染されるDB

公開日: 2025-06-21著者: Yov in YovStudio

今回のお題コード

AI 「ユーザーのプロフィールを更新する処理を作成しました。どうですか?」

// 表示用に名前を整形する関数
function formatUserName(user) {
  user.displayName = `${user.lastName} ${user.firstName}`;
  return user.displayName;
}

// 画面表示を更新する関数(簡略化のためログ表示で代替)
function updateDisplay(displayName) {
  console.log(`画面表示を更新: ${displayName}`);
}

// 完了メッセージを表示する関数(簡略化のためログ表示で代替)
function showSuccessMessage(displayName) {
  console.log(`${displayName}さんのプロフィールが更新されました。`);
}

// DBからユーザー情報を取得する関数(固定データを返しているがDBから取得したと仮定する)
function getUserFromDB(userId) {
  return { id: userId, firstName: '太郎', lastName: '山田' };
}

// DBにユーザー情報を保存する関数(保存されるはずのデータをログに表示して代替)
function saveUserToDB(user) {
  console.log('DBに保存したデータ: ')
  console.log(user);
}

// ユーザープロフィールを更新する関数
function updateUserProfile(userId, updates) {
  const user = getUserFromDB(userId);
  if (!user) {
    // ユーザー情報が存在しない場合は何もせず終了
    return;
  }

  // プロフィールの更新
  if (updates.firstName) {
    user.firstName = updates.firstName;
  }
  if (updates.lastName) {
    user.lastName = updates.lastName;
  }

  // 画面表示用に名前を取得
  const displayName = formatUserName(user);

  updateDisplay(displayName);

  saveUserToDB(user);

  showSuccessMessage(displayName);
}

updateUserProfile('user123', {
  firstName: 'Taro',
  lastName: 'Yamada'
});

どう動く?

さっそく読んでみましょう。 このコード、最終的にどんなデータがDBに保存されるでしょうか?

以下のようなサイトで動かしてみてもよいです。

気になる動き

実行すると、こうなります。

画面表示を更新: Yamada Taro
DBに保存したデータ: 
{
  id: 'user123',
  firstName: 'Taro',
  lastName: 'Yamada',
  displayName: 'Yamada Taro'
}
Yamada Taroさんのプロフィールが更新されました。

displayName がいつのまにか保存されていることに気づいたでしょうか?

DBから取得したもともとのユーザー情報には displayName は含まれていません。 以下の通り、idfirstNamelastName だけでしたね。

// DBからユーザー情報を取得する関数(固定データを返しているがDBから取得したと仮定する)
function getUserFromDB(userId) {
  return { id: userId, firstName: '太郎', lastName: '山田' };
}

処理の流れを見ても displayName は表示用に取得しているだけで、特にDBに追加したいような意図は読み取れません。
ということは、本来であれば次のような displayName のないデータが保存されるはずです。

{
  id: 'user123',
  firstName: 'Taro',
  lastName: 'Yamada'
}

ではなぜ displayName が追加されてしまったのでしょう?

原因は「意図しないデータの書き換え」

以下のコードをよく見てみましょう。

// 表示用に名前を整形する関数
function formatUserName(user) {
  user.displayName = `${user.lastName} ${user.firstName}`;
  return user.displayName;
}

lastNamefirstName をつなげて displayName を返しています。
一見正しいように見えますが、実はuser.displayName に値を入れてしまっていることが問題です。

これは formatUserName() の引数 user参照渡しと呼ばれる形で呼び出し元から渡されるため発生する副作用と呼ばれる現象で、user のデータを変えてしまうと呼び出し元にもその変更が発生してしまいます。

副作用とは、「関数の外の世界(変数や状態)に影響を与えること」です。たとえば今回のように、引数のオブジェクトを書き換えることなどが該当します。

この副作用により、user オブジェクトに displayName が追加され、そのまま saveUserToDB(user) に渡されたことで、意図しない形で保存されてしまったわけです。

今回のケース、一見「うっかり」で済みそうですが、実は実務でもたまに見るデータ汚染の入り口なんです。

これの何が問題?

基本的に、知らないところでいつの間にかデータが変わっているコードはバグの温床になります。

今回のコードはまだ短いので、全体を注意深く見れば気づけるかもしれません。 しかし実務においてはコードが長く複雑化し、ファイル数も多くなります。必要がなければ関数の中身まではいちいち見なかったりもしますし、常に全体を確認するのは現実的ではありません。

そんな状態で以下の呼び出しを見た場合、user が内部で更新されているなんて、私なら全く気づかないでしょう。
user を元に、ただ displayName を作って返してくれるだけにしか見えないからです。

  // 画面表示用に名前を取得
  const displayName = formatUserName(user);

気づかないままコードを修正したりすることになれば、なかなかうまく動かなかったりして苦戦することは確実です。

そしてもうひとつ、たとえば displayName はあくまで画面表示のための一時的な情報のつもりだったのに、本来存在しないものを保存してDBが汚染されてしまうことで、後々こんな問題が起きるかもしれません:

  • 厳密なチェックをしている場合、型チェックなど様々な場面でエラーになることがある
  • DBからの取得データに混入することで、画面表示等で意図せず処理してしまってデザインが崩れる
  • 将来的に本当に displayName を導入する際に、衝突や挙動不一致の原因になる
  • 開発者が displayName の存在を前提に使い始めると、データとして意味を持ってしまい、あとから不要だと気づいて消そうとしてもどこで使われているか分からず、消せなくなる

どう修正する?

displayName はただ表示用に取得したいだけなので、オブジェクトを直接書き換えないことが大切です。 この場合は、formatUserName() 内で user を変えず、作った文字列だけリターンするようにすればOKです。

function formatUserName(user) {
  return `${user.lastName} ${user.firstName}`;
}

こうすれば user オブジェクトは一切変更されず、安心して displayName を表示に使うことができます。

補足:あえて user オブジェクトを更新したいときは?

どうしてもオブジェクトに直接 displayName を追加したいケースもあるかもしれません。 その場合は、「ここで意図的に更新してますよ」というのが分かるように書くのが重要です。

また、オブジェクトを更新しながら戻り値も返すと混乱しやすいので、何も返さずに更新だけに専念させましょう。

// ユーザーの表示名を更新する関数
function updateDisplayName(user) {
  user.displayName = `${user.lastName} ${user.firstName}`;
}

こうすることで、読み手に「userが更新されるよ」と明示的に伝えることができます。

補足:途中で出てきた参照渡しって?

関数に引数を与えたとき、「値渡し」か「参照渡し」が行われます。

数値や文字列、true/false のような真偽値などは「値渡し」になり、元の値をコピーしたものが関数に渡されます。
このとき関数内で値を変更しても、コピーされた別の値を変更していることになるため、呼び出し元には反映されません。

オブジェクトや配列は、その**データのある場所(参照)**が関数に渡されます。
そのため関数内で値を変更すると、呼び出し元にも反映されます。

この説明だと言語による違いや JavaScript 特有の仕様を正確には伝えられていないのですが……まぁここではそこまで細かく気にしなくて大丈夫!
「オブジェクトや配列は関数内で変えると外にも影響が出るんだな」くらいで OK です。

今回の読みどころ

  • 「表示用に加工するだけのつもり」が、実はデータ汚染の原因に
  • 関数は可能な限り引数を更新しないように意識する
  • どうしても引数の更新が必要なときは、その意図を明示して「読み手に誤解されないように書く」ことがとても重要

おまけ:お題コードの修正版

少し長いので折りたたみます。

コードはこちら
// 表示用に名前を整形する関数
function formatUserName(user) {
  // 修正箇所はここだけ
  return `${user.lastName} ${user.firstName}`;
}

// 画面表示を更新する関数(簡略化のためログ表示で代替)
function updateDisplay(displayName) {
  console.log(`画面表示を更新: ${displayName}`);
}

// 完了メッセージを表示する関数(簡略化のためログ表示で代替)
function showSuccessMessage(displayName) {
  console.log(`${displayName}さんのプロフィールが更新されました。`);
}

// DBからユーザー情報を取得する関数(固定データを返しているがDBから取得したと仮定する)
function getUserFromDB(userId) {
  return { id: userId, firstName: '太郎', lastName: '山田' };
}

// DBにユーザー情報を保存する関数(保存されるはずのデータをログに表示して代替)
function saveUserToDB(user) {
  console.log('DBに保存したデータ: ')
  console.log(user);
}

// ユーザープロフィールを更新する関数
function updateUserProfile(userId, updates) {
  const user = getUserFromDB(userId);
  if (!user) {
    // ユーザー情報が存在しない場合は何もせず終了
    return;
  }

  // プロフィールの更新
  if (updates.firstName) {
    user.firstName = updates.firstName;
  }
  if (updates.lastName) {
    user.lastName = updates.lastName;
  }

  // 画面表示用に名前を取得
  const displayName = formatUserName(user);

  updateDisplay(displayName);

  saveUserToDB(user);

  showSuccessMessage(displayName);
}

updateUserProfile('user123', {
  firstName: 'Taro',
  lastName: 'Yamada'
});