0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RestController ⇄ Wijmo FlexGrid 連携メモ(実務レシピ)

Posted at

目的:ブラウザ(Wijmo FlexGrid)で編集したデータを安全かつ確実に Spring @RestController に送信・適用するための実務チェックリストとサンプル。


1. 高レベルフロー(要点)

  1. クライアントで差分を取得(view.trackChanges = trueitemsAdded/itemsEdited/itemsRemoved
  2. 送信前に正規化(Date → ISO、不要キー除去、キーソートは任意)+軽いスキーマ検証
  3. fetchContent-Type: application/json を付けて送信
  4. サーバーは @RequestBody を受け、Jacksonで DTO に変換
  5. PUT はフル更新(必須チェック)、PATCH は差分マージ(readerForUpdating
  6. バリデーション、楽観ロック、ID生成、結果返却、UI反映

2. クライアント(Wijmo + プレーンJS)の最低限の実装

A. 差分を集めて送る(簡潔)

view.trackChanges = true;

function makePayload(view) {
  return {
    added:  view.itemsAdded || [],
    edited: view.itemsEdited || [],
    removed: view.itemsRemoved || []
  };
}

async function saveChanges(view) {
  const payload = makePayload(view);
  // 正規化を推奨(下記 normalize を参照)
  const res = await fetch('/api/items/batch', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

B. 軽い正規化ユーティリティ(送る前に)

function normalize(obj) {
  if (obj == null) return obj;
  if (obj instanceof Date) return obj.toISOString();
  if (Array.isArray(obj)) return obj.map(normalize);
  if (typeof obj !== 'object') return obj;
  return Object.keys(obj).sort().reduce((acc,k)=>{
    acc[k] = normalize(obj[k]);
    return acc;
  }, {});
}
// 使い方: body = JSON.stringify(normalize(payload))

C. 送信前検証(推奨)

  • 軽量:手書きチェック(必須フィールドの有無)
  • 中量:zod / io-ts 等でスキーマを作り parse() して弾く
  • 重量:OpenAPI から生成した型で静的保障 + AJV でランタイム検証

3. サーバー側(Spring)での安全実装パターン

A. ObjectMapper 設定(推奨初期設定)

@Bean
public ObjectMapper objectMapper() {
  var om = new ObjectMapper();
  om.registerModule(new JavaTimeModule()); // JSR310
  om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
  om.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
  return om;
}
  • FAIL_ON_UNKNOWN_PROPERTIES = true でクライアントの余分なキーを拒否
  • JavaTimeModuleLocalDateTime 等を扱う

B. DTO の基本方針

  • プリミティブ型は使わずラッパーを使う(Integer / Long / Boolean)→ 欠損を null として扱う
  • 必須は @NotNull 等で明示
  • フィールド名は JSON と一致(@JsonProperty でマッピング可能)

C. バッチ受け取り(例)

public class ItemDto { /* id, title, completed, ... */ }
public class BatchItemsDto { List<ItemDto> added, edited, removed; }

@PostMapping("/api/items/batch")
public ResponseEntity<?> batch(@RequestBody BatchItemsDto batch) {
  // サービスに任せる
  var result = service.applyBatch(batch);
  return ResponseEntity.ok(result);
}

D. PATCH(差分マージ)の安全な実装

@Autowired ObjectMapper mapper;
@Autowired Validator validator; // javax.validation

@PatchMapping("/api/items/{id}")
public ResponseEntity<?> patch(@PathVariable Long id, @RequestBody JsonNode patchNode) throws Exception {
  Item entity = service.findById(id);
  ItemDto dto = service.toDto(entity);

  // 送られたキーだけ既存 DTO にマージ
  mapper.readerForUpdating(dto).readValue(patchNode);

  // バリデーション
  var violations = validator.validate(dto);
  if (!violations.isEmpty()) return ResponseEntity.badRequest().body(violations);

  service.updateFromDto(dto);
  return ResponseEntity.ok(...);
}
  • readerForUpdating は送られてないフィールドをそのままにしてくれる
  • @Valid を PATCH にそのまま使うと誤検出しやすいのでマージ後に明示的に検証する

4. 受信 JSON と DTO の「同型性検査」について

文字列比較ではなく、パースしてツリー比較(JsonNode.equals)を使う。

@PostMapping("/api/debug/check")
public ResponseEntity<?> check(@RequestBody JsonNode incoming) throws Exception {
  JsonNode original = incoming;
  ItemDto dto = mapper.treeToValue(incoming, ItemDto.class);
  JsonNode dtoNode = mapper.valueToTree(dto);
  boolean same = dtoNode.equals(original);
  return ResponseEntity.ok(Map.of("same", same));
}
  • ただし FAIL_ON_UNKNOWN_PROPERTIES の設定に依存する動作に注意
  • dtoNode.equals(original) が false の場合、差分をログに吐いて原因を追う(フォーマットの違い、欠損、型の違い、日付形式など)

5. 日付・形式の問題回避(実務Tips)

  • 送信は date.toISOString()(UTC ISO8601)で統一
  • Spring は JavaTimeModule を有効化
  • 入力で "2025-08-21T07:00:00Z" を受け取る想定にする

6. ID生成・新規行の扱い

  • 新規行(クライアントで ID が無い)は added として送る
  • サーバーで ID を発行し、更新後の DTO を返してクライアントで差し替える

例: サーバー返却

{ "added": [{"tempId":"t-1","id":123,...}], "edited": [...], "removed": [...] }
  • tempId を付けておくと置換が楽

7. 競合(並行更新)対策

  • 楽観ロック(JPA の @Version)を導入
  • 衝突検出時は 409 Conflict を返し、クライアントに差分解決を促す

8. エラー設計

  • バリデーションエラー → 400 Bad Request と詳細 JSON
  • 権限/認可 → 403/401
  • 楽観ロック競合 → 409 Conflict とサーバー側最新データ
  • サーバー障害 → 5xx
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?