CodeGym /Kursy /Java Server /POST i tworzenie w kolekcji

POST i tworzenie w kolekcji

Java Server
Poziom 7, Lekcja 2
Dostępny

1. Tworzenie jako osobna operacja

Czytanie tego, co już istnieje, to dopiero połowa historii. Tworzenie zasobu to moment, gdy „czegoś jeszcze nie ma”, ale klient już chce, żeby „to się pojawiło”. W konsolowej Javie często po prostu robimy new i dodajemy do listy — i wydaje się, że życie jest piękne. W świecie HTTP tworzenie to umowa: klient opisuje, co chce utworzyć, serwer decyduje, jak dokładnie to się pojawi, i zwraca wynik tak, żeby klient mógł dalej z tym pracować.

W domenie ReadLater tworzenie wygląda bardzo przyziemnie: użytkownik chce dodać książkę do osobistej listy do przeczytania. Wcześniej mógł mieć pustą listę. Po utworzeniu w kolekcji pojawia się nowy element, który dostaje identyfikator (żeby później dało się go odczytać, zaktualizować lub usunąć) i zestaw pól (na przykład tytuł i autor).

Wyobraź sobie zwyczajną sytuację: przychodzisz do kawiarni i mówisz „Poproszę latte i croissanta”. Nie mówisz „Utwórzcie zamówienie numer 42”. Numer zamówienia nadaje system kawiarni. W HTTP tworzenie działa podobnie: klient podaje skład zamówienia (dane przyszłego zasobu), a serwer „przybija paragon” — czyli tworzy nowy element i nadaje mu identyfikator.

Żeby zrobić to przewidywalnie i bez „wewnętrznej telepatii”, w HTTP jest metoda, której bardzo często używa się do tworzenia nowego elementu w kolekcji: POST.

2. POST i ścieżka kolekcji

Para zasobowa jest tu już znajoma: kolekcja /reading-list i pojedynczy element /reading-list/42. Dla POST z tej różnicy potrzebny jest jeden główny wniosek: tworzyć logiczniej jest przez ścieżkę kolekcji, bo nowy element nie ma jeszcze własnego adresu po id.

Dlatego typowa forma tworzenia wygląda tak:

POST /reading-list

A nie tak:

POST /reading-list/42

W drugiej wersji klient niejako próbuje z góry nazwać id. Na poziomie bazowym to zbędna odpowiedzialność: serwer sam tworzy element i przydziela mu identyfikator. Bywają kontrakty, w których klient sam przynosi id, ale dla naszego aplikacyjnego modelu uczciwiej i czytelniej przyjąć, że id nadaje serwer.

Oto mała „myślowa tabelka” do sprawdzenia samego siebie:

Czego chce klient W co celuje Dlaczego
„Chcę dostać listę” /reading-list potrzebny jest cały zbiór
„Chcę dostać jeden element” /reading-list/42 potrzebny jest konkretny element
„Chcę utworzyć nowy element” /reading-list nowy element pojawia się wewnątrz zbioru

W skrócie: POST to rozmowa z kolekcją. Pukamy „do drzwi działu kadr”, a nie „do konkretnego pracownika”, który nie został jeszcze nawet zatrudniony.

3. Treść żądania POST

Ścieżka i metoda odpowiadają na pytanie „z jakim zasobem i jaką czynnością pracujemy”. Ale tworzenie bez danych jest zwykle bez sensu: żeby dodać książkę do listy do przeczytania, trzeba przynajmniej wiedzieć, jaką książkę dodajemy.

Dlatego POST najczęściej ma body — to samo ciało żądania, które widzieliśmy już jako część HTTP request. Dziś nie jest dla nas ważny konkretny format ciała (nie omawiamy ani JSON-a, ani formularzy, ani kodowań), ważna jest idea: klient przekazuje opis przyszłego zasobu.

Na poziomie domeny ReadLater treść żądania „po ludzku” może zawierać mniej więcej takie dane:

title: tytuł książki
author: autor
comment: komentarz użytkownika (opcjonalnie)

Można sobie to wyobrazić jako prosty string (nie jako realny protokół, tylko jako ilustrację myśli):

// Metoda żądania HTTP
String method = "POST";

// Ścieżka kolekcji: tworzenie odbywa się „wewnątrz zbioru”
String path = "/reading-list";

// Treść żądania: dane przyszłego zasobu (w rzeczywistości format będzie inny, ale idea ta sama)
String body = "title=Clean Code; author=Robert C. Martin; comment=Znaleźć wydanie papierowe";

Zwróć uwagę na ważne przesunięcie myślenia. Nie wysyłamy „komendy” typu addBook. Wysyłamy opis zasobu: „na mojej liście do przeczytania powinien pojawić się wpis z takimi polami”. Serwer już sam zdecyduje, co dokładnie zachować, jaki nadać identyfikator, jakie zastosować wartości domyślne i co zwrócić w odpowiedzi.

Żeby silniej powiązać to z myśleniem javowym, można sobie wyobrazić, że body to „surowiec” do stworzenia obiektu. W programie konsolowym mógłbyś zebrać takie dane w obiekt, ale teraz nie przywiązujemy się do formatu transportu — po prostu pokazujemy, że do utworzenia potrzebny jest zestaw pól.

Możemy na przykład wyobrazić sobie „po stronie serwera” strukturę danych, którą chciałby dostać:

// Dane potrzebne do utworzenia elementu listy do przeczytania.
// Tu nie chodzi o JSON/serializację, tylko o „kontener danych wejściowych”.
record CreateReadingItemData(String title, String author, String comment) {
}

Ten record tutaj nie dotyczy JSON-a ani serializacji, tylko po prostu myśli: tworzenie to zestaw danych, który przychodzi z zewnątrz. W realnym HTTP te dane będą przyjeżdżać jako tekst w body, a serwer dopiero potem zamieni je w strukturę.

4. Przydzielanie identyfikatora przy tworzeniu

W konsolowej Javie bardzo łatwo niezauważalnie pomieszać dwie odpowiedzialności: „postanowiłem dodać element” i „sam wymyśliłem, jaki ma id”. W backendowym świecie lepiej przyzwyczajać się do innego obrazu: to serwer jest gospodarzem zasobu, on odpowiada za unikatowość i za to, by zasób istniał jako byt systemu, a nie jako fantazja klienta.

Dlatego przy tworzeniu prawie zawsze jest krok „serwer przydziela id”. Można to pokazać na prostym przykładzie w Javie, bez żadnej sieci: jakby to był fragment wewnętrznej logiki serwera.

import java.util.concurrent.atomic.AtomicLong;

// Sekwencja do generowania unikalnych identyfikatorów po stronie serwera
AtomicLong idSequence = new AtomicLong(1);

// Serwer „wystawia” nowe ID dla utworzonego elementu
long newId = idSequence.getAndIncrement();

System.out.println("newId=" + newId); // newId=1

Dlaczego jest to ważne właśnie w kontekście POST? Bo wyjaśnia, dlaczego klient nie powinien celować w /reading-list/42, kiedy tworzy element. Klient jeszcze nie wie, czy to będzie 42, 43 czy 100500 (i dobrze by było, żeby w ogóle go to nie obchodziło).

Złóżmy to teraz w krótką historyjkę. Klient wysyła POST /reading-list i „dokleja” dane. Serwer:

1. czyta dane z body żądania;
2. tworzy nowy element kolekcji;
3. przydziela mu identyfikator;
4. zwraca wynik tak, żeby klient mógł dalej adresować go jako pojedynczy zasób.

Oto prosty schemat procesu (bez statusów i nagłówków — będą osobno później, na razie tylko logika):

sequenceDiagram
    participant C as Client
    participant S as Server
    participant R as "ReadingList (collection)"

    C->>S: POST /reading-list + body z danymi książki
    S->>R: add(newItem)
    R-->>S: created item with id=42
    S-->>C: odpowiedź: utworzony element (wraz z id=42)

Zauważ, jak schludnie „rodzi się” ścieżka pojedynczego elementu: po utworzeniu pojawia się sens w /reading-list/42, bo teraz jest zasób, który można zaadresować.

5. Odpowiedź po POST

Po utworzeniu klient musi zrozumieć dwie rzeczy: po pierwsze, czy w ogóle udało się utworzenie, po drugie, co dokładnie się utworzyło. Nawet jeśli klient „i tak wie, co wysyłał”, serwer może zastosować jakieś reguły, dopełnić dane, znormalizować je albo po prostu nadać identyfikator, którego wcześniej nie było.

Dlatego typowa „zdrowa” odpowiedź po POST zawiera reprezentację utworzonego zasobu. W naszej domenie mógłby to być tekst w stylu „utworzono element listy do przeczytania o id=42, title=..., author=...”. W realnym API będzie to ustrukturyzowane ciało odpowiedzi (ale teraz nie omawiamy formatu).

Na poziomie podstawowego modelu mentalnego warto myśleć tak: POST to „wniosek o utworzenie”, a odpowiedź serwera to „pokwitowanie”, w którym jest numer i ostateczne dane.

Oto mini-przykład w Javie, który ilustruje ideę „serwer zwrócił to, co utworzył” (to nie jest kod HTTP, tylko logika):

// ID nadane przez serwer przy tworzeniu
long createdId = 42L;

// Serwer może zwrócić ostateczne dane zasobu (w tym te, które przyszły od klienta)
String createdTitle = "Clean Code";
String createdAuthor = "Robert C. Martin";

// Klient dostaje „pokwitowanie”: id + ostateczne pola utworzonego zasobu
System.out.println("Created: id=" + createdId + ", title=" + createdTitle);
// Created: id=42, title=Clean Code

Najważniejsze tu jest nie format, tylko sens: po POST pojawia się nowy element, a klient dostaje możliwość zwracania się do niego jak do osobnego zasobu.

Jeśli chcemy bardzo krótko powiązać GET i POST w jeden „przepływ”, wychodzi taka logika:

POST /reading-list tworzy element i zwraca go (wraz z id).
GET /reading-list/id pozwala potem odczytać ten element, używając nadanego identyfikatora.

Nie omawiamy jeszcze, jaki konkretnie status zostanie zwrócony i jakie nagłówki będą ważne. Tutaj potrzebna nam jest sama zasada: po POST klient musi zobaczyć, że zasób się pojawił i jak się do niego dalej zwrócić. Statusy liczbowe, Location i reszta semantyki response to już osobna warstwa kontraktu HTTP; bez tego tworzenie łatwo zamienia się w „czarną dziurę”.

6. POST i ścieżka zasobu

W tym miejscu warto trzymać tylko jedno przypomnienie: POST /reading-list czyta się lepiej niż /addBookToReadingList, bo ścieżka nadal nazywa zasób, a czynność wyraża sama metoda. Jeśli schowamy tworzenie w adresie, API znowu zsuwa się w zestaw komend.

Do zwykłego scenariusza CRUD ta zasada wystarczy: jest kolekcja, po utworzeniu element dostaje własny adres, a metody pokazują, co dokładnie z tym zasobem robimy.

Powtórki POST i duplikaty

POST ma jedną bardzo praktyczną niedogodność: powtórzenie tego samego żądania może utworzyć drugi taki sam element. To nie jest bug metody, tylko normalna semantyka tworzenia — każdy POST znów prosi o dodanie zasobu do kolekcji.

Tu wystarczy zapamiętać sam fakt ryzyka duplikatów: jeśli klient, UI albo sieć powtórzą tworzenie, wynik może się rozmnożyć mocniej, niż się spodziewałeś. Dlatego pytanie „co stanie się przy powtórzeniu żądania?” dla metod HTTP w ogóle nie jest ozdobne.

7. Typowe błędy przy pracy z POST

Błąd nr 1: wysyłanie POST na ścieżkę pojedynczego zasobu z „wymyślonym” id.
Wygląda to logicznie, jeśli w myślach chcesz wszystko kontrolować, ale zwykle łamie podstawowy model: klient nie powinien zgadywać identyfikatora. W myśleniu zasobowym tworzenie odbywa się na kolekcji, a konkretne id pojawia się jako wynik. Jeśli bardzo chce się zarządzać id, powinna to być osobna, świadoma decyzja kontraktu, a nie „no, tak czuję”.

Błąd nr 2: próba wyrażenia tworzenia samą ścieżką, bez treści żądania.
Nowicjusze czasem piszą coś w stylu POST /reading-list/clean-code w nadziei, że „adres sam wszystko powie”. To szybko zamienia API w dziwny zestaw reguł. Do utworzenia zwykle potrzebne są dane: tytuł, autor, komentarz, status itd. Ścieżka powinna pozostać prosta i stabilna, a dane — przyjeżdżać w body.

Błąd nr 3: zamienianie POST w uniwersalną metodę „na wszystko, co niejasne”.
Jeśli masz nawyk „nie wiem, co zrobić — wyślę POST”, API wychodzi mgliste. Klientowi trudno przewidywać zachowanie, a serwerowi — utrzymywać kontrakt. Nawet bez głębokiej znajomości pozostałych metod warto trzymać w głowie prostą myśl: POST to przede wszystkim tworzenie nowego elementu w kolekcji, a nie „magiczny przycisk”.

Błąd nr 4: brak myślenia o wyniku tworzenia dla klienta.
Czasem serwer „tworzy coś u siebie”, a klientowi zwraca pustkę albo mętny tekst. W efekcie klient nie wie, co się pojawiło, jaki ma identyfikator i jak się do niego dalej zwrócić. Nawet bez wchodzenia w statusy i nagłówki warto trzymać regułę: po utworzeniu klient musi dostać dość informacji, żeby kontynuować pracę z nowym zasobem.

Błąd nr 5: niedostrzeganie ryzyka duplikatów przy ponownym wysłaniu żądania.
Jeśli w myślach traktujesz POST jako coś w stylu „ustaw wartość”, zdziwisz się, gdy powtórka utworzy drugi element. To nie jest „podstawa HTTP”, tylko normalna semantyka tworzenia. Warto z góry przyjąć ten model, żeby potem nie projektować API i zachowania klienta na fałszywym założeniu „powtórki zawsze są bezpieczne”.

1
Zadanie
Java Server, poziom 7, lekcja 2
Niedostępne
Tekstowa reprezentacja żądania POST do utworzenia
Tekstowa reprezentacja żądania POST do utworzenia
1
Zadanie
Java Server, poziom 7, lekcja 2
Niedostępne
Tworzenie elementów z serwerowym identyfikatorem
Tworzenie elementów z serwerowym identyfikatorem
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION