# 業務ロジック -- OwnerController.processFindForm() [26 LOC]

| 項目 | 値 |
|-------|-------|
| 完全修飾名 | `org.springframework.samples.petclinic.owner.OwnerController` |
| レイヤー | Controller |
| モジュール | `owner` (パッケージ: `org.springframework.samples.petclinic.owner`) |

## 1. 役割

### OwnerController.processFindForm()

本メソッドは、ペットクリニック管理システムにおける「飼い主（Owner）検索」画面のエントリポイントとして機能する。URLマッピング `/owners` に対する GET リクエストを受け付け、入力された姓（lastName）をキーに飼い主データを検索し、結果件数に応じて適切なビューへのリダイレクトまたは結果一覧画面へのフォワードを決定する。

業務的には、飼い主の「姓」部分一致検索サービスを提供する。検索キーワードが空文字列の場合は未登録の全レコードを対象とした広範な検索とし、キーワード指定時は「指定文字列で始まる姓」を条件に部分一致検索を実行する。検索結果がゼロ件の場合はエラーメッセージを付与して検索画面を再表示し、1件のみ一致した場合は該当飼い主の詳細画面へリダイレクトする。2件以上一致した場合はページネーション付きの結果一覧画面へ遷移する。

デザインパターンとしては、Spring MVC の `@Controller` によるリクエストディスパッチパターンおよびデコレートルールパターン（結果件数に応じた分岐ハンドリング）を採用している。また、検索ロジックとビューレンダリングを分離するため、内部で `findPaginatedForOwnersLastName` および `addPaginationModel` の2つのプライベートメソッドを呼び出す委譲構造となっている。

本メソッドはシステム全体の共有検索入口であり、飼い主管理画面から頻繁に呼び出される。検索条件の緩急（空文字列 vs 半角空白のみ vs 具体的な姓）によって検索範囲を制御する点は、エンドユーザーの検索操作性に直結する重要なビジネスロジックである。

## 2. 処理パターン（詳細業務ロジック）

```mermaid
flowchart TD
    START(["processFindForm: 開始"])
    START --> GET_LAST_NAME["lastName = owner.getLastName()"]
    GET_LAST_NAME --> CHECK_NULL{lastName is null?}
    CHECK_NULL -->|true| SET_EMPTY["lastName = 空文字列"]
    CHECK_NULL -->|false| FIND_OWNER["findPaginatedForOwnersLastName"]
    SET_EMPTY --> FIND_OWNER
    FIND_OWNER --> CHECK_EMPTY{is empty?}
    CHECK_EMPTY -->|true| REJECT_VALUE["result.rejectValue エラー"]
    REJECT_VALUE --> RETURN_FORM["return owners/findOwners"]
    CHECK_EMPTY -->|false| CHECK_COUNT{count == 1?}
    CHECK_COUNT -->|true| GET_OWNER["owner = iterator.next()"]
    GET_OWNER --> REDIRECT["redirect:/owners/id"]
    CHECK_COUNT -->|false| MULTIPLE["addPaginationModel"]
    MULTIPLE --> RETURN_LIST["return owners/ownersList"]
```

| ブランチ | 条件 | 処理内容 | 戻り値 |
|----------|------|----------|--------|
| ブランチ 1 | `lastName == null` | 検索キーワード未入力を検知し、空文字列にデフォルト値を設定。広範な検索モードへ移行。 | `null` (継続) |
| ブランチ 2 | `ownersResults.isEmpty() == true` | 姓検索の結果がゼロ件。エラーコード `"notFound"` を `BindingResult` に設定。検索画面を再表示。 | `"owners/findOwners"` |
| ブランチ 3 | `ownersResults.getTotalElements() == 1` | 所有者が1件のみ一致。該当 Owner エンティティを取得し、詳細画面へリダイレクト。 | `"redirect:/owners/" + owner.getId()` |
| ブランチ 4 | `ownersResults.getTotalElements() > 1` | 所有者が2件以上一致。ページネーション情報をモデルに付加し、結果一覧画面を表示。 | `"owners/ownersList"` |

## 3. パラメータ分析

| No | パラメータ名 | 型 | 業務的説明 |
|----|---------------|------|-------------------|
| 1 | `page` | `@RequestParam(defaultValue = "1") int` | ページネーションの対象ページ番号。1-origin で指定され、デフォルト値は 1（最初のページ）。検索結果が複数件となった場合、このページ番号に基づいて結果を 5件/ページ で割付けた上で該当ページのレコード群をビューに渡す。 |
| 2 | `owner` | `Owner` | 検索条件として渡される飼い主オブジェクト。主に `lastName`（姓）フィールドが検索キーワードとして利用される。フォームから送信されたユーザー入力がバインドされる。 |
| 3 | `result` | `BindingResult` | Spring MVC のバインディング結果オブジェクト。検索結果がゼロ件の場合にエラーコード `"notFound"` を `lastName` フィールドに対して設定し、検索画面にエラーメッセージを返すために使用される。 |
| 4 | `model` | `Model` | Spring MVC のモデルオブジェクト。複数件検索結果の場合、ページネーション情報（現在ページ番号、総ページ数、総アイテム数、飼い主リスト）を格納し、一覧画面の表示データを構成する。 |

**インスタンスフィールド / 外部状態:**

| フィールド名 | 型 | 業務的説明 |
|-------------|------|----------------|
| `owners` | `OwnerRepository` | JPA の Owner リポジトリ。姓による部分一致検索を実行する永続化レイヤーの抽象化。 |

## 4. CRUD操作／呼び出しサービス

### コード解析グラフからの事前抽出エビデンス:

| CRUD | SC / CBS | SCコード | エンティティ / DB | 操作の説明 |
|------|----------|---------|-------------|----------------------|
| R | `Owner.getLastName` | Owner | - | 検索条件として飼い主の姓を取得 |
| R | `OwnerController.findPaginatedForOwnersLastName` | OwnerController | Owner | 姓による部分一致ページネーション検索を呼び出し |
| R | `OwnerRepository.findByLastNameStartingWith` | OwnerRepository | KK_T_OWNER (owners テーブル) | JPA リポジトリによる姓前方一致検索（DB 読取） |
| R | `Page.isEmpty` | Page | - | 検索結果が空かどうか判定 |
| R | `Page.getTotalElements` | Page | - | 検索結果の総件数を取得 |
| R | `Page.iterator` / `Iterator.next` | Page/Iterator | - | ページ結果から単一の Owner オブジェクトを取得 |
| W | `BindingResult.rejectValue` | BindingResult | - | エラーメッセージをバインディング結果に設定（UI 表示用） |
| C | `OwnerController.addPaginationModel` | OwnerController | Owner | ページネーションモデルを構築し、リスト・ページ情報をモデルに付加 |

**詳細:**

| CRUD | SC / CBS | SCコード | エンティティ / DB | 操作の説明 |
|------|----------|---------|-------------|----------------------|
| R | `findPaginatedForOwnersLastName` | OwnerController | KK_T_OWNER (owners) | ページング対象の飼い主リストを姓前方一致で取得。ページサイズは 5。 |
| R | `findByLastNameStartingWith` | OwnerRepository | KK_T_OWNER (owners) | JPA による DB レベルでの姓前方一致検索。Pageable によるページネーションを適用。 |
| W | `rejectValue` | BindingResult | - | 検索結果ゼロ件時に「見つかりません」エラーを画面に表示。 |
| C | `addPaginationModel` | OwnerController | KK_T_OWNER (owners) | ページネーション用モデルを構築。現在ページ、総ページ数、総アイテム数、オーナーリストを付加。 |
| R | `Owner.getId` | Owner | - | 単一一致時に詳細画面へのリダイレクトURLを生成するためにIDを取得。 |

## 5. 依存関係トレース

本メソッドを呼び出す呼び出し元は、テストクラスからのユニットテスト呼び出しのみが確認されている。本メソッドは Spring MVC コントローラであり、実際の呼び出しはフロントエンド（ブラウザ）からの HTTP GET リクエスト `/owners` を通じて Spring MVC フレームワークが行う。

| # | 呼び出し元（画面/バッチ） | 呼び出しチェーン（本メソッドまでの全経路） | 終端（SC / CRUD / エンティティ） |
|---|----------------------|--------------------------------------|-------------------------------|
| 1 | Screen: ブラウザ GET /owners | `DispatcherServlet` -> `OwnerController.processFindForm` -> `findPaginatedForOwnersLastName` -> `findByLastNameStartingWith` | `findByLastNameStartingWith [R] KK_T_OWNER (owners)` |
| 2 | テスト: OwnerControllerTests | `OwnerControllerTests.processFindFormSuccess` -> `mockMvc.perform(get("/owners"))` -> `processFindForm` | `findByLastNameStartingWith [R] KK_T_OWNER (owners)` |
| 3 | テスト: OwnerControllerTests | `OwnerControllerTests.processFindFormByLastName` -> `mockMvc.perform(get("/owners").param("lastName", "Franklin"))` -> `processFindForm` | `findByLastNameStartingWith [R] KK_T_OWNER (owners)` |
| 4 | テスト: OwnerControllerTests | `OwnerControllerTests.processFindFormNoOwnersFound` -> `mockMvc.perform(get("/owners").param("lastName", "Unknown Surname"))` -> `processFindForm` | `findByLastNameStartingWith [R] KK_T_OWNER (owners)` |

## 6. 分岐ごとの詳細ブロック

### ブロック 1 -- GET リクエスト受取・姓の取得 (L95-L98)

検索フォームから送信された姓パラメータを取得する初期ブロック。

| # | 種別 | コード |
|---|------|------|
| 1 | CALL | `owner.getLastName()` // 検索条件の姓を取得 |
| 2 | SET | `lastName = owner.getLastName()` // 姓をローカル変数に格納 |

### ブロック 2 -- NULL 判定とデフォルト値設定 (L99-L101)

入力された姓が null である場合、広範な検索（全件検索）を意味する空文字列にデフォルト値を設定する。

**ブロック 2** -- IF `(lastName == null)` [NULL チェック] (L99)

> 姓が null の場合、空文字列にデフォルト値を設定して全件検索モードとする。

| # | 種別 | コード |
|---|------|------|
| 1 | SET | `lastName = ""` // 空文字列 - 広範な検索を示す |

**ブロック 2.2** -- ELSE `(lastName != null)` (L99-101)

> 姓が null でない場合はそのまま検索条件として使用し、分岐を抜ける。

| # | 種別 | コード |
|---|------|------|
| 1 | -- | (処理なし。元の lastName の値を維持して次のブロックへ) |

### ブロック 3 -- ページネーション付き検索実行 (L104-L105)

取得した姓をキーに、ページネーション付きの飼い主検索を実行する。

| # | 種別 | コード |
|---|------|------|
| 1 | CALL | `findPaginatedForOwnersLastName(page, lastName)` // 姓によるページネーション検索 |
| 2 | SET | `ownersResults = findPaginatedForOwnersLastName(page, lastName)` // 検索結果を保持 |

### ブロック 4 -- 検索結果ゼロ件の判定とエラー処理 (L106-L110)

検索結果が空の場合、エラーメッセージを付与して検索画面を再表示する。

**ブロック 4** -- IF `(ownersResults.isEmpty())` [結果ゼロ件] (L106)

> 該当する飼い主が見つからなかった場合、エラーコード `notFound` を設定して検索画面へ戻る。

| # | 種別 | コード |
|---|------|------|
| 1 | EXEC | `result.rejectValue("lastName", "notFound", "not found")` // 姓フィールドにエラーメッセージ設定 |
| 2 | RETURN | `return "owners/findOwners"` // 検索画面を再表示 |

**ブロック 4.2** -- ELSE `(ownersResults.isEmpty() == false)` (L106-118)

> 1件以上検索結果がある場合、詳細な件数判定へ進み、エラー設定は行わない。

| # | 種別 | コード |
|---|------|------|
| 1 | -- | (結果があるためエラー設定跳过。次の件数判定ブロックへ) |

### ブロック 5 -- 1件のみの判定と詳細画面リダイレクト (L112-L116)

検索結果がちょうど1件の場合、該当の飼い主詳細画面へリダイレクトする。

**ブロック 5** -- IF `(ownersResults.getTotalElements() == 1)` [1件一致] (L112)

> 所有者が1件のみ一致したため、該当Ownerオブジェクトを取得し、詳細画面へリダイレクトする。

| # | 種別 | コード |
|---|------|------|
| 1 | CALL | `ownersResults.iterator()` // イテレータを取得 |
| 2 | CALL | `iterator.next()` // 1件目のOwnerを取得 |
| 3 | SET | `owner = ownersResults.iterator().next()` // Ownerエンティティを更新 |
| 4 | CALL | `owner.getId()` // 詳細画面遷移用のOwner IDを取得 |
| 5 | RETURN | `return "redirect:/owners/" + owner.getId()` // 詳細画面へリダイレクト |

**ブロック 5.2** -- ELSE `(ownersResults.getTotalElements() != 1)` (L112-L118)

> 2件以上の所有者が見つかった場合、ページネーション一覧画面へ遷移するため分岐する。

| # | 種別 | コード |
|---|------|------|
| 1 | -- | (複数件のため、次のブロックへ) |

### ブロック 6 -- 複数件のページネーション結果表示 (L118-L119)

検索結果が2件以上の場合、ページネーションモデルを構築して一覧画面に遷移する。

| # | 種別 | コード |
|---|------|------|
| 1 | CALL | `addPaginationModel(page, model, ownersResults)` // ページネーションモデルを構築 |
| 2 | RETURN | `return addPaginationModel(page, model, ownersResults)` // 一覧画面（owners/ownersList）へ遷移 |

> `addPaginationModel` は内部で `paginated.getContent()` により Owner リストを取得し、`currentPage`、`totalPages`、`totalItems`、`listOwners` の4 attributes を `Model` に付加した上で `"owners/ownersList"` を返す。

## 7. 用語集

| 用語 | 種別 | 業務的意味 |
|------|------|------------------|
| `Owner` | Entity | 飼い主エンティティ。ペットクリニックにおける飼育主の個人情報（姓、名、住所、電話番号等）を表現するドメインモデル。`Person` クラスを継承し、`lastName`（姓）フィールドを持つ。 |
| `Person` | Entity | 個人情報基本クラス。`Owner` の親クラス。`firstName`、`lastName` 等の基本個人情報を保持する。 |
| `lastName` | Field | 姓フィールド。Owner 検索の主要な検索キーとして利用される。前方一致検索の対象となる。 |
| `BindingResult` | Framework | Spring MVC のバインディング・バリデーション結果オブジェクト。フォームから送信されたデータのバインディングエラーや、ビジネスロジックによるエラーメッセージを保持する。 |
| `Model` | Framework | Spring MVC のモデルオブジェクト。Controller から View へデータを引き渡すためのマップ。ページネーション情報や検索結果リストを格納する。 |
| `Page<Owner>` | Framework | Spring Data のページネーション結果オブジェクト。検索結果のチャンク（ページ）を表し、総件数・総ページ数・現在のコンテンツリスト等へのアクセスを提供する。 |
| `Pageable` | Framework | Spring Data のページネーション仕様オブジェクト。ページ番号・ページサイズを指定し、DB レベルでの分页クエリを生成する。 |
| `PageRequest.of(page - 1, pageSize)` | Framework | Spring Data による Pageable インスタンス生成メソッド。ページ番号は 0-origin（ユーザが指定した page から 1 を減算）。`pageSize` は 5 で固定。 |
| `findByLastNameStartingWith` | Repository | JPA リポジトリメソッド。指定した文字列で始まる姓を持つ Owner を検索する。`LIKE 'xxx%'` パターンに相当する前方一致検索を内部的に実行する。 |
| `OwnerRepository` | DAO | JPA リポジトリインターフェース。Owner エンティティに対する永続化操作（検索、保存等）の抽象化。`JpaRepository<Owner, Integer>` を継承する。 |
| `KK_T_OWNER` | DB Table | 飼い主マスタテーブル。Owner エンティティとマッピングされるDBテーブル。 |
| `owners/findOwners` | View | 飼い主検索画面のビュー名。検索フォームと検索結果エラーメッセージを表示する。 |
| `owners/ownersList` | View | 飼い主検索結果一覧画面のビュー名。ページネーション付きの検索結果リストを表示する。 |
| `owners/ownerDetails` | View | 飼い主詳細画面のビュー名。単一 Owner の詳細情報を表示する。 |
| `/owners` | Endpoint | 飼い主検索のエントリポイントURL。GET リクエストによって `processFindForm` メソッドが呼び出される。 |
| `notFound` | Error Code | 検索結果ゼロ件時のエラーコード。`BindingResult` に設定され、検索画面で「見つかりません」メッセージとして表示される。 |
| `addPaginationModel` | Internal Method | ページネーションモデル構築のプライベートメソッド。検索結果リストとページネーション情報を Model に付加し、一覧画面ビュー名を返す。 |
| `findPaginatedForOwnersLastName` | Internal Method | 姓によるページネーション検索のプライベートメソッド。ページサイズ5で検索を実行し、`Page<Owner>` 結果を返す。 |