Od CRUD-a do edycji na żywo, czyli jak ogarnąć równoczesne zmiany
Utworzono: czerwiec 2023 Zaktualizowano: 1 października 2026
z pracy
Python, Django, PostgreSQL
Co zrobić, gdy ten sam rekord zmienia kilka osób naraz.
Dwie osoby otwierają ten sam rekord, każda coś zmienia i zapisuje. W zwykłym CRUD-zie drugi zapis nadpisuje pierwszy i nikt się o tym nie dowiaduje. Przy jednym użytkowniku tego nie widać, testy zwykle robi jedna osoba, więc wychodzi to dopiero na produkcji, jako dane, które „same się zmieniły”.
Co można z tym zrobić
Najprościej nic, ostatni zapis wygrywa. Czasem to naprawdę wystarcza, np. kiedy rekord edytuje zawsze jedna osoba.
Pierwsze, co przychodzi do głowy, to blokada przy otwarciu, tak jak w systemie, przy którym pracowałem lata temu: pierwsza osoba blokuje, druga widzi, kto trzyma, i może poprosić o zwolnienie. Nic nie ginie, za to blokady zostają po ludziach, którzy wyszli z otwartym formularzem, więc trzeba je zwalniać ręcznie albo po czasie.
Zamiast blokować, można trzymać w rekordzie wersję i przy zapisie sprawdzać, czy nikt go w międzyczasie nie zmienił. Blokad nie ma, a konflikt wyskakuje tylko przy prawdziwej kolizji.
Albo w ogóle nie edytować danych wprost, tylko składać propozycje zmian, które przechodzą zatwierdzanie, jak ankiety w systemie, przy którym teraz pracuję. Każda zmiana ma autora i zatwierdzającego, zmiany różnych fragmentów nie wchodzą sobie w drogę, tylko że zmiana nie jest widoczna od razu.
No i jest edycja na żywo, jak w Google Docs, gdzie kilka osób pisze naraz i widzi się nawzajem. Pod spodem są algorytmy scalania zmian, OT albo CRDT, i gotowe biblioteki, np. Yjs, ale to już zupełnie inna bajka niż formularz z przyciskiem „Zapisz”.
Co bym wybrał
Najczęściej wybrałbym wersję w rekordzie, bo jest tania i nikogo nie blokuje, a propozycje zmian tam, gdzie zmiany i tak trzeba zatwierdzać, tak jak u nas przy ankietach.
Wersja w rekordzie w Django to jedno pole i warunek przy zapisie:
from django.db.models import F from documents.models import Document class Conflict(Exception): pass def save_document(doc_id: int, version: int, **changes) -> None: updated = Document.objects.filter(pk=doc_id, version=version).update(**changes, version=F("version") + 1) if not updated: raise Conflict("Ktoś zmienił ten dokument w międzyczasie")
Jak update nie zmieni żadnego wiersza, to ktoś był szybszy, i zamiast nadpisać jego zmiany pokazujemy konflikt.