Pracownik poprawia adres dostawy, klika „Zapisz” i widzi potwierdzenie. Kilka minut później w zamówieniu wraca stary adres. Serwer działa, baza nie zgłasza awarii, a kopia zapasowa wykonuje się poprawnie. Problem może leżeć w sposobie obsługi równoczesnej edycji: druga osoba zapisała formularz otwarty przed pierwszą zmianą.
To tak zwana utracona aktualizacja — poprawny zapis jednej osoby zostaje nadpisany przez dane pochodzące ze starszego odczytu. Poniżej pokazujemy, jak rozpoznać ten przypadek, zaprojektować zachowanie aplikacji i zabezpieczyć zapis w SQL Server. Przykład jest hipotetyczny; nie opisuje wdrożenia ani wyników klienta Inkluzji.
Jak zmiana może zniknąć mimo poprawnego zapisu?
Wyobraźmy sobie zamówienie zawierające adres „Leśna 4” i notatkę „Dostawa rano”. Anna oraz Piotr otwierają ten sam formularz.
- Anna zmienia adres na „Polna 8” i zapisuje zamówienie.
- Piotr nadal widzi „Leśna 4”. Poprawia tylko notatkę na „Dostawa po 14:00”.
- Jego aplikacja wysyła wszystkie pola formularza, łącznie ze starym adresem.
- Serwer bez sprawdzenia zapisuje całą paczkę. Notatka jest nowa, ale adres ponownie błędny.
Każdy zapis osobno może być poprawny technicznie. Błąd wynika z kolejności i braku informacji, na jakiej wersji zamówienia pracował użytkownik. Samo komunikowanie „zapisano pomyślnie” nie dowodzi, że program chroni wcześniejsze zmiany innych osób.
W firmie skutkiem może być wysyłka pod niewłaściwy adres, powrót starej ceny albo ponowne przypisanie zadania do innego pracownika. Zanim powstanie kosztowna pomyłka, zwykle pojawiają się zgłoszenia typu „system nie zapamiętuje danych”. Warto potraktować je jako wskazówkę do odtworzenia kolejności operacji, a nie od razu jako błąd obsługi.
Najprostsza zasada: zapisz, jeżeli wersja nadal się zgadza
Aplikacja powinna pobrać razem z danymi ich znacznik wersji. Przy zapisie odsyła ten sam znacznik. Serwer dopuszcza zmianę tylko wtedy, gdy zamówienie nadal ma wersję odczytaną przez użytkownika. Taki mechanizm nazywa się optymistyczną kontrolą współbieżności: zakładamy, że konflikty będą sporadyczne, i wykrywamy je podczas zapisu zamiast blokować rekord na cały czas edycji.
W naszym przykładzie obie osoby odczytały umownie wersję A. Zapis Anny zmienia ją na B. Zapis Piotra nadal wskazuje A, więc serwer go odrzuca. Adres pozostaje poprawny, a Piotr dostaje możliwość porównania danych. Litery A i B służą wyłącznie objaśnieniu mechanizmu.
Sprawdzenie wersji oraz zapis muszą stanowić jedną niepodzielną operację. Schemat „najpierw sprawdź wersję, chwilę później wykonaj bezwarunkowy zapis” pozostawia lukę: inny użytkownik może zapisać dane właśnie pomiędzy tymi czynnościami. Dokumentacja Microsoft pokazuje kontrolę w warunku samego polecenia aktualizacji [1][2].
Komunikat o konflikcie to część rozwiązania
Zatrzymanie nadpisania chroni bazę, ale nie wystarczy użytkownikowi. Sam komunikat „Błąd zapisu” skłania do ponownego klikania. Lepsza informacja brzmi: „To zamówienie zmieniło się od momentu otwarcia. Twoje poprawki nie zostały zapisane. Porównaj je z aktualną wersją”.
Formularz powinien zachować wpisane wartości i pokazać różnice: co użytkownik otworzył, co sam zmienił oraz co jest obecnie na serwerze. Piotr może wtedy ponownie zastosować wyłącznie swoją notatkę do aktualnego zamówienia. Ten kolejny zapis również wymaga sprawdzenia wersji — w międzyczasie mogła pojawić się trzecia zmiana.

Automatyczne połączenie zmian bywa możliwe, gdy dotyczą niezależnych pól. Nie należy jednak zakładać, że różne pola zawsze są niezależne biznesowo. Zmiana kraju dostawy może wpływać na dopuszczalnego przewoźnika, cenę lub wymagane dane. Po połączeniu trzeba ponownie sprawdzić reguły całego zamówienia.
Co wybrać: kontrolę wersji, blokadę czy zmianę sposobu pracy?
- Formularz sporadycznie edytowany przez kilka osób: kontrola wersji jest dobrym punktem wyjścia. Nie wymaga trzymania blokady bazy przez kilka minut, gdy ktoś rozmawia przez telefon.
- Dokument z jednym odpowiedzialnym operatorem: można rozważyć rezerwację edycji w aplikacji. Musi mieć czas wygaśnięcia i sposób odzyskania po zamknięciu przeglądarki. Nadal trzeba zabezpieczyć zapis przed wygasłą rezerwacją.
- Często zmieniany stan magazynowy: zamiast zapisywać stan odczytany wcześniej, lepiej projektować operacje typu „zarezerwuj 3 sztuki”, z kontrolą dostępności w bazie. Ochrona formularza nie zastępuje reguł rezerwacji.
- Wiele konfliktów w jednym dużym formularzu: warto sprawdzić podział danych i odpowiedzialności. Rozdzielenie niezależnych operacji może ograniczyć kolizje, ale wymaga zachowania spójności procesu.
Optymistyczna kontrola nie oznacza braku wszystkich blokad w silniku bazy. Chodzi o to, aby nie utrzymywać długiej blokady podczas pracy człowieka nad formularzem. Krótkie operacje zapisu nadal korzystają z mechanizmów bazy danych.
Dla dociekliwych: warunkowy UPDATE w SQL Server
SQL Server udostępnia typ rowversion: automatycznie zmieniany, 8-bajtowy znacznik wiersza. Nie jest to data ani godzina i nie należy używać go jako daty ostatniej edycji. Zmienia się również przy aktualizacji ustawiającej dotychczasową wartość [1].
Poniższy uproszczony przykład pokazuje istotę zabezpieczenia. Tabela demonstracyjna nie ma wyzwalaczy; to materiał do analizy na bazie testowej, a nie gotowa migracja produkcyjna.
CREATE TABLE dbo.OrderEditDemo (
OrderId int NOT NULL PRIMARY KEY,
DeliveryAddress nvarchar(200) NOT NULL,
Note nvarchar(500) NOT NULL,
VersionToken rowversion NOT NULL
);
-- Parametry przekazuje aplikacja:
-- @OrderId int, @Address nvarchar(200),
-- @Note nvarchar(500), @ExpectedVersion binary(8)
UPDATE dbo.OrderEditDemo
SET DeliveryAddress = @Address,
Note = @Note
OUTPUT inserted.VersionToken AS NewVersion
WHERE OrderId = @OrderId
AND VersionToken = @ExpectedVersion;
Jeżeli zapis się uda, aplikacja otrzymuje nowy znacznik. Brak zaktualizowanego wiersza oznacza, że zapis nie nastąpił: wersja przestała pasować albo rekord już nie istnieje. Program powinien obsłużyć oba przypadki. Parametry trzeba wiązać przez sterownik bazy, bez sklejania SQL z tekstem formularza.
W produkcji dochodzą uprawnienia do konkretnego zamówienia, walidacja danych, obsługa błędów i ewentualna transakcja obejmująca kilka tabel. Token wersji nie jest uprawnieniem do edycji. Jeśli wyzwalacz dodatkowo modyfikuje wiersz, sposób pobrania ostatecznej wersji po zapisie również wymaga uwzględnienia.
Trzeba zabezpieczyć wszystkie ścieżki aktualizacji: formularz, import, panel administracyjny oraz integracje. Jeden starszy moduł wykonujący bezwarunkowy zapis może nadal nadpisywać dane. Znacznik z jednej tabeli nie chroni automatycznie powiązanych pozycji zamówienia; granicę spójnej operacji trzeba zaprojektować świadomie.
Jak przekazać wersję przez API?
W aplikacji korzystającej z HTTP można wykorzystać ETag, czyli identyfikator wersji reprezentacji zasobu, oraz nagłówek If-Match. Klient odsyła znacznik otrzymany podczas odczytu. Niespełniony warunek skutkuje odpowiedzią 412 Precondition Failed [3].
PATCH /api/orders/42
If-Match: "order-42-v7"
Content-Type: application/json
{"note":"Dostawa po 14:00"}
To ilustracyjny kontrakt API, nie gotowy endpoint. Backend musi rzeczywiście egzekwować warunek podczas zapisu; samo dodanie nagłówka przez przeglądarkę niczego nie zabezpiecza. If-Match wymaga silnego porównania znaczników. Jeśli odpowiedź zawiera dane z kilku tabel, znacznik musi uwzględniać wszystkie zmiany istotne dla tej reprezentacji, a nie tylko wersję jej głównego wiersza.
Nie należy automatycznie pobierać nowego tokenu po konflikcie i ponawiać starego zapisu bez porównania danych. Takie „naprawianie” błędu usuwa ochronę przed nadpisaniem. To inny problem niż powtórzenie tego samego żądania, omawiane w artykule o duplikatach i idempotencji integracji.
Test odbiorowy: dwie sesje i jeden rekord
Właściciel firmy nie musi czytać SQL, aby sprawdzić podstawowe zachowanie aplikacji. Na danych testowych można przeprowadzić następujący scenariusz:
- Otwórz ten sam rekord w dwóch niezależnych sesjach.
- W pierwszej zmień adres i zapisz.
- W drugiej, bez odświeżania, zmień notatkę i spróbuj zapisać.
- Sprawdź, czy aplikacja zatrzymała konflikt, zachowała notatkę do porównania i nie przywróciła starego adresu.
- Wczytaj aktualne dane, świadomie zastosuj notatkę i potwierdź, że obie zamierzone zmiany są zachowane.
Programista powinien uzupełnić to testem równoczesnych żądań na poziomie API. Warto sprawdzić też usunięcie rekordu podczas edycji, dwa zapisy tego samego pola, aktualizację przez import oraz ponowny konflikt po wyświetleniu porównania. Test samego interfejsu nie wykryje obejścia kontroli przez inną ścieżkę zapisu.
Do monitorowania przydają się liczba konfliktów względem liczby zapisów, moduł aplikacji oraz identyfikator żądania. Rosnąca liczba konfliktów może wskazywać na źle podzielony proces. Nie trzeba kopiować pełnych formularzy z danymi klientów do logów, aby znaleźć miejsce kolizji.
Co ustalić przed zleceniem poprawki?
Najważniejsze pytania dotyczą zachowania programu: które dane można scalać, kto podejmuje decyzję przy konflikcie i czy istnieją importy zapisujące te same rekordy. Dopiero później dobiera się mechanizm techniczny. Dobrze przygotowane zlecenie obejmuje zabezpieczenie zapisu, czytelny ekran porównania i testy wszystkich wejść do systemu.
Sprawdzona kopia zapasowa pozostaje potrzebna, ale przywracanie całej bazy nie jest wygodnym sposobem odzyskania jednej nadpisanej notatki. Historia zmian może pomóc w ustaleniu, co się stało; kontrola współbieżności ma zapobiegać cichej utracie danych już w chwili zapisu.
Jeśli w Twojej aplikacji „czasem znikają poprawki”, przygotuj opis kroków i przykładowy rekord bez danych klientów. Skontaktuj się z Inkluzją, aby omówić diagnozę oraz zakres zabezpieczenia.
Źródła i dokumentacja
- Microsoft Learn: rowversion (Transact-SQL) — działanie znacznika i warunkowa aktualizacja.
- Microsoft Learn: Optimistic concurrency — wykrywanie konfliktów i interpretacja braku zaktualizowanych wierszy.
- MDN: If-Match — warunkowe żądania HTTP i silne porównanie ETag.
Dokumentację sprawdzono 10 listopada 2026 r. Zdjęcie główne: Priscilla Du Preez / Unsplash, licencja Unsplash. Obie fotografie mają charakter ilustracyjny.
