Closed13

100日チャレンジ day21 (設備予約&カレンダーアプリ)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/47f9e612a3609b


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

100day_challenge_backend/day21_reservation_system at main · lirlia/100day_challenge_backend

riddle_tecriddle_tec

今日は カレンダーと設備の予約 にしようかな

riddle_tecriddle_tec

良いですね!「カレンダー+設備の予約システム」は、バックエンドの機能(データの整合性、時間に関するロジック)とフロントエンド(カレンダー表示)の連携が学べる良いテーマだと思います。

では、以下の仕様で Day 21 の開発を進めましょう。

アプリケーション名: Day21 - Facility Reservation System

主要機能:

  1. 設備管理:
    • 設備の名前、説明、定員などを登録・編集・削除できる。
    • (今回はシンプル化のため、管理画面は分けずに誰でも操作可能とします)
  2. カレンダー表示:
    • 設備ごとの予約状況を月/週/日ビューで表示する。
    • 空いている時間帯をクリックして予約作成を開始できる。
  3. 予約機能:
    • ユーザーが設備と利用開始・終了時間を選択して予約する。
    • 予約時に他の予約との重複がないかチェックする。
    • 予約可能な時間帯(例: 9:00〜18:00)を設備ごとに設定できる(任意)。
  4. マイ予約:
    • ログイン中(に見立てた)ユーザー自身の予約一覧を表示し、キャンセルできる。
  5. ユーザー切り替え:
    • ヘッダーなどにシンプルなドロップダウンを設置し、操作するユーザーを簡単に切り替えられるようにする。(例: User1, User2, ...)

データモデル案:

  • User: (id, name)
  • Facility: (id, name, description, capacity?, availableStartTime?, availableEndTime?)
  • Reservation: (id, facilityId, userId, startTime, endTime)

作業順序:

  1. プロジェクト初期化: template をコピーして day21_reservation_system を作成、package.json 更新、README.md 作成、必要なライブラリ (react-big-calendar, date-fns 等) のインストール。
  2. データモデリングとDB設定: schema.prisma 定義、マイグレーション実行。
  3. APIエンドポイント実装: 設備 (Facilities) と予約 (Reservations) の CRUD API を作成。予約作成時の重複チェックロジックを含む。
  4. ユーザー識別機構: ユーザー切り替えUIとコンテキストの実装。
  5. UIコンポーネント実装: 設備管理ページ、カレンダー表示ページ (react-big-calendar利用)、マイ予約ページ、共通レイアウト等。
  6. 主要業務フロー実装: カレンダーからの予約作成フロー、予約キャンセル等の実装。
  7. デバッグとテスト: curl 及び Playwright (MCP) による動作確認。
  8. ドキュメント作成: README.mdknowledge.mdc の更新。
riddle_tecriddle_tec

肝これだけなんだよなあ


    // 3. Check for overlapping reservations for the same facility
    const overlappingReservations = await prisma.reservation.findMany({
      where: {
        facilityId: facilityId,
        // Find existing reservations where:
        // (existing.startTime < new.endTime) AND (existing.endTime > new.startTime)
        startTime: {
          lt: endDateTime,
        },
        endTime: {
          gt: startDateTime,
        },
      },
    });

    if (overlappingReservations.length > 0) {
      return NextResponse.json(
        { error: "Time slot already booked" },
        { status: 409 }, // Conflict
      );
    }
riddle_tecriddle_tec

設備予約システムは、基本的なCRUDと重複チェックだけであれば比較的シンプルに見えますが、規模が大きくなったり、実際のビジネス要件に応えようとしたりすると、途端に複雑さが増していきます。主な難しさとしては、以下のような点が挙げられます。

  1. パフォーマンスとスケーラビリティ:

    • 膨大な予約データ: ユーザー数、設備数、予約期間が長くなると、予約データは爆発的に増加します。特定の期間や設備の空き状況を高速に検索・表示するためのデータベースクエリ最適化(インデックス設計、効率的なアルゴリズム)が非常に重要になります。単純な全件検索ではすぐに破綻します。
    • 同時アクセスと競合: 多くのユーザーが同時に予約操作を行うと、同じ時間枠をほぼ同時に予約しようとする競合状態が発生しやすくなります。これを防ぐための排他制御(データベーストランザクション、楽観的/悲観的ロックなど)が必要になり、実装とテストが複雑になります。
    • カレンダー表示の負荷: カレンダーに表示するイベント数が多くなると、フロントエンドの描画パフォーマンスが低下します。表示範囲外のデータを読み込まない、イベントのレンダリングを最適化するなどの工夫が必要です。
  2. 複雑な予約ルールと制約:

    • 繰り返し予約: 「毎週月曜日の10時〜11時」のような繰り返し予約とその例外処理(祝日は除くなど)は、データ構造とチェックロジックが複雑になります。
    • 詳細な利用制限: 設備ごとに「利用は〇〇部のみ」「最低利用時間」「最大利用時間」「予約は〇日前まで」「連続利用不可」「利用前後の準備/片付け時間(バッファタイム)」など、細かいルールが追加されると、予約可否の判定ロジックが指数関数的に複雑化します。
    • 承認フロー: 特定の設備(役員会議室など)や、長時間利用の場合に管理者の承認が必要になるケース。ステータス管理(申請中、承認済み、却下など)と通知の仕組みが必要になります。
    • リソースの組み合わせ: 「会議室AとプロジェクターBを同時に予約」のように、複数のリソースをセットで予約する場合の空き状況確認と予約処理。
  3. 高度な機能とUX:

    • 空き時間検索: 「今週空いている3人用の会議室」のような曖昧な条件での検索機能。
    • 外部カレンダー連携: Google CalendarやOutlook Calendarとの双方向同期。API連携や認証、データ形式の差異吸収などが必要です。
    • 変更・キャンセル処理: 単純な削除だけでなく、「時間だけ変更」「参加者を追加」などの部分変更や、キャンセルポリシー(期限、料金など)の適用。
    • 通知: 予約完了、リマインダー、変更・キャンセル通知などをメールやチャットツールに送信。
  4. 権限管理:

    • 一般ユーザー、設備管理者、システム管理者など、役割に応じた操作権限(予約できる設備、他の人の予約を編集できるかなど)の厳密な管理。

今回実装したシステムは、これらの複雑な要件の多くを省略しています。コアとなる「指定した時間枠が空いているか確認して予約する」機能は実装しましたが、実世界の要求に応えるには、上記のような多くの課題に取り組む必要があり、それが大規模な設備予約システムの難しさにつながります。

riddle_tecriddle_tec

たしかに、同時アクセスと競合の制御は、多くのユーザーが利用するシステム、特にリソースの確保(まさに設備予約!)を行うシステムでは非常に重要な課題です。単純な実装では、意図しないダブルブッキングなどが発生する可能性があります。

主な対応策として、以下の3つのアプローチがよく議論されます。

1. データベーストランザクション

  • 考え方: 関連する一連のデータベース操作(①空き状況チェック、②予約レコード作成)を「すべて成功するか、すべて失敗するか」のどちらかしか許さない、アトミックな単位として扱います。これにより、①と②の間に他のユーザーが割り込んで予約を入れてしまう、といった中途半端な状態を防ぎます。

  • 仕組み:

    • データベースにはトランザクション分離レベルという設定があり、他のトランザクションからどの程度影響を受けるか(他の人が変更中のデータを読めるか、など)を定義します。
    • 最も厳格な Serializable レベルでは、トランザクションが完全に直列に実行されるかのように動作し、多くの競合問題を原理的に防ぎます。しかし、パフォーマンスへの影響が大きくなる可能性があります。
    • 他の分離レベル(Read Committed, Repeatable Read など)では、特定の種類の競合(例: ファントムリード)を防げない場合があります。
  • Prisma での実装:

    • prisma.$transaction() を使います。複数の Prisma クエリを配列として渡すか、コールバック関数内でインタラクティブに操作します。
    // 例:インタラクティブトランザクション
    try {
      const newReservation = await prisma.$transaction(async (tx) => {
        // 1. トランザクション内で再度重複チェック (tx を使う)
        const overlapping = await tx.reservation.findMany({
          where: {
            facilityId: facilityId,
            startTime: { lt: endDateTime },
            endTime: { gt: startDateTime },
          },
          // ★必要であればここで FOR UPDATE (悲観ロック) をかけることも可能
        });
    
        if (overlapping.length > 0) {
          throw new Error("Time slot already booked within transaction");
        }
    
        // 2. 予約を作成 (tx を使う)
        const created = await tx.reservation.create({
          data: { facilityId, userId, startTime: startDateTime, endTime: endDateTime },
        });
        return created;
      });
      // トランザクション成功
      return NextResponse.json(newReservation, { status: 201 });
    
    } catch (error: any) {
      // トランザクション失敗 (ロールバックされる)
      if (error.message.includes("Time slot already booked")) {
        return NextResponse.json({ error: "Time slot already booked" }, { status: 409 });
      }
      console.error("Transaction failed:", error);
      return NextResponse.json({ error: "Failed to create reservation" }, { status: 500 });
    }
    
  • 利点: 関連する操作の一貫性を保証する基本的な方法。多くのDBでサポート。

  • 欠点: 分離レベルによっては競合を防ぎきれない。高分離レベルは性能に影響。上記の例だけでは、「チェック後に他のトランザクションがコミットする」競合(Lost Update)を完全に防げない場合がある(ロック機構が必要になる)。

2. 悲観的ロック (Pessimistic Locking)

  • 考え方: データにアクセスする に、他のトランザクションがそのデータ(または関連リソース)を変更できないように明示的にロックを獲得します。「自分が使い終わるまで他の誰も触れないようにする」戦略です。
  • 仕組み (DBレベル):
    • SELECT ... FOR UPDATE のような SQL 文を発行し、読み取った行に対して書き込みロックをかけます。他のトランザクションが同じ行を FOR UPDATE で読み取ろうとしたり、更新/削除しようとしたりすると、ロックが解放されるまで待たされます。
  • Prisma での実装:
    • インタラクティブトランザクション内で $queryRaw を使って SELECT ... FOR UPDATE を実行するか、Prisma の比較的新しいバージョンでは .findUnique.findFirstforUpdate() メソッドが追加されている場合があります(DBとバージョンによる)。
    • 設備予約の場合、単純に Reservation テーブルをロックするのではなく、例えば「特定の設備IDと時間帯」を示す別のロック用テーブルや行をロックする、あるいは関連する Facility レコードをロックするなどの戦略が考えられますが、設計が複雑になりがちです。
  • 利点: 競合を確実に防ぐことができます。
  • 欠点: ロックを獲得・解放するオーバーヘッドがある。ロック待ちによりシステムの同時実行性が低下する。ロックの範囲や順序によってはデッドロックが発生する可能性がある。

3. 楽観的ロック (Optimistic Locking)

  • 考え方: 基本的に競合はあまり起こらないだろうと楽観視し、ロックはかけずに処理を進めます。ただし、データを更新する 直前 に、自分がデータを読み取ってから他の誰も変更していないことを確認します。「とりあえずやってみて、最後に誰かが横やり入れてないか確認する」戦略です。
  • 仕組み:
    • テーブルに version カラム(更新ごとに+1する数値)や updatedAt カラム(最終更新日時)を持たせます。
    • ① データを読み込む際に versionupdatedAt の値も一緒に取得します。
    • ② データを更新する際、WHERE 句に「ID が一致し、かつ 読み込んだ時の version / updatedAt と同じである」という条件を追加します。
    • ③ もし他のトランザクションが先に更新していれば、version / updatedAt が変わっているため、WHERE 条件にマッチせず更新は失敗します(更新件数が0件になる)。
    • ④ 更新が失敗した場合、アプリケーションはエラーを検知し、データを再読み込みして処理をリトライするか、ユーザーにエラーメッセージを表示します。
  • Prisma での実装:
    • スキーマに version Int @default(1) を追加し、update 時に data: { ..., version: { increment: 1 } }where: { ..., version: currentVersion } を指定する。
    • または、updatedAt DateTime @updatedAt を利用し、update 時に where: { ..., updatedAt: readUpdatedAt } を指定する(ただし、ミリ秒以下の精度やDBによる挙動に注意が必要)。
  • 利点: 競合が実際に発生しない限り、ロックのオーバーヘッドがなく、同時実行性が高い。
  • 欠点: 競合が頻繁に発生すると、リトライが増えて逆にパフォーマンスが悪化する可能性がある。リトライ処理の実装が必要になる。予約 作成 のようなケースには直接適用しにくい(更新対象の既存レコードがないため)。予約 変更キャンセル には有効。

どの方法を選ぶか?

  • トランザクション は、データの一貫性を保つための基本として、ほとんどの場合で必須です。
  • 競合が頻繁に起こりうるクリティカルな処理(例: 人気の設備や時間帯の予約)で、かつ 確実にダブルブッキングを防ぎたい 場合は 悲観的ロック の導入を検討します。ただし、パフォーマンスへの影響とデッドロックのリスクを考慮する必要があります。
  • 予約の変更・キャンセルなど、既存のレコードを更新する処理で、同時更新の可能性はあるが頻度はそれほど高くない、という場合には 楽観的ロック が有効な選択肢となります。
  • 設備予約の 新規作成 に関しては、Serializable 分離レベルを持つ トランザクション で保護するか、あるいはトランザクション内で 悲観的ロック(例: 関連する Facility レコードをロック)をかけるのが一般的です。楽観的ロックは適用が難しいです。

今回の実装では、基本的なトランザクション(prisma.$transaction を使った例)を導入するのが、複雑さと安全性のバランスを取る第一歩となるでしょう。

riddle_tecriddle_tec

繰り返し予約(例: 「毎週月曜 10:00-11:00」)とその例外(祝日など)の実現は、設備予約システムの複雑さを増す典型的な要件の一つです。主に2つのアプローチがあります。

アプローチ1: 予約インスタンス展開方式

  • 考え方: 繰り返しルールに基づいて、未来の特定の期間(例: 1年間)の 個別の予約レコード(インスタンス)を事前にすべて生成 してデータベースに保存します。
  • データ構造例 (Prisma風):
    // 繰り返しルールを定義するモデル
    model RecurringRule {
      id          Int      @id
      facilityId  Int
      userId      Int
      rruleString String   // RRULE形式 (例: "FREQ=WEEKLY;BYDAY=MO;INTERVAL=1")
      startTime   String   // "HH:mm"
      endTime     String   // "HH:mm"
      startDate   DateTime // ルール適用開始日
      endDate     DateTime // ルール適用終了日 (この日までのインスタンスを生成)
      // ... 他のメタデータ
      facility    Facility @relation(fields: [facilityId], references: [id])
      user        User     @relation(fields: [userId], references: [id])
    }
    
    // 実際に生成された個別の予約レコード (通常の予約と同じテーブルでも良い)
    model Reservation {
      id         Int      @id @default(autoincrement())
      facilityId Int
      userId     Int
      startTime  DateTime // 具体的な予約開始日時
      endTime    DateTime // 具体的な予約終了日時
      recurringRuleId Int? // どのルールから生成されたか (Nullable)
      // ... 他のメタデータ
      facility    Facility @relation(fields: [facilityId], references: [id])
      user        User     @relation(fields: [userId], references: [id])
      // rule     RecurringRule? @relation(fields: [recurringRuleId], references: [id])
    
      @@unique([facilityId, startTime, endTime]) // 重複防止
    }
    
    // 祝日など、予約を除外する日付
    model ExclusionDate {
      id   Int      @id
      date DateTime @unique // YYYY-MM-DD (時間部分は00:00:00Z)
      name String
    }
    
  • ロジック:
    • 予約作成時:
      1. RecurringRule レコードを作成します。
      2. 指定された startDate から endDate までの期間で、rruleString に基づいて該当日時を計算します (例: rrule.js ライブラリなどを使用)。
      3. 各該当日時について、ExclusionDate に含まれていないかチェックします。
      4. 除外日でなければ、対応する startTimeendTime を持つ Reservation レコードを作成し、recurringRuleId を設定して INSERT します。この際、他の予約との重複もチェックします(失敗したらトランザクション全体をロールバック)。
    • 空き状況チェック: 特定の日時 (checkTime) について、Reservation テーブルに facilityId = X AND startTime < checkTime AND endTime > checkTime となるレコードが存在するかどうかを検索するだけです。非常にシンプルです。
    • 変更/削除:
      • ルール全体の変更/削除: RecurringRule を更新/削除し、関連する 未来の Reservation レコードもすべて更新/削除する必要があります。非常に手間がかかります。
      • 特定の日だけキャンセル: 対応する Reservation レコードを直接削除するか、ステータスを変更します。
  • メリット:
    • 空き状況のチェックロジックが非常にシンプルで高速。
    • 個別の予約インスタンスの変更(特定日のみキャンセルなど)が容易。
  • デメリット:
    • 長期間の繰り返し予約の場合、膨大な Reservation レコードが生成され、DB容量を圧迫する可能性がある。
    • 繰り返しルール自体の変更(例: 毎週月曜→火曜に変更)や、適用期間の変更が非常に複雑でコストが高い(関連インスタンスの再計算・更新/削除が必要)。
    • 「無限」の繰り返しには対応できない(endDate が必須)。

アプローチ2: ルールベース評価方式

  • 考え方: データベースには 繰り返しルール の情報だけを保存し、個別の予約インスタンスは生成しません。空き状況をチェックする際に、その都度 ルールを評価して、指定した日時が予約されているかどうかを判断します。
  • データ構造例 (Prisma風):
    // 繰り返しルールを定義するモデル (アプローチ1とほぼ同じ)
    model RecurringRule {
      id          Int      @id
      facilityId  Int
      userId      Int
      rruleString String   // RRULE形式 (例: "FREQ=WEEKLY;BYDAY=MO;INTERVAL=1;UNTIL=...")
      startTime   String   // "HH:mm"
      endTime     String   // "HH:mm"
      startDate   DateTime
      // endDateは必須ではない (無限繰り返し可能)
      endDate     DateTime?
      // ...
      facility    Facility @relation(fields: [facilityId], references: [id])
      user        User     @relation(fields: [userId], references: [id])
      exceptions  RecurringException[] // このルールに対する例外
    }
    
    // 通常の1回限りの予約 (繰り返しルールと共存)
    model Reservation {
      id         Int      @id @default(autoincrement())
      facilityId Int
      userId     Int
      startTime  DateTime
      endTime    DateTime
      // recurringRuleId は持たない
      // ...
      facility    Facility @relation(fields: [facilityId], references: [id])
      user        User     @relation(fields: [userId], references: [id])
       @@unique([facilityId, startTime, endTime])
    }
    
    // 特定の繰り返しルールに対する例外 (キャンセル、変更など)
    model RecurringException {
      id              Int      @id
      ruleId          Int
      originalDate    DateTime // 例外対象となる元の発生日 (時間部分は無視しても良い)
      status          String   // CANCELED, MODIFIED
      modifiedStartTime DateTime? // 時間変更の場合
      modifiedEndTime DateTime?
      // ...
      rule RecurringRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
      @@index([ruleId, originalDate])
    }
    
    // 祝日など (アプローチ1と同じ)
    model ExclusionDate { /* ... */ }
    
  • ロジック:
    • 予約作成時: RecurringRule レコード(または Reservation レコード)を作成するだけ。
    • 空き状況チェック: 特定の日時 (checkTime) について、以下の処理を行います。
      1. まず、Reservation テーブルに facilityId = X AND startTime < checkTime AND endTime > checkTime となる 通常の予約 がないか検索します。あれば予約済み。
      2. 次に、checkTime を含む可能性のある RecurringRule をすべて取得します (facilityId = X AND startDate <= checkTime AND (endDate IS NULL OR endDate >= checkTime))。
      3. 取得した各ルールについて、rrule.js などを使って checkTime がそのルールの 発生日時に該当するか を評価します。
      4. 該当する場合、ExclusionDate にその日が含まれていないかチェックします。
      5. 該当する場合、RecurringException テーブルに、そのルールIDと元の発生日に対応する CANCELED な例外がないかチェックします。
      6. 上記 1, 4, 5 のいずれにも該当せず、3 でルールに該当した場合、予約済みと判断します。
    • 変更/削除:
      • ルール全体の変更/削除: RecurringRule レコードを更新/削除するだけ。非常に簡単。
      • 特定の日だけキャンセル: RecurringException テーブルに、対象ルールID、元の発生日、ステータス CANCELED のレコードを追加します。
  • メリット:
    • DBのレコード数が少なく、ストレージ効率が良い。
    • 繰り返しルール全体の変更が非常に容易。
    • 無限の繰り返しに対応できる。
  • デメリット:
    • 空き状況チェックのロジックが非常に複雑 になり、パフォーマンスへの影響が大きい可能性がある。多くのルールが存在する場合、チェック対象を効率的に絞り込む必要がある。
    • 例外処理の実装も必要になる。

どちらを選ぶか?

  • 予約の変更が少なく、ルールも単純で、チェック速度が最優先 なら アプローチ1 が有利な場合があります(ただしDB容量とルール変更コストに注意)。
  • ルール変更が多く、ストレージ効率が重要で、無限繰り返しが必要 なら アプローチ2 が適しています(ただしチェックロジックの複雑さとパフォーマンスに注意)。
  • ハイブリッド方式 として、アプローチ2を基本としつつ、直近の予約インスタンスだけをキャッシュとして生成・利用する方法もありますが、さらに実装は複雑になります。

RRULE について:

繰り返しルールを表現する標準的な方法として iCalendar (RFC 5545) で定義されている RRULE 形式があります。FREQ=WEEKLY;BYDAY=MO;INTERVAL=1;UNTIL=... のような文字列で複雑なルールを表現でき、多くのライブラリ(JavaScriptなら rrule.js など)がこの形式のパースと日付計算をサポートしています。これを利用すると、ルール定義部分の実装が楽になります。

祝日などの例外は、外部の祝日APIを利用したり、事前に ExclusionDate テーブルに登録しておいたりして、チェックロジックの中で考慮します。

このように、繰り返し予約は見た目以上に考慮事項が多く、システム設計上の重要なポイントとなります。

riddle_tecriddle_tec

なるほど。しかしそうなるとルール数が増えると処理が線形に伸びますよね


はい、その通りです! アプローチ2(ルールベース評価方式)の最も大きな課題は、まさにおっしゃる通り、ルール数が増えるにつれて空き状況チェックの処理時間が線形的に(あるいはそれ以上に)増加してしまう点にあります。

ナイーブな実装では、特定の時間帯 (checkTime) の空きを確認するために、

  1. その時間帯を含む可能性のある全繰り返しルール (RecurringRule) をデータベースから取得する。
  2. 取得した各ルールについて、checkTime がルールの発生条件に合致するかをライブラリ (rrule.js など) を使って評価する。
  3. 合致した場合、祝日などの除外日 (ExclusionDate) や個別の例外 (RecurringException) がないかを確認する。

というステップを踏む必要があり、ステップ2と3はルール数 만큼 繰り返されるため、ルール数が100倍になれば、チェックにかかる時間も単純計算で100倍になってしまう可能性があります。これは、特に多くのユーザーが頻繁に空き状況を確認するようなシステムでは、パフォーマンス上の深刻なボトルネックになり得ます。

では、大規模システムではどう対応しているのか?

この問題を解決・緩和するために、実際のシステムでは以下のような最適化手法が組み合わせて用いられることが多いです。

  1. データベースインデックスの最適化:

    • RecurringRule テーブルの facilityId, startDate, endDate などに適切にインデックスを貼ることで、ステップ1(関連ルールの取得)を高速化します。これは基本ですが重要です。
  2. ルールの事前計算とキャッシュ(時間ベースでの展開):

    • これが最も効果的な対策の一つです。**「完全にオンデマンドで評価する」のではなく、「ある程度の未来(例: 次の数週間、数ヶ月)までのルールの発生インスタンスを計算し、検索しやすい形でキャッシュしておく」**という考え方です。
    • 具体的な方法:
      • バックグラウンドジョブ(例: 1日1回実行)を用意します。
      • ジョブは、各 RecurringRule について、例えば「今日から3ヶ月先まで」の発生日時を rrule.js などで計算します。
      • 計算した発生日時から、祝日や例外を除外します。
      • 最終的に残った「確定済みの未来の予約スロット(設備ID、開始日時、終了日時)」を、別の高速検索用テーブル(予約キャッシュテーブル) に保存します。このテーブルは単純な構造で、日時範囲での検索に特化したインデックスを持ちます。
    • 空き状況チェック時の動作:
      • まず、予約キャッシュテーブル を検索します。チェックしたい時間帯がこのテーブルにあれば「予約済み」と即座に判断できます。
      • キャッシュテーブルになければ「空き」と判断できます(ただし、キャッシュがカバーしていない遠い未来や、キャッシュ生成後に作られたルールについては、別途ルールベース評価が必要になる場合もあります)。
    • メリット: 通常の空き状況チェック(特に直近の未来)が非常に高速になります。ルール評価の負荷がバックグラウンドジョブに分散されます。
    • デメリット: 実装が複雑になります(バックグラウンドジョブ、キャッシュテーブル管理、ルール変更時のキャッシュ更新など)。キャッシュデータのためのストレージ容量が余分に必要になります。
  3. ルール検索範囲の限定:

    • 空き状況チェック時に、明らかに checkTime と無関係なルール(例: 終了日が checkTime よりずっと前、開始日がずっと未来)を効率的に除外するクエリを工夫します。
  4. ルール評価ロジックの最適化:

    • rrule.js などのライブラリも、特定の日付がルールに合致するかどうかを効率的に評価する機能を持っています。ライブラリの機能を最大限活用します。
  5. リードレプリカの活用:

    • 空き状況の確認(読み取り)が非常に多い場合、データベースのリードレプリカに参照クエリを分散させることも有効です(ただし、キャッシュ戦略との兼ね合いも考慮)。

まとめ:

ルールベース評価方式の線形スケーリング問題は現実的な課題であり、大規模システムでは単純なルール評価だけでなく、「事前計算とキャッシュ」戦略 を中心とした最適化を行うのが一般的です。これにより、ルール数の増加に対するパフォーマンスの劣化を大幅に抑制し、ユーザー体験を維持しています。もちろん、その分システムの複雑性は増すことになります。

riddle_tecriddle_tec

なるほどな〜〜。まあ流石に事前計算しないとダメだよなあ。
しかしそう考えると設備予約を新規で作るのってうまみ少ないのに考えること多くて大変だな〜

そして、作る前は学びを思いつかなかったけどやってみると意外と色々とあるのね。

このスクラップは4ヶ月前にクローズされました