Bezpieczne przy konkurencji numerowanie faktur, które unika duplikatów i luk
Poznaj praktyczne wzorce bezpiecznego przy konkurencji numerowania faktur, aby wielu użytkowników mogło tworzyć faktury lub zgłoszenia bez duplikatów i niespodziewanych luk.

Co idzie nie tak, gdy dwie osoby tworzą rekordy jednocześnie
Wyobraź sobie zatłoczone biuro o 16:55. Dwie osoby kończą fakturę i klikają Zapisz w odstępie sekundy. Na obu ekranach chwilowo pojawia się 'Faktura nr 1042'. Jeden zapis wygrywa, drugi kończy się błędem, albo jeszcze gorzej — obie zostają zapisane z tym samym numerem. To najczęstszy objaw: duplikaty, które wychodzą na jaw dopiero przy obciążeniu.
Zgłoszenia (tickets) zachowują się podobnie. Dwaj agenci tworzą nowe zgłoszenie dla tego samego klienta w tym samym czasie, a system próbuje "wybrać następny numer" patrząc na ostatni rekord. Jeśli oba żądania odczytają tę samą wartość "ostatnią" przed zapisaniem, mogą obydwa przypisać ten sam następny numer.
Drugi objaw jest bardziej subtelny: pominięte numery. Możesz zobaczyć #1042, potem #1044, a #1043 będzie brakować. Często pojawia się to po błędzie lub ponowieniu próby. Jedno żądanie rezerwuje numer, a zapis kończy się niepowodzeniem z powodu błędu walidacji, timeoutu lub użytkownik zamyka kartę. Albo zadanie w tle próbuje ponownie i pobiera nowy numer, mimo że pierwsza próba już "zużyła" jeden.
W przypadku faktur to ważne, bo numeracja jest częścią śladu audytowego. Księgowi oczekują unikalnej identyfikacji faktur, a klienci mogą odwoływać się do numerów przy płatnościach czy zgłoszeniach. W zgłoszeniach numer jest uchwytem używanym w rozmowach, raportach i eksportach. Duplikaty wprowadzają zamieszanie. Brakujące numery rodzą pytania podczas przeglądów, nawet jeśli nie doszło do nieuczciwości.
Kluczowa oczekiwana wartość, którą warto ustawić od początku: nie każda metoda numerowania może jednocześnie być bezpieczna przy konkurencji i bez luk. Bezpieczne numerowanie przy konkurencji (brak duplikatów nawet przy wielu użytkownikach) jest osiągalne i powinno być niepodważalne. Numeracja bez luk też jest możliwa, ale wymaga dodatkowych zasad i często zmienia sposób obsługi szkiców, błędów i anulowań.
Dobry sposób, by ująć problem, to zapytać, co liczby mają gwarantować:
- Nigdy się nie powtarzać (zawsze unikalne)
- Powinny być w większości rosnące (mile widziane)
- Nie mogą pomijać numerów (tylko jeśli tak zaprojektujesz)
Po wybraniu reguły, dobór rozwiązania technicznego staje się prostszy.
Dlaczego powstają duplikaty i luki
Większość aplikacji stosuje prosty wzorzec: użytkownik klika Zapisz, aplikacja pyta o następny numer faktury lub zgłoszenia, potem wstawia nowy rekord z tym numerem. Działa to idealnie, gdy działa jedna osoba.
Kłopoty zaczynają się, gdy dwa zapisy zdarzają się niemal jednocześnie. Obydwa żądania mogą dojść do kroku "pobierz następny numer" zanim któreś skończy wstawianie. Jeśli oba odczytają tę samą wartość, spróbują zapisać ten sam numer. To warunek wyścigu: wynik zależy od czasu, nie od logiki.
Typowy przebieg wygląda tak:
- Żądanie A odczytuje następny numer: 1042
- Żądanie B odczytuje następny numer: 1042
- Żądanie A wstawia fakturę 1042
- Żądanie B wstawia fakturę 1042 (lub zawodzi, jeśli baza blokuje duplikaty)
Duplikaty pojawiają się, gdy nic w bazie nie zatrzymuje drugiego insertu. Jeśli w aplikacji jedynie sprawdzasz "czy numer jest zajęty?" przed wstawieniem, nadal możesz przegrać wyścig między sprawdzeniem a zapisem.
Luki to inny problem. Powstają, gdy system "rezerwuje" numer, ale rekord nigdy nie staje się prawdziwą, zatwierdzoną fakturą lub zgłoszeniem. Typowe przyczyny to nieudane płatności, błędy walidacji znalezione późno, timeouty lub użytkownik porzucający kartę. Nawet jeśli insert się nie powiedzie i nic nie zostanie zapisane, numer może już zostać skonsumowany.
Ukryta konkurencja pogarsza sprawę, bo rzadko jest to tylko "dwóch ludzi klikających Zapisz". Możesz mieć też:
- Klienty API tworzące rekordy równolegle
- Importy uruchamiane w batchach
- Zadania w tle generujące faktury w nocy
- Ponowienia z aplikacji mobilnych przy słabym łączu
Zatem przyczyny to: (1) konflikty czasowe, gdy wiele żądań odczytuje tę samą wartość licznika, oraz (2) numery przydzielane zanim wiemy, że transakcja się powiedzie. Każdy plan musi zdecydować, jaki wynik jesteś w stanie zaakceptować: brak duplikatów, brak luk, albo oba, i w jakich okolicznościach (szkice, ponowienia, anulowania).
Zdecyduj regułę numerowania zanim wybierzesz rozwiązanie
Zanim zaprojektujesz bezpieczne przy konkurencji numerowanie, zapisz, co numer ma znaczyć w twoim biznesie. Najczęstszy błąd to wybór metody technicznej, a potem odkrycie, że reguły księgowe lub prawne oczekują czegoś innego.
Zacznij od rozdzielenia dwóch celów, które często się mieszają:
- Unikalność: żadna dwie faktury lub zgłoszenia nie mają tego samego numeru.
- Brak luk (gapless): numery są unikalne i ściśle kolejne (bez braków).
Wiele systemów celuje w samo unikatowe i akceptuje luki. Luki zdarzają się z normalnych powodów: użytkownik tworzy szkic i porzuca go, płatność się nie udaje po zarezerwowaniu numeru, rekord jest tworzony i potem anulowany. W zgłoszeniach luki zwykle nie mają znaczenia. Nawet w fakturach wiele zespołów akceptuje luki, jeśli można je wyjaśnić śladem audytowym (unieważnione, anulowane, testowe itp.). Numeracja bez luk jest możliwa, ale wymusza dodatkowe reguły i często zwiększa tarcia.
Następnie zdecyduj zakres licznika. Małe różnice w sformułowaniu zmieniają projekt:
- Jedna globalna sekwencja dla wszystkiego, czy osobne sekwencje na poziomie firmy/tenanta?
- Resetować co rok (2026-000123) czy nigdy?
- Różne serie dla faktur vs not kredytowych vs zgłoszeń?
- Potrzebujesz formatu przyjaznego dla ludzi (prefiksy, separatory), czy tylko numeru wewnętrznego?
Przykład: SaaS z wieloma klientami może wymagać numerów unikalnych dla każdej firmy i resetu co rok, podczas gdy zgłoszenia są unikalne globalnie i nigdy się nie resetują. To są dwa różne liczniki z różnymi regułami, choć w UI wyglądają podobnie.
Jeśli naprawdę potrzebujesz braku luk, bądź konkretny co do zdarzeń, które są dozwolone po przypisaniu numeru. Na przykład: czy faktura może być usunięta, czy tylko unieważniona? Czy użytkownicy mogą zapisywać szkice bez numeru i przydzielać numer dopiero przy zatwierdzeniu? Te wybory zwykle mają większe znaczenie niż technika bazy danych.
Zapisz regułę w krótkim specyfikacjach przed budową:
- Jakie typy rekordów korzystają z sekwencji?
- Co sprawia, że numer jest "użyty" (szkic, wysłane, opłacone)?
- Jaki jest zakres (globalny, na firmę, na rok, na serię)?
- Jak obsługujesz unieważnienia i korekty?
W AppMaster ten rodzaj reguły warto trzymać obok modelu danych i procesu biznesowego, aby zespół implementował jednako zachowanie w API, web UI i mobilnie.
Popularne podejścia i co każde z nich gwarantuje
Gdy mówimy o "numeracji faktur", często mieszamy dwa cele: (1) nie generować tego samego numeru dwukrotnie, i (2) nie mieć luk. Większość systemów bez trudu zagwarantuje pierwszy. Drugi jest trudniejszy, bo luki mogą powstać przy każdej nieudanej transakcji, porzuconym szkicu czy unieważnieniu.
Podejście 1: Sekwencja w bazie (szybka unikalność)
Sekwencja PostgreSQL to najprostszy sposób, by uzyskać unikalne, rosnące numery pod obciążeniem. Skalowalność jest dobra, bo baza jest zbudowana tak, by szybko wydawać wartości sekwencji, nawet przy wielu równoczesnych tworzeniach.
Co dostajesz: unikalność i kolejność (głównie rosnące). Czego nie dostajesz: braku luk. Jeśli insert się nie powiedzie po przydzieleniu numeru, ten numer jest "spalony" i zobaczysz lukę.
Podejście 2: Unikalny indeks + ponawianie (niech baza zdecyduje)
Generujesz kandydacki numer (w logice aplikacji), zapisujesz go i polegasz na constraints UNIQUE, która odrzuci duplikat. W razie konfliktu próbujesz ponownie z nowym numerem.
To może działać, ale przy dużej konkurencji staje się głośne: więcej ponowień, więcej nieudanych transakcji i trudniejsze do debugowania skoki. Nie gwarantuje też braku luk bez dodatkowych zasad rezerwacji, co dodaje złożoności.
Podejście 3: Wiersz licznika z blokowaniem (cel: bez luk)
Jeśli naprawdę potrzebujesz numerów bez luk, typowy wzorzec to dedykowana tabelka liczników (po jednym wierszu na zakres, np. na rok lub firmę). Blokujesz ten wiersz w transakcji, inkrementujesz i używasz nowej wartości.
To najbliżej braku luk w klasycznym projekcie bazodanowym, ale ma koszt: tworzy "gorący punkt", na którym wszyscy zapisujący muszą czekać. Zwiększa też ryzyko przy błędach operacyjnych (długie transakcje, timeouty, deadlocki).
Podejście 4: Osobny serwis rezerwacji numerów (dla specjalnych przypadków)
Osobny "serwis numerujący" centralizuje reguły między wieloma aplikacjami lub bazami. Ma sens, gdy masz wiele systemów wydających numery i nie możesz scalić zapisu.
Kosztem jest ryzyko operacyjne: kolejny serwis musi być poprawny, wysoko dostępny i spójny.
Praktyczna tabela gwarancji:
- Sekwencja: unikatowe, szybkie, akceptuje luki
- Unikalność + ponawianie: unikatowe, proste przy niskim obciążeniu, może thrashować przy dużym
- Zablokowany wiersz licznika: może być bez luk, wolniejsze przy dużym współbiegu
- Osobny serwis: elastyczne między systemami, najwyższa złożoność i tryby awarii
W narzędziu no-code takim jak AppMaster wybory są te same: poprawność leży w bazie danych. Logika aplikacji może pomagać przy ponowieniach i czytelnych błędach, ale ostateczna gwarancja powinna pochodzić z ograniczeń i transakcji w bazie.
Krok po kroku: zapobieganie duplikatom za pomocą sekwencji i unikalnych ograniczeń
Jeśli głównym celem jest zapobieganie duplikatom (nie gwarantowanie braku luk), najprostszy solidny wzorzec to: pozwól bazie wygenerować wewnętrzne ID i wymuś unikalność na kolumnie numeru widocznego dla klienta.
Rozdziel pojęcia. Użyj wartości generowanej przez bazę (identity/sekwencja) jako klucza głównego do joinów, edycji i eksportów. Trzymaj invoice_no lub ticket_no jako osobną kolumnę pokazywaną użytkownikom.
Praktyczne ustawienie w PostgreSQL
Poniżej powszechne podejście PostgreSQL, które trzyma logikę "następny numer" w bazie, gdzie konkurencja jest obsługiwana poprawnie.
-- Internal, never-shown primary key
create table invoices (
id bigint generated always as identity primary key,
invoice_no text not null,
created_at timestamptz not null default now()
);
-- Business-facing uniqueness guarantee
create unique index invoices_invoice_no_uniq on invoices (invoice_no);
-- Sequence for the visible number
create sequence invoice_no_seq;
Teraz generuj numer wyświetlany podczas INSERT (nie przez "select max(invoice_no) + 1"). Jednym prostym wzorcem jest sformatowanie wartości sekwencji w INSERT:
insert into invoices (invoice_no)
values (
'INV-' || lpad(nextval('invoice_no_seq')::text, 8, '0')
)
returning id, invoice_no;
Nawet jeśli 50 użytkowników kliknie "Create invoice" jednocześnie, każdy insert dostanie inną wartość sekwencji, a unikalny indeks zablokuje przypadkowe duplikaty.
Co zrobić przy kolizji
Przy zwykłej sekwencji kolizje są rzadkie. Zazwyczaj występują, gdy dodasz dodatkowe reguły jak "reset na rok", "na tenanta" albo edytowalne przez użytkownika numery. Dlatego unikalne ograniczenie nadal jest ważne.
Na poziomie aplikacji obsłuż błąd naruszenia unikalności pętlą ponawiania:
- Spróbuj insertu
- Jeśli otrzymasz błąd unique constraint na invoice_no, spróbuj ponownie
- Zatrzymaj po ograniczonej liczbie prób i wyświetl czytelny komunikat
Działa to dobrze, bo ponawiania są wyzwalane tylko przy wyjątkowych zdarzeniach, np. dwóch dróg kodu produkujących ten sam sformatowany numer.
Trzymaj okno wyścigu małe
Nie obliczuj numeru w UI i nie "rezerwuj" numerów odczytując najpierw, a zapisując potem. Generuj numer jak najbliżej zapisu w bazie.
W AppMaster z PostgreSQL możesz modelować id jako identity primary key w Data Designer, dodać unikalne ograniczenie dla invoice_no i generować invoice_no podczas tworzenia, tak by działo się to razem z insert.
Krok po kroku: zbuduj bezlukowy licznik z blokowaniem wiersza
Jeśli naprawdę potrzebujesz numerów bez luk, możesz użyć transakcyjnej tabeli liczników i blokowania wiersza. Idea: tylko jedna transakcja naraz może pobrać następny numer dla danego zakresu, więc numery są wydawane po kolei.
Najpierw zdecyduj zakres. Wiele zespołów potrzebuje osobnych sekwencji na firmę, rok lub serię (np. INV vs CRN). Tabela liczników przechowuje ostatni użyty numer dla każdego zakresu.
Oto praktyczny wzorzec z użyciem blokad wiersza w PostgreSQL:
- Stwórz tabelę
number_countersz kolumnamicompany_id,year,series,last_numberi unikalnym kluczem na(company_id, year, series). - Rozpocznij transakcję bazodanową.
- Zablokuj wiersz licznika dla twojego zakresu używając
SELECT last_number FROM number_counters WHERE ... FOR UPDATE. - Oblicz
next_number = last_number + 1, zaktualizuj wiersz nalast_number = next_number. - Wstaw wiersz faktury lub zgłoszenia używając
next_number, potem commit.
Kluczowe jest FOR UPDATE. Pod obciążeniem nie dostaniesz duplikatów. Nie dostaniesz też luk wynikających z "dwóch użytkowników pobierających ten sam numer", bo drugiej transakcji nie pozwolono odczytać i zwiększyć tego samego wiersza, dopóki pierwsza nie zatwierdzi. Zamiast duplikatu druga transakcja po prostu chwilę poczeka. Ta chwila to cena za brak luk.
Inicjalizacja nowego zakresu
Potrzebujesz też planu na pierwszy raz, gdy pojawi się nowy zakres (nowa firma, nowy rok, nowa seria). Dwie opcje:
- Wstępnie twórz wiersze liczników (np. utwórz wiersze na kolejny rok w grudniu).
- Twórz w locie: spróbuj wstawić wiersz licznika z
last_number = 0, a jeśli już istnieje, przejdź do normalnego flow lock-and-increment.
W no-code (AppMaster) trzymaj cały "lock, increment, insert" w jednej transakcji w logice biznesowej, żeby albo wszystko się udało, albo nic.
Krawędziowe przypadki: szkice, nieudane zapisy, anulowania i edycje
Większość błędów numerowania pojawia się w nieporządnych częściach: szkice, które nigdy nie zostają opublikowane, zapisy kończące się błędem, faktury unieważnione i rekordy edytowane po tym, jak ktoś już widział numer. Jeśli chcesz bezpieczne numerowanie, potrzebujesz jasnej reguły, kiedy numer staje się "prawdziwy".
Największa decyzja to timing. Jeśli przypisujesz numer w momencie, gdy ktoś klika "Nowa faktura", będziesz mieć luki spowodowane porzuconymi szkicami. Jeśli przypisujesz dopiero przy finalizacji (posted, issued, sent lub jakikolwiek "final" w twoim biznesie), możesz utrzymać numery bliżej siebie i łatwiej to wyjaśnić.
Nieudane zapisy i rollbacki to punkt, gdzie oczekiwania często zderzają się z zachowaniem bazy. Przy typowej sekwencji, gdy numer zostanie pobrany, jest uznawany za pobrany nawet jeśli transakcja później się wycofa. To normalne i bezpieczne, ale tworzy luki. Jeśli polityka wymaga braku luk, numer musisz przypisać tylko na ostatnim kroku i tylko jeśli transakcja się zatwierdzi — zwykle oznacza to zablokowanie jednego wiersza licznika, zapisanie finalnego numeru i commit w jednej jednostce. Jeśli krok się nie powiedzie, nic nie zostaje przypisane.
Anulowania i unieważnienia prawie nigdy nie powinny "ponownie używać" numeru. Zachowaj numer i zmień status. Audytorzy i klienci oczekują, że historia pozostanie spójna, nawet przy korektach.
Edycje są prostsze: gdy numer jest widoczny na zewnątrz, traktuj go jako trwały. Nigdy nie numeruj faktury lub zgłoszenia na nowo po tym, jak został udostępniony, wyeksportowany lub wydrukowany. Jeśli potrzebna jest korekta, utwórz nowy dokument i odwołaj się do starego (np. nota kredytowa lub dokument zastępczy), ale nie przepisywaj historii.
Praktyczny zestaw reguł często stosowany:
- Szkice nie mają finalnego numeru (użyj wewnętrznego ID lub 'DRAFT').
- Przypisz numer tylko przy 'Post/Issue', wewnątrz tej samej transakcji co zmiana statusu.
- Unieważnienia i anulowania zachowują numer, ale mają jasny status i powód.
- Wydrukowane/emailed numery nigdy się nie zmieniają.
- Importy zachowują oryginalne numery i aktualizują licznik do następującej bezpiecznej wartości.
Migracje i importy wymagają szczególnej uwagi. Jeśli przenosisz dane z innego systemu, przenieś istniejące numery bez zmian, a licznik ustaw tak, by startował za maksymalną zaimportowaną wartością. Zdecyduj też, co zrobić z konfliktującymi formatami (różne prefiksy na lata). Zwykle lepiej przechować numer wyświetlany dokładnie tak, jak był i trzymać oddzielny wewnętrzny klucz główny.
Przykład: helpdesk tworzy zgłoszenia szybko, ale wiele to szkice. Przypisz numer tylko gdy agent kliknie "Wyślij do klienta". To zapobiega marnowaniu numerów na porzucone szkice i utrzymuje widoczną sekwencję zgodną z realną komunikacją z klientem. W no-code (AppMaster) ta sama zasada: szkice bez publicznego numeru, generowanie finalnego numeru podczas kroku "submit" w procesie biznesowym, który zatwierdza się jako jedna transakcja.
Typowe błędy powodujące duplikaty lub zaskakujące luki
Większość problemów numerowania wynika z jednego prostego założenia: traktowania numeru jak wartości wyświetlanej zamiast stanu współdzielonego. Gdy kilka osób zapisuje jednocześnie, system potrzebuje jednego jasnego miejsca, które decyduje o następnym numerze, i jednej jasnej reguły co się dzieje przy błędzie.
Klasyczny błąd to używanie SELECT MAX(number) + 1 w kodzie aplikacji. Wygląda dobrze przy jednym użytkowniku, ale dwa żądania mogą odczytać ten sam MAX zanim któreś się zatwierdzi. Oba generują ten sam next value i masz duplikat. Nawet z "sprawdź potem ponów" możesz stworzyć dodatkowe obciążenie i dziwne skoki przy dużym ruchu.
Innym źródłem duplikatów jest generowanie numeru po stronie klienta (przeglądarka lub mobilnie) przed zapisem. Klient nie wie, co robią inni użytkownicy i nie może bezpiecznie zarezerwować numeru, jeśli zapis się nie powiedzie. Numery generowane po stronie klienta są OK jako tymczasowe etykiety typu "Szkic 12", ale nie do oficjalnych faktur czy numerów zgłoszeń.
Luki zaskakują zespoły, które zakładają, że sekwencje są bez luk. W PostgreSQL sekwencje są projektowane pod kątem unikalności, nie doskonałej ciągłości. Numery mogą być pominięte, gdy transakcja się wycofa, gdy prefetchniesz ID, lub po restarcie bazy. To normalne. Jeśli twoim wymogiem jest "brak duplikatów", sekwencja + unikalne ograniczenie to często właściwe rozwiązanie. Jeśli naprawdę potrzebujesz "bezlukowości", musisz użyć innego wzorca (zwykle blokowania wiersza) i zaakceptować kompromisy w przepustowości.
Blokowanie też może być problemem, gdy jest zbyt szerokie. Jedna globalna blokada dla całej numeracji zmusza każdą operację tworzenia do stania w kolejce, nawet jeśli można by podzielić liczniki wg firmy, lokalizacji czy typu dokumentu. To spowalnia system i sprawia, że zapisy wydają się "losowo" zablokowane.
Błędy, które warto sprawdzić implementując numerowanie:
- Używanie
MAX + 1bez unikalnego ograniczenia na poziomie bazy. - Generowanie finalnych numerów po stronie klienta i próba "naprawiania" konfliktów później.
- Oczekiwanie, że sekwencje PostgreSQL będą bez luk i traktowanie luk jako błędów.
- Blokowanie jednego współdzielonego licznika dla wszystkiego zamiast partycjonowania liczników.
- Testowanie tylko z jednym użytkownikiem, więc warunki wyścigu wychodzą dopiero po starcie.
Praktyczny test: uruchom równoległą próbę tworzenia 100–1000 rekordów i sprawdź duplikaty oraz nieoczekiwane luki. W no-code (AppMaster) ta sama zasada: upewnij się, że finalny numer przypisywany jest w jednej transakcji po stronie serwera, nie w UI.
Szybkie kontrole przed wdrożeniem
Przed wdrożeniem numeracji faktur lub zgłoszeń, przejrzyj elementy, które zwykle zawodzą przy prawdziwym ruchu. Cel jest prosty: każdy rekord ma dokładnie jeden numer biznesowy, a reguły są zachowane nawet gdy 50 osób kliknie "Create" naraz.
Lista kontrolna przed wdrożeniem:
- Potwierdź, że pole numeru biznesowego ma unikalne ograniczenie w bazie (nie tylko w UI). To ostatnia linia obrony przy kolizjach.
- Upewnij się, że numer jest przypisywany wewnątrz tej samej transakcji, która zapisuje rekord. Jeśli przypisanie numeru i zapis są rozdzielone na różne żądania, prędzej czy później zobaczysz duplikaty.
- Jeśli wymagasz bezlukowości, przypisuj numer dopiero przy finalizacji (np. wydanie faktury), a nie przy tworzeniu szkicu. Szkice, porzucone formularze i nieudane płatności to najczęstsze źródła luk.
- Dodaj strategię ponawiania przy rzadkich konfliktach. Nawet z blokowaniem wiersza lub sekwencją możesz napotkać błędy serializacji, deadlocki lub naruszenia unikalności w skrajnych przypadkach. Proste ponowienie z krótkim backoffem często wystarcza.
- Przetestuj obciążenie z 20–100 jednoczesnymi tworzeniami ze wszystkich punktów wejścia: UI, publiczne API i importy. Testuj realistyczne scenariusze typu nagłe skoki, wolne sieci i podwójne wysłania.
Szybki sposób walidacji: zasymuluj moment dużej aktywności helpdesku: dwóch agentów otwiera formularz "Nowe zgłoszenie", jeden wysyła z weba, w tym samym czasie import z maila wstawia bilety. Po teście sprawdź, że wszystkie numery są unikalne, w prawidłowym formacie i że nie ma pół-zapisanych rekordów.
W AppMaster te same zasady: przypisywanie numeru w transakcji bazy, poleganie na ograniczeniach PostgreSQL i testowanie zarówno akcji UI, jak i endpointów API tworzących ten sam typ encji. To miejsce, gdzie wiele zespołów czuje się bezpiecznie ręcznie, ale zostaje zaskoczonych, gdy użytkownicy przyjdą jednocześnie.
Przykład: zatłoczony helpdesk i co dalej
Wyobraź sobie support, gdzie agenci tworzą zgłoszenia cały dzień w aplikacji webowej, a integracja jednocześnie tworzy zgłoszenia z czatu i e-maili. Wszyscy oczekują numerów typu T-2026-000123 i każdy numer ma wskazywać dokładnie jedno zgłoszenie.
Naive podejście to: odczytaj 'ostatni numer zgłoszenia', dodaj 1, zapisz nowe zgłoszenie. Pod obciążeniem dwa żądania mogą odczytać ten sam 'ostatni numer' zanim któreś zapisze. Oba policzą ten sam następny numer i masz duplikaty. Jeśli próbujesz to "naprawić" przez ponawianie po błędzie, często tworzysz luki bez sensu.
Baza danych może zatrzymać duplikaty nawet jeśli kod aplikacji jest naiwne. Dodaj unikalne ograniczenie na kolumnę ticket_number. Gdy dwa żądania próbują tego samego numeru, jeden insert się nie powiedzie i możesz elegancko ponowić. To sedno bezpiecznej numeracji: niech baza wymusza unikalność, nie UI.
Numeracja bez luk zmienia workflow. Jeśli wymagane są brak luk, zwykle nie możesz przypisywać finalnego numeru podczas tworzenia zgłoszenia (szkic). Twórz zgłoszenie ze statusem Draft i bez ticket_number. Numer przypisuj tylko przy finalizacji, żeby porzucone szkice i nieudane zapisy nie marnowały numerów.
Prosty projekt tabel:
- tickets: id, created_at, status (Draft, Open, Closed), ticket_number (nullable), finalized_at
- ticket_counters: key (np. "tickets_2026"), next_number
W AppMaster możesz wymodelować to w Data Designer i zbudować logikę w Business Process Editor:
- Create Ticket: insert ticket with status=Draft and no ticket_number
- Finalize Ticket: start a transaction, lock the counter row, set ticket_number, increment next_number, commit
- Test: run two "Finalize" actions at the same time and confirm you never get duplicates
Co dalej: zacznij od reguły (tylko unikalność vs rzeczywisty brak luk). Jeśli możesz zaakceptować luki, sekwencja w bazie + unikalne ograniczenie zwykle wystarczą i upraszczają flow. Jeśli musisz mieć brak luk, przenieś numerowanie do kroku finalizacji i traktuj "draft" jako pierwszorzędny stan. Potem testuj obciążenie wieloma agentami i integracjami, żeby zobaczyć zachowanie przed wejściem w produkcję.


