飛行計画機能 forDemo アーキテクチャ方針
本ドキュメントは、飛行計画機能 forDemo(10月デモ向け)の固有アーキテクチャ方針を定義する。共通のアーキテクチャ方針(ArchitecturePolicy_Common.md)と合わせて参照すること。
対象範囲はBusinessLogicSpecifications.mdの通り以下のAPIのみとする(それ以外の飛行計画関連API・運航調整との紐づけは対象外)。
- 飛行計画仮登録(
createFlightPlan) - 飛行計画本登録(
registerFlightPlan) - 飛行計画一覧取得(
listFlightPlans) - 飛行計画詳細取得(
getFlightPlan) - 飛行計画更新(
updateFlightPlan) - 飛行計画削除(
deleteFlightPlan) - 飛行計画通報(
reportFlightPlan)
deleteFlightPlanのうち、DIPS通報済み(reportStatus=REPORTED)の飛行計画に対する取り下げ処理(DIPS API呼び出しとWITHDRAWING/WITHDRAWNへの遷移)は対象外である。模擬DIPSに飛行計画の削除APIが存在せず取り下げ自体を実行できないため(BusinessLogicSpecifications.md5.3節手順1-7・4.6節)。通報済みの飛行計画を削除した場合、DIPS側の登録は残る。
本ドキュメントは、飛行計画機能の Java Spring Boot 実装におけるアーキテクチャ方針を定義する。
Claude Code によるコード生成時は、本ドキュメントおよび ArchitecturePolicy_Common.md の内容を前提として、以下の成果物を生成すること。
- Handler(Primary Adapter)
- UseCase
- Domain Model(Entity・値オブジェクト)
- Port(Repositoryインターフェース・TransactionManagerインターフェース)
- Repository実装(Secondary Adapter)
- MyBatis Mapper(インターフェース + XML)
- Exception(ドメイン例外)
- 生成DTO(OpenAPIから自動生成)
- レスポンスMapper(ドメイン型 → 生成DTO変換)
- Unit Test / Integration Test の雛形
2. 技術スタック
Section titled “2. 技術スタック”飛行計画機能は、UTM Backends共通の技術スタック(ArchitecturePolicy_Common.md参照)に加え、以下を使用する。
| 区分 | 採用技術 |
|---|---|
| 言語 | Java 25 |
| フレームワーク | Spring Boot 4 |
| Web | Spring Web |
| DB アクセス | MyBatis(XML マッパー) |
| DB | PostgreSQL(PostGIS拡張) |
| ビルドツール | Gradle(Kotlin DSL) |
| API 仕様 | OpenAPI 3.x(スキーマファースト) |
| コード生成 | openapi-generator(interfaceOnly) |
| Validation | Jakarta Bean Validation(Handler 層のみ) |
| テスト | JUnit 6 / Mockito / Testcontainers / ArchUnit |
| ログ | SLF4J + Logback |
| コードフォーマット | Spotless(Google Java Format) |
| 静的解析 | Error Prone / NullAway |
| API Lint | Spectral(OAS) |
| SQL Lint | slowql |
| Null 安全 | JSpecify(@NullMarked / @Nullable) |
3. パッケージ構成
Section titled “3. パッケージ構成”基本パッケージは、UTM Backends共通の以下とする(既にmainブランチへ統合済みのフラット構成。飛行計画機能専用の基本パッケージは設けない)。
com.intent_exchange.utm飛行計画機能のクラスは、以下の既存の共通レイヤに直接配置する。
com.intent_exchange.utm├── config/ ← 明示的 DI(@Bean で依存グラフを組み立てる)├── generated/ ← OAS 生成コード(手編集禁止、Handler 層からのみ参照可)│ └── api/ , model/ ← frontend audience(docs/openapi/frontend/openapi.yaml)から生成。`FlightPlanningApi`等├── handler/ ← Primary Adapter(XxxApi 実装・意味的 parse)│ ├── problem/ ← RFC 9457 エラー基盤│ └── response/ ← ドメイン型 → 生成 DTO 変換├── usecase/ ← アプリケーション層(1 ユースケース 1 クラス・Tx 境界)├── domain/│ ├── model/ ← Always-Valid ドメインモデル│ ├── port/ ← Repository・TransactionManager インターフェース│ ├── exception/ ← ドメイン例外│ └── common/└── infrastructure/ └── persistence/postgres/ ← Secondary Adapter(MyBatis 実装) └── mapper/ ← Mapper インターフェース + XML- Handlerクラス名は
FlightPlanningHandlerとする。 - UseCaseクラスは各APIの
operationIdに対応させる(例:CreateFlightPlanUseCase)。 - 既存の
domain.modelには、運航調整機能が飛行計画を参照するための最小限のスタブ(FlightPlanSimple・FlightPlanSimpleId・FlightPlanStatus・DipsFlightPlanId等)が既に存在する。飛行計画機能本体のドメインモデル(FlightPlan・FlightPlanRevision等、19節参照)を実装する際は、これらの既存スタブとの重複・整合を確認すること(既存スタブは運航調整側が参照する最小限の型のため、置き換えではなく共存が基本方針になる見込み。詳細は実装時に判断する)。
各パッケージの責務はpackage-info.javaに記述する(既存の記述を踏襲)。
4. 論理削除方針
Section titled “4. 論理削除方針”飛行計画機能のDBテーブルの論理削除方針は、ER図(flight-plan-ER.md)の設計に従う。
| カラム名 | 型 | 用途 |
|---|---|---|
| deleted_at | timestamptz(nullable) | 論理削除日時。NULLなら未削除 |
- 論理削除カラムを持つのは
flight_planテーブルのみ(deleted_at)。flight_plan_revision以下(リビジョン・領域・操縦者機体割当等)は不変レコード(INSERT only)として設計されており、個別の論理削除カラムは持たない。 - 削除APIでは物理削除を行わず、
flight_plan.deleted_atに削除時刻を設定する。 - 削除可能な状態(
status)の制約はBusinessLogicSpecifications.md5.5節手順1-1・OAS(deleteFlightPlan)に従う(DRAFT・ACCEPTED・CANCELLEDは削除可、ACTIVATED・ENDEDは409)。 deleted_atを設定するのはdeleteFlightPlanだけではない。DRAFTからのキャンセル(cancelFlightPlan。冒頭の対象範囲外のため実装対象外)も削除と同等に扱い、statusはDRAFTのままdeleted_atのみを設定する(ER図の「一時保存は専用テーブルで表す」節)。deleteFlightPlanはstatusを変えないためflight_plan_state_eventを追記しない。対象がDRAFTの場合はflight_plan_draftの行を物理削除する(BusinessLogicSpecifications.md5.5節手順1-3)。- 検索処理では
deleted_at IS NULLのデータのみを対象とする。
(カラム定義はdb/schema/flight-planning.sqlを参照すること。)
5. 監査項目方針
Section titled “5. 監査項目方針”飛行計画機能はER図(flight-plan-ER.md)の設計に従い、テーブルごとに意味の異なる日時カラムを持つ。
flight_plan.created_at: 飛行計画自体の作成日時(仮登録時に設定。不変)。flight_plan.updated_at: 最終更新日時の導出キャッシュ。内容変更(flight_plan_revision.changed_at)・状態遷移(flight_plan_state_event.occurred_at)・一時保存中の上書き(flight_plan_draft.updated_at)の3者の最大値を保持し、全レスポンスで必須のupdatedAtに対応する(詳細はflight-plan-ER.md図1カラム補足参照)。flight_plan_revision.changed_at: 当該リビジョンが作成された日時(Gitのコミット日時相当。不変)。内容変更・状態遷移のいずれも新リビジョンの作成として記録するため、リビジョンの正確な変更時刻を表す(flight_plan.updated_atの算出元の1つ)。- その他のテーブル(
dips_report.reported_at、dips_nearby_flight_plan.collected_at等)は、テーブルごとの業務的な意味を持つ日時カラムをそれぞれ定義する。
created_by・updated_by相当として、flight_plan.created_by(作成者)・flight_plan_revision.changed_by(変更者)をUSERへの外部キーとして持つ(IAMドメインとの連携)。
日時型はTIMESTAMPTZを使用する。カラム定義はdb/schema/flight-planning.sqlを参照すること。
6. ページング方針
Section titled “6. ページング方針”forDemoではページングは不要とする(listFlightPlansは絞り込み条件に合致する飛行計画をすべて登録順で返す)。
7. セキュリティ方針(forDemo)
Section titled “7. セキュリティ方針(forDemo)”forDemoでは認証なしで実装する。認証関連の依存・実装を最小構成にするため、Spring Security 自体を導入しない(spring-boot-starter-security 依存を追加しない、SecurityConfig を作成しない)。認証チェックの仕組みが存在しないため、Authorization ヘッダを付与せずにAPIを実行しても401にはならない。
RLS(Row Level Security)については、forDemoでは実装対象外とする。TransactionSessionInitializerは no-op とする。
8. 設定ファイル方針(飛行計画固有)
Section titled “8. 設定ファイル方針(飛行計画固有)”設定値はapplication.yamlに定義する。環境依存の値や機密情報の扱いはArchitecturePolicy_Common.mdの19節に従う。
飛行計画固有の設定対象:
- DB 接続情報(既存の共通DataSourceを使用。専用DataSourceは設けない)
- ログレベル
- トランザクション設定(
app.transaction.*)
模擬DIPS連携の設定(reportFlightPlanが使用):
- 接続情報は
dips.api.*(DipsApiProperties)に持つ。ホスト・USS ID・クライアント証明書のキーストアとパスワードが対象で、既定値は組み込まない(環境ごとに必ず明示する)。 - 通報者の連絡先は
dips.api.reporter.*に固定値として持つ(flight-plan-field-mapping.md11-5節)。個人情報を含むため設定ファイルに直書きせず、環境変数で注入する。 - キーストアはファイルパスで指定するため、コンテナ環境ではマウント方法とあわせて設定する。
dips.api.enabled=false(模擬DIPS未起動のローカル環境)でもアプリは起動する。通報APIだけが503を返す(18節)。
9. API バージョニング方針
Section titled “9. API バージョニング方針”API は URI パス方式でバージョニングする。基本パスはArchitecturePolicy_Common.mdの全体方針に従う。
飛行計画機能はUI向けのfrontend APIのみを提供する。
UI向け外部 API : /api/v1/fp/flight-plansパスプレフィックス/api/v1/fp/はdocs/openapi/frontend/flight-planning.yamlの定義に従う(fp = FlightPlanning)。server.servlet.context-pathは設定しない。
10. HTTPステータスコード方針(飛行計画固有)
Section titled “10. HTTPステータスコード方針(飛行計画固有)”基本のHTTPステータスコードはArchitecturePolicy_Common.mdの10節に従う。
| 処理結果 | HTTP ステータス |
|---|---|
| 仮登録・本登録の入力値不正 | 422(DIPS必須項目のビジネスルール違反) |
| 本登録済み・削除済み等への不正な操作 | 409(状態不正) |
| ロック競合 | 409(20節参照) |
| 模擬DIPSの応答を解釈できない | 502(18節参照) |
| 模擬DIPSへ到達できない・模擬DIPSが通報を受理しない | 503(18節参照) |
| 模擬DIPSへ届いたが結果が分からない | 504(18節参照) |
HTTP ステータスの設定は Handler 層(または GlobalExceptionHandler)の責務であり、UseCase・Domain では設定しない。
11. コード生成時の出力方針
Section titled “11. コード生成時の出力方針”Claude Code は以下の成果物を生成すること。
src/main/java/com/intent_exchange/utm/handler/src/main/java/com/intent_exchange/utm/handler/response/src/main/java/com/intent_exchange/utm/usecase/src/main/java/com/intent_exchange/utm/domain/model/src/main/java/com/intent_exchange/utm/domain/port/src/main/java/com/intent_exchange/utm/domain/exception/src/main/java/com/intent_exchange/utm/infrastructure/persistence/postgres/src/main/java/com/intent_exchange/utm/infrastructure/persistence/postgres/mapper/src/test/java/com/intent_exchange/utm/生成時は以下を遵守する(ArchitecturePolicy_Common.mdと重複する項目も、飛行計画機能で特に注意すべき点として再掲する)。
- コンパイル可能なコードを生成する
- 未使用 import を含めない
- TODO コメントを乱用しない
- 判断できない仕様はコメントで明示する
- Domain Entity に
@Entity・ORM アノテーションを付与しない - Domain・UseCase に
@Component・@Serviceを付与しない(自動スキャンを使わない) - Handler に業務ロジックを書かない
- UseCase に HTTP 固有の処理を書かない
@Transactionalを使わずTransactionManager.executeを使う- Repository 実装内で
DataAccessExceptionをRepositoryExceptionにラップする - SQL を文字列連結で組み立てない(MyBatis の
#{}を使用) - ドメイン例外に HTTP ステータスを含めない
- OpenAPI と DB 定義に基づいて型を決定する
- 飛行領域(
geometry)はPostGIS型⇔ドメイン型の変換をInfrastructure層に閉じ込め、Domain層にPostGIS依存のクラスを持ち込まない
12. 実装対象外
Section titled “12. 実装対象外”以下は明示的な仕様がない限り実装対象外とする(ArchitecturePolicy_Common.mdと共通)。
- 画面 UI
- フロントエンド実装
- バッチ処理
- メッセージキュー連携(拡張点は用意する)
- ファイルアップロード/ダウンロード
- 認証基盤そのものの実装
- 本番環境向け CI/CD 設定
- Kubernetes マニフェスト
- JWT認証連携
- RLS(Row Level Security)
- ページング実装
- Observability計装(Micrometer Tracing / OpenTelemetry)
ただし、OpenAPI または業務仕様に記載がある場合は、その内容を優先する。
13. Claude Code への補足指示
Section titled “13. Claude Code への補足指示”コード生成時は、以下の入力資料を参照すること。
- OpenAPI yaml(
docs/openapi/frontend/flight-planning.yaml。スキーマファースト) - DB create SQL(
db/schema/flight-planning.sql) - 本アーキテクチャ方針(本ドキュメントおよび ArchitecturePolicy_Common.md)
- 業務仕様書(
docs/flight-planning/BusinessLogicSpecifications.md) - コーディング規約(
CodingConventions_Flightplanning_forDemo.md) - 認証・認可仕様(
AuthenticationSpecification_Flightplanning_forDemo.md) - ER図(
docs/data-model/flight-plan-ER.md) - 状態遷移図(
docs/flight-planning/design/statemachine/flight-plan-statemachine.md) - シーケンス(
docs/flight-planning/design/sequence/flight-plan-sequence.md)
不明点がある場合は、勝手に複雑な設計を追加せず、以下のいずれかで対応すること。
- 実装リファレンスの標準構成で実装する
- コメントで判断理由を残す
- 実装上の仮定を明記する
14. 日時方針(飛行計画機能固有)
Section titled “14. 日時方針(飛行計画機能固有)”飛行計画機能では、サーバの内部時刻処理・DB格納・API入出力の3点をすべてUTC基準に統一する。
- DB側の日時型は
TIMESTAMPTZ(タイムゾーン付き)を使用する(TIMESTAMPは使用しない)。カラム定義はdb/schema/flight-planning.sqlを参照すること。 - ドメイン層の日時型は
OffsetDateTime(UTC固定)とする(ArchitecturePolicy_Common.md15節と整合)。 - API入出力は RFC3339 UTC表記(末尾
Z)とする。 - 飛行開始予定日時(
flightPeriod.startTime)は分単位(TimestampMinute、秒以下切り捨て)を前提とする(OAS参照)。
15. DBスキーマ修飾方針
Section titled “15. DBスキーマ修飾方針”飛行計画機能のテーブルは専用スキーマflight_planningに属する(CREATE SCHEMA IF NOT EXISTS flight_planning;)。DB自体(DB名utm)は共有し、スキーマで機能ごとに分離する。
application.yamlのdatasource.urlに?currentSchema=flight_planningは付与しない。- Mapper XMLのSQLでは、テーブル名に
flight_planning.プレフィックスを付与し、スキーマを明示的に修飾する(例:FROM flight_planning.flight_plan)。 flight_plan_pilot_assignmentはasset.pilot/asset.assetへ実FK参照するため、Assetドメイン(db/schema/asset.sql)を先に適用しておく必要がある(db/schema/flight-planning.sql冒頭コメント参照)。- PostgreSQLイメージは
postgis/postgis:18-3.6を使用する(flight_plan_area・dips_nearby_flight_planのジオメトリカラムでPostGIS型を使用するため)。
16. コード生成方針(タグ設定)
Section titled “16. コード生成方針(タグ設定)”飛行計画としての特記事項なし。
17. 対向先 Mockサーバー方針
Section titled “17. 対向先 Mockサーバー方針”模擬DIPSの飛行計画API(通報・参照)は、Prism(stoplight/prism)でモックサーバーを立てて代替する。
モック定義はtests/mocks/mock-dips/flightplan.yamlに置く。これは模擬DIPSの仕様書と実機の実行結果から
書き起こした派生物であり、正は仕様書と実機の実行結果である(同ファイル冒頭の注記)。
- テストからはTestcontainersでPrismコンテナを起動し、
testプロファイルの接続先をそのコンテナへ向ける。 - PrismはmTLSを表現できないため、テストでの接続先は平文のHTTPとする。クライアント証明書の要否・ 有効性の検証はモックの対象外であり、実機との疎通で確認する。
- モック定義には4xx/5xxを持たせない(模擬DIPSの仕様書にエラー応答の定義が無く、実行結果にも例が無いため、
推測でエラー応答を作らない)。異常系はPortの実装(
DipsApiClientImpl)に対するテストで賄う。
18. 対向呼び出しPort設計
Section titled “18. 対向呼び出しPort設計”模擬DIPSの呼び出しはdomain.port.DipsApiClient(Port)を経由し、実装は
infrastructure.client.dips(Secondary Adapter)に置く。認証方式(mTLS)の詳細は
DipsHttpClientProviderに閉じ、UseCaseはPortのインターフェースだけを見る。
失敗は**「リクエストが模擬DIPSに届いたか」**を軸に3種のドメイン例外へ詰め替える。届いたのであれば
模擬DIPS側で処理が完了している可能性があり、通報の再送が二重登録になりうるためである。
3種はGlobalExceptionHandlerがHTTPステータスとDIPS専用のproblem typeへ対応付ける。
| 例外 | 意味 | HTTP | type |
|---|---|---|---|
ExternalDipsInvalidResponseException | 完全な応答は届いたが解釈できない | 502 | /problems/dips-invalid-response |
ExternalDipsUnavailableException | 未送信が確定(接続不可・ハンドシェイク失敗) | 503 | /problems/dips-call-failed |
ExternalDipsTimeoutException | 届いたが結果が分からない | 504 | /problems/dips-timeout |
- Portのインターフェースは、宣言していない例外を実装が投げてはならない契約とする。
- 200応答でも業務的に失敗している場合があるため(
flightPlanRegistrationResultが1以外)、 成否の判定はPortではなくUseCaseの責務とする。Portは受け取った値をそのまま返す。 - 自動リトライはPort・UseCaseのいずれにも実装しない。再送の可否は上表の3分類で呼び出し側が判断する。
- 外部I/Oには自動計装が付かないため、トレースが必要なら
infrastructure側でObservationRegistryを 使ってObservationを張る(docs/implementation-guide.md「Observability」)。usecase・domainには 計装コードを置かない(ArchUnitがio.micrometer../io.opentelemetry..依存を禁止する)。 - 通報リクエストは通報者・操縦者の氏名・住所・電話番号・メールアドレスを含む。リクエスト/レスポンスの
本文をログに出さない(
ArchitecturePolicy_Common.mdのログ方針、PR-09のopen-questions.mdD-6)。 - 模擬DIPS連携は
dips.api.enabledで有効・無効を切り替える。無効時も同じPortのBeanを必ず登録する (常にExternalDipsUnavailableExceptionを送出するフォールバック実装)。Beanが存在しないと、Portに依存する UseCaseを組み立てられずアプリ自体が起動しなくなり、模擬DIPS未起動のローカル環境で他のAPIまで使えなくなる。 未送信が確定している状況のため、分類は「到達できない」(503)が正しい。
19. ドメインモデル方針(不変リビジョン)
Section titled “19. ドメインモデル方針(不変リビジョン)”19.1 背景
Section titled “19.1 背景”共通のドメインモデル方針(ArchitecturePolicy_Common.md6.2節)は「1つの状態軸をsealed interfaceで型分けし、メソッド呼び出しで遷移する」パターンを示している。飛行計画のコア集約(FlightPlan/FlightPlanRevision)は、この前提と以下の点で性質が異なる。
- ER図(
flight-plan-ER.md)の設計方針「飛行計画は不変リビジョンの連なりとして管理する」により、flight_plan_revisionはINSERT onlyの不変レコードであり、内容変更・状態遷移のいずれも新しいリビジョン行の追加として表現される(既存行のUPDATEではない)。 flight_planは不変ヘッダ(ID・所有組織・作成者)+現在リビジョンへのポインタ(current_revision_id)のみを持つ。- リビジョン間の親子関係は
parent_revision_idによるチェーン(Gitのコミットparent相当)で表現する。
19.2 集約の構造
Section titled “19.2 集約の構造”FlightPlan: 不変ヘッダ+現在リビジョンへの参照FlightPlanRevision: 1リビジョン分のスナップショット(status・飛行日時・飛行領域・操縦者機体割当等、全項目を含む不変値)
19.3 状態遷移の妥当性判定
Section titled “19.3 状態遷移の妥当性判定”status(DRAFT/ACCEPTED/ACTIVATED/CANCELLED/ENDED)の遷移可否は、flight-plan-statemachine.mdの状態遷移図に従い、UseCase層で判定する(例:registerFlightPlanは対象がDRAFT以外の場合409)。FlightPlanRevision集約自身は、新しいリビジョンを組み立てる便宜メソッドを持ってよいが、遷移可否の判定ロジックは持たない。
reportStatus(DIPS通報状態軸)の遷移はreportFlightPlanとupdateFlightPlanで発生する。運航状態軸(status)とは独立しており、いずれもstatusを動かさない。遷移はFLIGHT_PLAN_STATE_EVENTへの追記として記録し、現在値は最大seqの行から導出する。
reportFlightPlan:UNREPORTED→REPORTING→REPORTED/UNREPORTED。updateFlightPlan: 内容を変更した場合のREPORTED→UNREPORTEDのリセット(BusinessLogicSpecifications.md5.2節手順3-3。通報済みの内容を変更したため再通報が必要になることを表す。内容が変わらない更新ではリビジョンを作らずREPORTEDのまま維持する。同手順3-2)。
取り下げに伴う遷移(WITHDRAWING・WITHDRAWN)は、模擬DIPSに削除APIが無いため10月デモでは発生しない。deleteFlightPlanが通報済みの飛行計画に対して行う取り下げ(BusinessLogicSpecifications.md5.5節手順1-2)も同じ理由で扱わない。
deleteFlightPlanはstatus・reportStatusのいずれの遷移も伴わない(削除可否の判定のみ行い、statusは変更せずdeleted_atを設定する。4節参照)ため、状態遷移図上の遷移としては現れない。
20. ロック制御方針
Section titled “20. ロック制御方針”飛行計画の同時更新に対する排他制御は、ADR-019に従い、FLIGHT_PLANの対象行に対する行ロック(SELECT ... FOR UPDATE)による悲観ロックとする(flight-plan-ER.mdの「飛行計画は不変リビジョンの連なりとして管理する」節と整合)。
20.1 ロック対象・範囲
Section titled “20.1 ロック対象・範囲”-
対象は
FLIGHT_PLANテーブルの対象飛行計画1行のみとする(FLIGHT_PLAN_REVISION等の子テーブルは個別にロックしない。FLIGHT_PLAN行のロックがロックの実体であるため)。 -
排他制御が必要なのは、既存の
FLIGHT_PLANの内容・状態を書き換える操作(registerFlightPlan・updateFlightPlan・deleteFlightPlan・reportFlightPlan)とする。createFlightPlanは新規FLIGHT_PLAN行のINSERTのため、既存行への排他制御は不要。listFlightPlans・getFlightPlanは参照のみのため、排他制御を行わない(通常のSELECT)。updateFlightPlanはDRAFTの分岐(新しいリビジョンを作らずFLIGHT_PLAN_DRAFT行を上書きする)でもロックを取得する。statusの読み取りから上書きまでの間に他のリクエスト(本登録・更新)が割り込むと、ロストアップデート・状態の取り違えが起きるためである。deleteFlightPlanはリビジョンを作らないが、削除可否の判定に用いるstatusの読み取りからdeleted_atの設定までの間に他のリクエスト(本登録・更新)が割り込むと、削除不可の状態へ遷移した飛行計画を削除してしまうためロックを取得する。OASの409にlockConflictの例があるのも本APIがロックを取ることを前提としている。reportFlightPlanはリビジョンを作らないが、通報状態の読み取りから遷移の確定までを排他する必要がある。外部呼び出しをロックの外へ出すため、findByIdForUpdateを2つのトランザクションでそれぞれ取り直す(20.3節)。
-
FlightPlanRepositoryに、findByIdForUpdate(FlightPlanId)(SELECT ... FOR UPDATEによる行ロック取得。論理削除済みでも返す)とadvanceToRevision(FlightPlanStateEvent)を用意する。registerFlightPlan・updateFlightPlan(ACCEPTED以降の分岐)のUseCaseは、1 UseCase = 1トランザクションの中で、まず前者により対象行をロックしたうえで現在のcurrent_revision_idを読み取り、新リビジョンのparent_revision_idにその値を記録してINSERTする。続けて同一トランザクション内(行ロックを保持したまま)でadvanceToRevisionを呼び出しUPDATE flight_plan SET current_revision_id = :new WHERE id = :idを実行する。-- 1 UseCase = 1 トランザクション(ADR-013)BEGIN;SELECT current_revision_id FROM flight_plan WHERE id = :flight_plan_id FOR UPDATE;-- 取得した current_revision_id を parent として新リビジョンを作成INSERT INTO flight_plan_revision (id, flight_plan_id, parent_revision_id, ...) VALUES (...);UPDATE flight_plan SET current_revision_id = :new_revision_id WHERE id = :flight_plan_id;COMMIT;行ロックにより読み取りから書き込みまでの間に他のリクエストが割り込むこと自体を防ぐため、CASのような更新時の条件式・影響行数チェックは不要になる。
parent_revision_idとcurrent_revision_idの一致検証は、ロストアップデート防止としてではなく、リビジョン鎖の整合性検証として別途行う。検証はドメイン(FlightPlan#requireRevisionChain(FlightPlanRevision))に置き、advanceToRevisionを呼ぶすべてのUseCase(registerFlightPlan・updateFlightPlan)がロック保持下で呼び出す。不一致はロック保持下では発生し得ないため、クライアント起因の競合(409)ではなくIllegalStateException(500)として扱う。DRAFTの分岐はリビジョンを作らないため、ロックの内側で行うのは
FLIGHT_PLAN_DRAFT行の上書きと、導出キャッシュであるFLIGHT_PLAN.updated_atの追随のみである(status・current_revision_idは変わらず、FLIGHT_PLAN_STATE_EVENTも追記しない)。updateFlightPlan(ACCEPTED以降の分岐)で、パッチ適用後の内容が現在リビジョンと同一だった場合は、ロックの内側で書き込みを一切行わない(BusinessLogicSpecifications.md5.2節手順3-2)。読み取り・組み立て・検証はロック内で行うため、同一判定に必要な現在リビジョンの内容が他のリクエストに書き換えられている可能性はない。 -
ロック待機がタイムアウトした場合(他のリクエストが同一行をロックしたまま解放しない)は、PostgreSQLセッションの
lock_timeout(値は実装時に調整。目安5秒)を超過した時点でロック取得自体が失敗するため、これをFlightPlanConflictException(409、/problems/version-conflict)に変換する。 -
ロック範囲は
FLIGHT_PLAN行の取得からcurrent_revision_idの差し替えまでとする。リビジョン作成に伴う重い処理のうち、空域制限との競合判定はロック内に含める。判定は保存済みのFLIGHT_PLAN_AREAに対して行い(BusinessLogicSpecifications.md5.1節手順2-2)、検出結果を同じリビジョンに紐づけてCONFLICT_DETECTIONへ記録するため、リビジョンの書き込みと同一トランザクションに収める必要がある。外部通信を伴わずDBに閉じた処理であり、ロックを保持する時間も1リビジョン分の空間結合1文で収まる(外部通信を含めない方針は20.2節)。
20.2 既知の限界(forDemo許容事項)
Section titled “20.2 既知の限界(forDemo許容事項)”本方式は、ロックを取得したトランザクションが完了する(コミット/ロールバックする)までブロックすることで、同一行への同時更新自体を防ぐ(ロストアップデートを防げる)。ただし以下の点に留意する。
- ロック保持中は同一飛行計画への後続の更新リクエストがブロックされるため、トランザクションを短く保つ必要がある。DIPS API呼び出しのような外部通信はロック内に含めない。 外部呼び出しを伴うのは
reportFlightPlanだけであり(registerFlightPlan・updateFlightPlan・deleteFlightPlanは伴わない)、外部の成否に応じて状態が決まるためADR-022に従い次の3段に分ける(20.3節)。deleteFlightPlanが通報済みの飛行計画に対して行うDIPS取り下げは対象外であり、将来実装する際は同じ3段構成が必要になる。 lock_timeout超過時に返す409は、「相手が先に更新を確定させた」ケースと「相手がまだ更新中で解放待ち」のケースを区別できない。区別が必要になった場合は別途検討する。- クライアントに対して明示的な「あなたの見ている内容は古い」というAPIレベルの通知手段(
versionレスポンスフィールドやIf-Matchヘッダー)は対象APIには実装しない。悲観ロック方式であっても、クライアントが古い内容を元に編集したことをUIへ積極的に伝える機能(stale read対策のUX)は同様に対象外とする。
20.3 外部呼び出しを伴う状態遷移(reportFlightPlan)
Section titled “20.3 外部呼び出しを伴う状態遷移(reportFlightPlan)”外部の成否に応じて状態が決まる遷移は、ADR-022(外部システム連携を伴う状態遷移)に従い 「中間状態への遷移 → トランザクション外での外部呼び出し → 応答による確定」の3段に分ける。 1 UseCase = 1 トランザクションの原則に対する例外であり、UseCaseのJavadocに分割の理由を残す。
- 対象行をロックして事前条件(
status=ACCEPTED・reportStatusがUNREPORTED)を確認し、REPORTINGへ 遷移させてコミットする。ここでロックを解放する。reportRequiredは事前条件にしない(通報が必須かどうかを 表すフラグであり、通報の可否を決めない)。 - トランザクションの外で模擬DIPSを呼ぶ。
- 応答を受けて対象行を取り直し、別トランザクションで
REPORTEDまたはUNREPORTEDへ確定させる。
- 手順2の間はロックを持たないため、同一計画への他の操作(更新・キャンセル)が割り込みうる。手順3は
対象を
findByIdForUpdateで取り直し、通報状態がREPORTINGでなければ確定させない (docs/implementation-guide.md「外部連携を伴う状態遷移」の例に従いFlightPlanInvalidTransitionException)。 取り直した内容で無条件に上書きすると、割り込んだ操作の結果を消すことになる。 - 手順3が失敗すると、
REPORTINGのまま滞留する。504(成否不明)と同じ状態であり、滞留した計画を 復帰させる手段は10月デモでは用意しない(ADR-022の残課題)。 - 中間状態を挟むのは、外部レイテンシの間ロックを保持しないためと、二重通報を防ぐためである
(
REPORTINGは事前条件で弾かれる)。 - 手順1・3の遷移は新しいリビジョンを作らない(通報で計画の内容は変わらない)。既存の
FlightPlanRepository.advanceToRevisionはリビジョンを伴う遷移専用(flightPlanRevisionIdが空だとIllegalArgumentException)のため、状態遷移イベントの追記だけを行うPortのメソッドを追加する。 同メソッドはFLIGHT_PLAN_STATE_EVENTへ1行追記し、導出キャッシュであるFLIGHT_PLAN.updated_atを 追随させる。status・current_revision_idは変更しない。
21. Gradleコード生成タスク構成
Section titled “21. Gradleコード生成タスク構成”飛行計画APIのコード生成は、build.gradle.ktsのOASコード生成タスク(audience動的生成方式)にそのまま従う。飛行計画専用の独立したGradleタスクは持たない。
21.1 audienceとタスクの対応
Section titled “21.1 audienceとタスクの対応”docs/openapi/<audience>/openapi.yamlというディレクトリ構成が単一の真実源であり、現時点ではfrontend(docs/openapi/frontend/openapi.yaml)のみが検出される唯一のaudienceである。flight-planning.yamlはaudience内の一部ファイルであり、frontend/openapi.yamlから$refで取り込まれる(geospatial.yaml・asset.yaml・notification.yamlと同居)。飛行計画のOASだけを対象にした個別のGenerateTask/Syncタスクは存在せず、frontendaudience全体で1回のコード生成にまとめて含まれる(運航調整機能のような3ファイル個別生成方式を採らない理由は21.5参照)。frontendはaudience一覧の先頭(現時点で唯一のaudience)のため、生成タスクはプラグイン標準のopenApiGenerateタスクをそのまま流用する。2つ目以降のaudienceが追加された時点でopenApiGenerate<Audience>という名前で新規登録される方式に切り替わる。- 同期タスクは
openApiSyncFrontendToSrc(build/generated/openapi-frontend/.../generatedをsrc/main/java/com/intent_exchange/utm/generatedへGradleSyncで反映。package-info.javaは手書き管理のため再生成時も保持される)。 - 全体の集約タスク
openApiSyncToSrc(全audience+openApiSyncProblemToSrc+openApiSyncCoordinationToSrcに依存)を実行すれば、飛行計画分も含めて再生成される。flight-planning.yamlだけを対象にした専用タスクは無いため、飛行計画APIのみを変更した場合も./gradlew openApiSyncToSrcを実行する(ArchitecturePolicy_Common.md21節と同じコマンド)。
21.2 生成先パッケージ
Section titled “21.2 生成先パッケージ”- API:
com.intent_exchange.utm.generated.api(FlightPlanningApi) - Model:
com.intent_exchange.utm.generated.model(FlightPlan関連のリクエスト/レスポンスDTO等) - いずれもaudience名を含まないフラットなパッケージ(
apiPackage/modelPackageがgenerated.{api,model}に固定されている)。geospatial・asset・notificationの生成物と同じパッケージに同居するため、スキーマ名が衝突しないようOAS側で命名する。 - 横断的関心事(
ProblemDetail・ValidationProblemDetail・FieldError)はopenApiGenerateProblemによりcom.intent_exchange.utm.problem.generated.modelへ1度だけ生成され、flight-planning.yaml側はschemaMappingsで再生成せずimportする(FlightPlanningApiのエラーレスポンス型はここに収束する)。
21.3 configOptions(飛行計画固有の追加設定はなし)
Section titled “21.3 configOptions(飛行計画固有の追加設定はなし)”飛行計画APIはfrontendaudience共通の設定(hideGenerationTimestamp・useSpringBoot4・useJakartaEe・openApiNullable=false・documentationProvider=none・useJspecify=true・interfaceOnly=true・skipDefaultInterface=true・useTags=true)をそのまま使う。飛行計画のためだけに追加したconfigOptionsは無い。
skipDefaultInterface=trueにより、FlightPlanningApiの未実装メソッドはランタイムの501ではなくコンパイルエラーとして検出される(Handler実装の実装漏れを防ぐ)。- 生成対象から外したいoperation(10月デモ対象外の
parseFlightPlanFromGcsMission・execConflictCheck等)は、OAS側の当該operationにx-internal: trueを付与することでFlightPlanningApiへの生成を防ぐ(実装対象外APIの表現方法。2026-08-10 IX社決定)。Scalar表示からはdocs/openapi/redocly/のdecoratorで別途隠す(コード生成自体はOASソースにx-internalが残ったままで動作する)。
21.4 破壊的変更検知(checkOasBreakingChanges)への影響
Section titled “21.4 破壊的変更検知(checkOasBreakingChanges)への影響”checkOasBreakingChangesタスクはfrontendaudience全体のバンドル済みドキュメント(frontend/openapi.bundled.yaml)を単位にbaseブランチと比較する。flight-planning.yamlの変更もこの1本のドキュメントに含まれるため、geospatial・asset・notification側の変更と切り分けて検知されることはない。破壊的変更が検出された場合、PRのbreaking-change-approvedラベル運用は飛行計画APIの変更単体でも適用対象になる。
21.5 運航調整機能との違い
Section titled “21.5 運航調整機能との違い”運航調整機能は他社共有のASTMプロトコル定義を含む3ファイルを、共通スキーマへ統合せずファイルごとに独立したGenerateTask/Syncタスク(build.gradle.ktsのOpenApiTargetリスト)で生成している。飛行計画はこの方式を採らない。flight-planning.yamlは自社管理のUI向けAPIのみで、既にfrontend/openapi.yamlへ$refで統合済みのため、独立タスクを設ける理由が無い。
22. 前提・仮定
Section titled “22. 前提・仮定”本飛行計画機能の実装において、本ドキュメントでは以下を仮定する。
- REST API として実装する
- DB は PostgreSQL(PostGIS拡張)を使用する
- DB アクセスは MyBatis(XML マッパー)を使用する
- ヘキサゴナルアーキテクチャ(Handler / UseCase / Domain / Port / Infrastructure)の構成とする
- Domain は Spring・MyBatis・PostGISに依存しない
@Transactionalを使わずTransactionManagerPort でトランザクションを制御する- 1 UseCase = 1 トランザクション単位
- Domain オブジェクトは Always-Valid(スマートコンストラクタで保証)
- API 仕様は OpenAPI yaml(スキーマファースト)を正とする
- DB 定義は create SQL を正とする
- エラーレスポンスは RFC 9457 Problem Details 形式とする
- ArchUnit による依存方向の自動検査を実施する
- 基本パッケージ名は既存の
com.intent_exchange.utmとする(飛行計画機能専用の基本パッケージは設けない)