CodeGym /Kursy /Spring REST & MVC /Projektowanie TaskPatchRequest

Projektowanie TaskPatchRequest

Spring REST & MVC
Poziom 13, Lekcja 1
Dostępny

1. TaskPatchRequest jako osobne DTO

Gdy po raz pierwszy zetkniesz się z PATCH, bardzo chce się zrobić „na szybko": wziąć już istniejący TaskCreateRequest lub nawet TaskDetailsResponse, wyrzucić kilka pól i powiedzieć: „No i mam patch DTO". To normalny ludzki odruch: mózg lubi ponowne użycie. Ale w kontraktowym API ponowne użycie często zamienia się w ukryty wyciek sensu: DTO zaczyna żyć jednocześnie w kilku scenariuszach i tracisz możliwość uczciwego opisania, co dokładnie jest dozwolone klientowi, a czego nie.

Główna idea tu jest prosta: TaskPatchRequest — to model transportowy do częściowej zmiany. Ma inny cel niż modele create/update. Create mówi: „Oto dane do stworzenia zasobu". Detail response mówi: „Oto jak zasób wygląda na zewnątrz". Patch mówi: „Oto jakie kawałki zasobu chcę zmienić". To zupełnie różne gatunki tekstu, mniej więcej jak „przepis", „zdjęcie potrawy" i „lista zamienników składników". Próba zrobienia jednego DTO na wszystkie przypadki — to jak drukowanie CV, umowy i kartki do mamy jednym szablonem Word.

Jest jeszcze jedna ważna kwestia: patch DTO nie jest „okrojonym pełnym modelem". Jest celowo wąski. Powinny się w nim znaleźć tylko te pola, które naprawdę jesteśmy gotowi zmieniać „punktowo" i które rozumiemy, jak zmieniać bez dwuznaczności.

2. Pola dla patch DTO

Przed napisaniem record TaskPatchRequest(...), warto się zatrzymać i zrobić to, czego programiści zazwyczaj nie lubią: przez chwilę pomyśleć. PATCH — to historia o „zmieniam właśnie to", więc musimy odpowiedzieć na pytanie: co klient ma prawo zmieniać częściowo?

Pola zarządzane przez serwer to nie „tajne pola dla elity". To po prostu pola zadawane przez system: identyfikator, momenty czasu, czasem wartości obliczalne. Klasyczne pola server-managed w naszym projekcie:

Pole modelu wewnętrznego Task Skąd pochodzi „prawda" Powinno być w TaskPatchRequest? Dlaczego
id serwer (UUID) nie identyfikator jest zadawany przez serwer i adresuje zasób przez path
createdAt serwer nie server-managed, nie zmieniane przez klienta
updatedAt serwer nie server-managed, obliczane po zmianach
status logika dziedzinowa zazwyczaj nie (w ramach dnia) przejścia statusów często wymagają osobnej logiki/reguł, lepiej nie mieszać z patch pól
title klient tak część zmienialnego stanu (ale z własnymi regułami)
description klient tak pole opcjonalne, typowy „ulubieniec" scenariuszy patch
assigneeName klient tak może być przypisany/wyczyszczony
dueDate klient tak wygodny przykład pola często zmienianego osobno
priority klient tak enum, dobrze wpisuje się w patch
tags klient tak ale z jawną semantyką (zazwyczaj „zamiana całej listy")

3. TaskPatchRequest w kodzie

Patch DTO w naszym kursie pozostaje typizowany. To nie „dynamiczny worek JSON", lecz staranna model wejścia. Dlatego pola wybieramy zgodnie z tym, co pokazywaliśmy w JSON-baseline: ciągi pozostają ciągami, daty — LocalDate, wyliczenia — enum, kolekcje — List<String>.

Oto kanoniczny przykład:

package com.example.tasktracker.api.dto.request;

import java.time.LocalDate;
import java.util.List;

import com.example.tasktracker.domain.model.TaskPriority;

/**
 * DTO dla PATCH: wszystkie pola są opcjonalne na poziomie modelu Java.
 * Sam DTO nie rozróżnia `absent` i `explicit null` — tę granicę zadaje kontrakt
 * i sposób odczytu wejścia.
 */
public record TaskPatchRequest(
        // Nowa nazwa zadania (jeśli klient chce zmienić właśnie ją)
        String title,
        // Nowy opis (typowy scenariusz: dodać/poprawić tekst)
        String description,
        // Wykonawca: może być zadany lub wyczyszczony (zależnie od reguł obsługi null)
        String assigneeName,
        // Termin wykonania: data lokalna bez czasu
        LocalDate dueDate,
        // Priorytet: enum, żeby uniknąć „magicznych ciągów" na wejściu
        TaskPriority priority,
        // Tagi: zazwyczaj traktowane jako „zastąp całą listę", a nie „dodaj jeden tag"
        List<String> tags
) {
}

Świadomie robimy pola nullable. W modelu patch „opcjonalność" — to nie „wszystko nam jedno", lecz to, że reprezentacja Java musi dopuszczać brak wartości. Ale sam DTO nie przechowuje faktu obecności pola w JSON, dlatego absent i explicit null nie można sklecić w jedną frazę „null = nie zmieniaj".

4. taskId: path osobno od body

Bardzo chce się dodać do TaskPatchRequest pole id, żeby „było wygodnie". To klasyka: „żeby w jednym obiekcie było wszystko". Ale kontrakt REST właśnie próbuje uwolnić nas od takiego „wszystko w jednym". W naszym API adres zasobu — to path, a opis zmiany — to body.

Jeśli taskId przychodzi i w path, i w body, pojawia się pytanie: „A jeśli są różne, komu wierzyć?". I nagle piszesz logikę porównywania, błędy w stylu „id mismatch", i wszystko to tylko dlatego, że kiedyś zdecydowałeś „zróbmy wygodnie".

Normalny model wygląda tak:

flowchart TD
    %% Adres zasobu (path) i dane zmiany (body) nie mieszamy w jednym DTO
    A["PATCH /api/v1/tasks/{taskId}"] --> B["taskId (path) = adres zasobu"]
    A --> C["body = TaskPatchRequest = zmiany"]
    C --> D["title/description/... (część może nie być obecna)"]

Przykład kontrolera w stylu projektu:

package com.example.tasktracker.api.controller;

import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.tasktracker.api.dto.request.TaskPatchRequest;
import com.example.tasktracker.api.dto.response.TaskDetailsResponse;

@RestController
public class TaskController {

    @PatchMapping("/api/v1/tasks/{taskId}")
    public TaskDetailsResponse patchTask(
            // Identyfikator zasobu pobieramy z path: odpowiada za „co zmieniamy"
            @PathVariable String taskId,
            // DTO w body odpowiada tylko za „jak zmieniamy"
            @RequestBody TaskPatchRequest request
    ) {
        // Tu ważna jest sama granica: path zadaje zasób, body opisuje zmiany.
        // Reguły merge i sprawdzenia wejścia żyją dalej w łańcuchu.
        throw new UnsupportedOperationException("Not implemented");
    }
}

5. Odpowiedź po PATCH

Jest pokusa: „Klient przysłał TaskPatchRequest, no to zwrócimy mu z powrotem TaskPatchRequest — oszczędność!". Oszczędność, oczywiście, jest. Oszczędność sensu. Po PATCH klient zazwyczaj chce wiedzieć, jakim zasób się stał. A patch DTO — to nie „zasób stał się", to „co próbowaliśmy zmienić".

Dlatego zewnętrzny kontrakt zdrowego API wygląda tak: wejście — patch DTO, wyjście — pełnoprawne response DTO (zazwyczaj model detail), w którym serwer pokazuje aktualny stan zasobu po zmianie.

Dlatego trzymamy dyscyplinę architektoniczną: w endpoincie PATCH będziemy zwracać TaskDetailsResponse (lub analogiczny model detail response). To nie jest „zbędne". To sprawia, że API jest przewidywalne: po każdej zmianie klient może dostać potwierdzenie, że serwer zrozumiał go poprawnie.

Antyprzykład: response DTO na wejściu

Utrwalmy zły przykład, bo czasem bez złego przykładu dobry się nie zapamięta. Mamy TaskDetailsResponse (response DTO), i zdecydowaliśmy: „A przyjmijmy go jako request, przecież już jest".

package com.example.tasktracker.api.controller;

import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

import com.example.tasktracker.api.dto.response.TaskDetailsResponse;

public class BadTaskController {

    @PatchMapping("/api/v1/tasks/{taskId}")
    public TaskDetailsResponse patchTask(
            // Nawet jeśli id bierzemy z path, problem pozostaje: body staje się zbyt „szerokie"
            @PathVariable String taskId,
            // ŹLE: response DTO na wejściu => klient może przysyłać pola server-managed i zbędne dane
            @RequestBody TaskDetailsResponse request
    ) {
        // Zaślepka: przykład potrzebny jako ilustracja antywzorca, a nie jako działający kod
        throw new UnsupportedOperationException();
    }
}

Na zewnątrz wygląda to „logicznie". Ale kontraktowo oznacza: klient teraz może przysyłać wszystko, co pokazywałeś w detail response. A detail response prawie na pewno zawiera pola server-managed (id, createdAt, updatedAt) i być może pola, których w ogóle nie chcesz zmieniać w taki sposób. Właśnie zrobiłeś API szerokie i trudne do zarządzania.

6. Typowe błędy przy projektowaniu TaskPatchRequest

Błąd nr 1: „Zrobię patch DTO kopią detail response, tylko bez kilku pól".
Takie podejście prawie zawsze prowadzi do rozszerzania kontraktu bez Twojej zgody. Detail response i patch input mają różne cele. Jeśli je mylisz, sam sobie tworzysz przyszłe breaking changes i problemy z over-posting.

Błąd nr 2: dodawanie pól server-managed „dla wygody".
Najczęściej dotyczy to id, createdAt, updatedAt. W patch DTO nie ma dla nich miejsca. Prawidłowy model: taskId przychodzi przez path, timestamp-y aktualizuje serwer.

Błąd nr 3: próba zrobienia „uniwersalnego DTO na wszystkie operacje".
Jedno DTO na create, put, patch, response — brzmi jak oszczędność, ale w efekcie to wyłączenie dyscypliny kontraktowej. API staje się mętne. W naszym kursie podejście odwrotne: jedna operacja — jeden jasny model wejścia/wyjścia.

Błąd nr 4: włączanie „skomplikowanych" pól bez uzgodnienia sensu.
Na przykład status zadania. Jeśli włączysz status do patch DTO bez reguł, bardzo szybko dostaniesz „PATCH zmienia status jak mu się podoba", a potem próbę przykręcenia ograniczeń post factum. Lepiej trzymać patch DTO skupione na polach, które naprawdę wygodnie zmieniać częściowo.

Błąd nr 5: mieszanie adresu zasobu i opisu zmiany.
Jeśli taskId (lub id) przychodzi jednocześnie i w path, i w body, jesteś zmuszony rozwiązywać konflikt wartości. To sztuczny problem. Adres zasobu powinien być w path, zmiany — w body. Tak kontrakt czyta się prościej zarówno dla człowieka, jak i dla kodu klienta.

1
Zadanie
Spring REST & MVC, poziom 13, lekcja 1
Niedostępne
Oddzielny `TaskPatchRequest` dla endpointu PATCH
Oddzielny `TaskPatchRequest` dla endpointu PATCH
1
Zadanie
Spring REST & MVC, poziom 13, lekcja 1
Niedostępne
Patch DTO dla profilu autora bez pól zarządzanych przez serwer
Patch DTO dla profilu autora bez pól zarządzanych przez serwer
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION