家計簿アプリを作る #17:複数ユーザー対応

家計簿アプリ作成シリーズの第17回です。今回は複数ユーザー対応を実装します。これまで全ユーザーのデータが共有されていたため、ログインユーザーのデータのみ表示・操作できるように修正します。

実装方針

transactions テーブルに userId カラムを追加し、登録・一覧・編集・削除の各操作で userId を使ってフィルタリングします。userId には Better Auth が発行するユーザー ID(locals.user.id)を使います。

メールアドレスではなくユーザー ID を使う理由:

  • メールアドレスは変更される可能性があるが、ID は変わらない
  • ID はユーザーを一意に識別するために設計されている

Step 1:スキーマに userId を追加する

export const transactions = sqliteTable('transactions', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  userId: text('user_id').notNull(),
  amount: int('amount').notNull(),
  type: text('type').notNull(),
  category: text('category').notNull(),
  memo: text('memo').notNull().default(''),
  date: text('date').notNull()
});

Step 2:2段階マイグレーション

既存データがある状態で NOT NULL カラムを追加するとエラーになります。以下の手順で対応します。

段階1:NULL 許容で追加する

まず userId を NULL 許容として追加するマイグレーションを作成・適用します。

段階2:既存データに userId をセットする

npx wrangler d1 execute kakeibo-app-db --local \
  --command "UPDATE transactions SET user_id = 'your-user-id' WHERE user_id IS NULL"

段階3:NOT NULL に変更する

スキーマを notNull() に変更して再度マイグレーションを作成・適用します。

ハマりポイント①:誤って NOT NULL でマイグレーションファイルを作成してしまった

NOT NULL のままマイグレーションファイルを生成してしまった場合、そのファイルを直接編集して NOT NULL を削除します。余分に生成されたファイルは手動で削除します。

ハマりポイント②:_journal.json にエントリが残る

drizzle-kit generate した時点で migrations/meta/_journal.json に記録されます。適用に失敗・キャンセルしたマイグレーションファイルを削除した場合、_journal.json からも該当エントリを手動で削除する必要があります。

migrations/meta/ 配下のスナップショットファイルはそのままで問題ありません。

Step 3:登録時に userId をセットする

create: async ({ request, platform, locals }) => {
  const userId = locals.user?.id;

  await db.insert(transactions).values({
    userId: String(userId),
    // ...
  });
}

Step 4:一覧・集計を userId でフィルタリングする

import { eq, like, and } from 'drizzle-orm';

// 全件取得(userId でフィルタ)
const allTransactions = await db.select().from(transactions)
  .where(eq(transactions.userId, locals.user!.id));

// 月フィルタ時(and で条件を追加)
const selectedTransactions = await db
  .select()
  .from(transactions)
  .where(
    and(
      eq(transactions.userId, locals.user!.id),
      like(transactions.date, `${selectedMonth}%`)
    )
  );

複数条件の WHERE 句は and() を使います。

Step 5:削除・編集にも userId の条件を追加する

// 削除
await db.delete(transactions).where(
  and(
    eq(transactions.userId, locals.user!.id),
    eq(transactions.id, String(id))
  )
);

// 編集(load)
const transaction = await db.select().from(transactions)
  .where(
    and(
      eq(transactions.userId, locals.user!.id),
      eq(transactions.id, params.id)
    )
  ).get();

まとめ

ポイント 内容
userId メールアドレスではなく locals.user.id を使う
2段階マイグレーション NULL 許容で追加 → データ更新 → NOT NULL に変更
_journal.json 削除したマイグレーションファイルのエントリも手動で削除が必要
複数条件の WHERE and() を使う
削除・編集 userId の条件を追加して他ユーザーのデータを操作できないようにする
← トップページに戻る