はじめに
「更新(UPDATE)」の実装を始めると、意外と迷いどころが多いことに気づきます。
この記事でわかること:
- 更新処理が登録処理より複雑な理由
- PUT と PATCH の設計上の使い分け
- URL 設計パターン
- 登録コンポーネントの再利用方針
更新処理が難しい理由
登録処理との本質的な違い
登録処理(POST)と更新処理(PUT/PATCH)の一番大きな違いは、処理の出発点です。
| 処理 | 出発点 | DB 操作 |
|---|---|---|
| 登録(POST) | 何もないゼロの状態 | INSERT |
| 更新(PUT/PATCH) | すでにデータが存在している状態 | UPDATE |
登録処理ではフォームに入力された値をそのまま保存すればよかったのが、更新処理では「今すでに何が保存されているか」を把握した上で変更を加える必要があります。
「現在のデータを取得してから更新する」が必要な理由
更新画面を開くとき、ユーザーは「今の値」を見ながら変更箇所を編集します。そのため、画面表示のために既存データを取得する GET リクエストが更新より先に走ります。
# 更新処理のおおまかな流れ
1. GET /products/42 ← 現在の値を取得して画面に表示(初期値の pre-fill)
2. ユーザーが値を変更する
3. PUT /products/42 ← 変更後の値をサーバーへ送信して更新
登録処理では手順 1 がありません。この「データを取得してフォームに pre-fill する」ステップが、更新処理を複雑に感じさせる主な要因です。
「変更していないフィールド」をどう扱うか
例えば商品情報(名前・価格・在庫数)のうち、ユーザーが「価格だけ変更して保存」したとします。このとき、名前と在庫数はどうなるべきでしょうか?
- 全フィールドを送る(PUT):変更していないフィールドも含めて全体を送信する。サーバーはすべてを上書きする。
- 変更フィールドだけ送る(PATCH):変更があったフィールドのみ送信する。サーバーは受け取ったフィールドだけを上書きする。
この違いが PUT と PATCH の使い分けに直結します。
URL設計:更新エンドポイントをどう書くか
ID はパスパラメータで指定する
どのリソースを更新するかは、URL のパスパラメータで識別します。
PUT /products/{id} # 商品の全体更新
PATCH /products/{id} # 商品の部分更新
PUT /users/{id} # ユーザーの全体更新
PATCH /orders/{id} # 注文ステータスの変更
{id} の部分に対象リソースの一意な識別子(数値IDやUUID)を入れます。
具体的な URL 設計例
マスタ系とトランザクション系でエンドポイントを整理すると次のようになります。
# マスタ系(商品マスタ)
GET /products # 一覧取得
GET /products/42 # 1件取得
POST /products # 新規登録
PUT /products/42 # 全体更新
PATCH /products/42 # 部分更新
DELETE /products/42 # 削除
# トランザクション系(注文)
GET /orders # 一覧取得
GET /orders/1001 # 1件取得
POST /orders # 新規登録
PATCH /orders/1001 # ステータス変更など部分更新
PUTとPATCHの使い分け
PUT はリソース全体を置き換える
PUT は「リクエストで送ったデータでリソースを丸ごと置き換える」メソッドです。べき等(idempotent) であり、同じリクエストを何度送っても結果は同じになります。
PUT /products/42 HTTP/1.1
Content-Type: application/json
{
"name": "高機能マウス",
"price": 3500,
"stock": 100
}
このリクエストを送ると、/products/42 のデータはリクエストボディの内容に完全に置き換わります。リクエストに含まれていないフィールドは null になる(または削除される) と考えてください。
PATCH はフィールドを部分的に更新する
PATCH は「指定したフィールドだけを変更する」メソッドです。
PATCH /products/42 HTTP/1.1
Content-Type: application/json
{
"price": 3200
}
このリクエストは price だけを 3200 に変更し、name や stock は現在の値を保持します。
どちらを選ぶか
以下のフローで判断するとスムーズです。
更新リクエストを設計するとき
↓
フォームに全フィールドが表示されて一括保存するUIか?
↓
YES → PUT(全体置換)
NO → 特定フィールドだけを変更するUIか?
↓
YES → PATCH(部分更新)
実際の使い分け例:
| 操作 | メソッド | 理由 |
|---|---|---|
| 商品編集画面で全項目を保存 | PUT | 全フィールドを送信するため |
| 注文のステータスだけ「発送済み」に変更 | PATCH | 一つのフィールドだけを変える |
| ユーザープロフィールの一部を更新 | PATCH | 変更箇所だけを送れば十分 |
| 設定オブジェクトをまるごと上書き | PUT | 既知の状態にリセットしたい |
登録処理のコンポーネントをどこまで再利用できるか
バリデーションロジック:基本的に共通化できる
「商品名は必須」「価格は0以上」といったバリデーションルールは、登録時も更新時も同じです。バリデーション関数・クラスは共通化して再利用するのが原則です。
ただし、更新時のみ追加が必要なバリデーションがあります:
- 存在確認:更新対象のIDが本当にDBに存在するか
- 所有者確認:ログインユーザーが更新権限を持つリソースか(認可チェック)
- 状態確認:既にキャンセル済みの注文は更新できない、などのビジネスルール
フォームコンポーネント(フロントエンド):初期値の有無で分岐
React や Vue などのフロントエンドフレームワークでは、登録フォームと更新フォームを同じコンポーネントで実装するのが一般的です。コンポーネントが initialValues(初期値)を受け取るかどうかで動作を切り替えます。
// 登録時:initialValues なし
<ProductForm onSubmit={handleCreate} />
// 更新時:初期値を渡す(GETで取得したデータをセット)
<ProductForm
initialValues={currentProduct}
onSubmit={handleUpdate}
/>
フォームの入力項目・バリデーション・UIは共通、サブミット時の処理(POST か PUT/PATCH か)だけを切り替える、という設計が保守しやすいです。
サービス層・DB操作:INSERT と UPDATE は分ける
バックエンドのサービス層(ビジネスロジック)は、登録と更新を別メソッドとして分けることを推奨します。
# 良い例:明確に分ける
ProductService.create(data) # INSERT
ProductService.update(id, data) # UPDATE
# 避けたい例:曖昧な upsert
ProductService.save(data) # IDがあれば UPDATE、なければ INSERT → 意図が不明確
まとめ
この記事で学んだポイントを振り返ります。
- 更新処理が難しい理由:既存データを取得して初期値を設定する「2ステップ」が必要なため
- PUT vs PATCH:全体置換には PUT、部分更新には PATCH を使う。
- コンポーネント再利用:バリデーション・フォームUIは共通化できるが、サービス層の INSERT/UPDATE は分ける