Czy istnieje sposób ochrony przed równoczesnymi modyfikacjami tego samego wpisu w bazie danych przez dwóch lub więcej użytkowników?
Dopuszczalne byłoby wyświetlenie komunikatu o błędzie użytkownikowi wykonującego drugą operację zatwierdzenia / zapisania, ale dane nie powinny być po cichu nadpisywane.
Myślę, że blokowanie wpisu nie wchodzi w grę, ponieważ użytkownik może użyć przycisku „Wstecz” lub po prostu zamknąć przeglądarkę, pozostawiając blokadę na zawsze.
Odpowiedzi:
Oto jak optymistyczne blokowanie w Django:
updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\ .update(updated_field=new_value, version=e.version+1) if not updated: raise ConcurrentModificationException()
Kod wymieniony powyżej można zaimplementować jako metodę w Custom Manager .
Przyjmuję następujące założenia:
Te założenia są wystarczające, aby zapewnić, że nikt inny nie zaktualizował wcześniej wpisu. Jeśli w ten sposób zaktualizowanych jest wiele wierszy, należy użyć transakcji.
OSTRZEŻENIE Django Doc :
źródło
filter
, oba otrzymują identyczną listę z niezmodyfikowanąe
, a następnie oba jednocześnie wywołująupdate
? Nie widzę semafora, który blokowałby jednocześnie filtrowanie i aktualizację. EDYCJA: och, rozumiem teraz leniwy filtr. Ale jaka jest słuszność założenia, że update () jest atomowy? z pewnością DB obsługuje równoczesny dostępTo pytanie jest trochę stare, a moja odpowiedź trochę późna, ale po tym, co rozumiem, zostało to naprawione w Django 1.4 za pomocą:
select_for_update(nowait=True)
zobacz dokumentację
Oczywiście zadziała to tylko wtedy, gdy zaplecze obsługuje funkcję „wybierz do aktualizacji”, co na przykład sqlite nie. Niestety:
nowait=True
nie jest obsługiwany przez MySql, tam musisz użyć:,nowait=False
który będzie blokował się tylko do momentu zwolnienia blokady.źródło
W rzeczywistości transakcje nie pomagają Ci tutaj zbytnio ... chyba że chcesz, aby transakcje były uruchamiane przez wiele żądań HTTP (których najprawdopodobniej nie chcesz).
To, czego zwykle używamy w takich przypadkach, to „optymistyczne blokowanie”. O ile wiem, ORM Django tego nie obsługuje. Ale była dyskusja na temat dodania tej funkcji.
Więc jesteś sam. Zasadniczo powinieneś dodać pole „wersja” do swojego modelu i przekazać je użytkownikowi jako pole ukryte. Normalny cykl aktualizacji to:
Aby zaimplementować optymistyczne blokowanie, podczas zapisywania danych należy sprawdzić, czy wersja otrzymana od użytkownika jest taka sama, jak wersja w bazie danych, a następnie zaktualizować bazę danych i zwiększyć wersję. Jeśli tak nie jest, oznacza to, że nastąpiła zmiana od czasu wczytania danych.
Możesz to zrobić za pomocą pojedynczego wywołania SQL za pomocą czegoś takiego:
UPDATE ... WHERE version = 'version_from_user';
To wywołanie zaktualizuje bazę danych tylko wtedy, gdy wersja jest nadal taka sama.
źródło
Django 1.11 ma trzy wygodne opcje radzenia sobie z tą sytuacją w zależności od wymagań logiki biznesowej:
Something.objects.select_for_update()
zablokuje się, dopóki model nie zostanie zwolnionySomething.objects.select_for_update(nowait=True)
i złap,DatabaseError
jeśli model jest obecnie zablokowany do aktualizacjiSomething.objects.select_for_update(skip_locked=True)
nie zwróci obiektów, które są aktualnie zablokowaneW mojej aplikacji, która ma zarówno interaktywne, jak i wsadowe przepływy pracy w różnych modelach, znalazłem te trzy opcje, aby rozwiązać większość moich współbieżnych scenariuszy przetwarzania.
„Czekanie”
select_for_update
jest bardzo wygodne w sekwencyjnych procesach wsadowych - chcę, aby wykonały wszystkie, ale nie spiesz się. Pliknowait
Jest używany, gdy użytkownik chce zmodyfikować obiekt, który jest obecnie zamknięty dla aktualizacji - Ja po prostu powiedz im, że to jest modyfikowana w tej chwili.Jest
skip_locked
to przydatne w przypadku innego rodzaju aktualizacji, gdy użytkownicy mogą wywołać ponowne skanowanie obiektu - i nie obchodzi mnie, kto go wyzwala, o ile jest wyzwalany, więcskip_locked
pozwala mi po cichu pominąć zduplikowane wyzwalacze.źródło
W przyszłości zajrzyj na https://github.com/RobCombs/django-locking . Blokuje w sposób, który nie pozostawia wiecznych blokad, poprzez połączenie odblokowywania javascript, gdy użytkownik opuszcza stronę, i limitów czasu blokady (np. W przypadku awarii przeglądarki użytkownika). Dokumentacja jest całkiem kompletna.
źródło
Prawdopodobnie powinieneś użyć przynajmniej oprogramowania pośredniczącego do transakcji django, nawet niezależnie od tego problemu.
Jeśli chodzi o rzeczywisty problem z tym, że wielu użytkowników edytuje te same dane ... tak, użyj blokowania. LUB:
Sprawdź, jaką wersję aktualizuje użytkownik (zrób to bezpiecznie, aby użytkownicy nie mogli po prostu włamać się do systemu, aby powiedzieć, że aktualizują najnowszą kopię!) I aktualizuj tylko wtedy, gdy ta wersja jest aktualna. W przeciwnym razie wyślij użytkownikowi z powrotem nową stronę z oryginalną wersją, którą edytował, przesłaną wersją oraz nowymi wersjami napisanymi przez innych. Poproś ich o połączenie zmian w jedną, całkowicie aktualną wersję. Możesz spróbować połączyć je automatycznie za pomocą zestawu narzędzi, takiego jak diff + patch, ale i tak będziesz potrzebować ręcznej metody scalania działającej w przypadku awarii, więc zacznij od tego. Musisz także zachować historię wersji i zezwolić administratorom na cofnięcie zmian, na wypadek gdyby ktoś przypadkowo lub celowo zepsuł scalanie. Ale i tak prawdopodobnie powinieneś to mieć.
Jest bardzo prawdopodobne, że istnieje aplikacja / biblioteka django, która zrobi to za Ciebie.
źródło
Inną rzeczą, której należy szukać, jest słowo „atomowy”. Niepodzielna operacja oznacza, że zmiana bazy danych zakończy się pomyślnie lub oczywiście zakończy się niepowodzeniem. Szybkie wyszukiwanie pokazuje to pytanie z pytaniem o operacje atomowe w Django.
źródło
Pomysł powyżej
updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\ .update(updated_field=new_value, version=e.version+1) if not updated: raise ConcurrentModificationException()
wygląda świetnie i powinien działać dobrze nawet bez transakcji, które można serializować.
Problem polega na tym, jak zwiększyć głuche zachowanie .save (), aby nie musieć wykonywać ręcznej instalacji hydraulicznej w celu wywołania metody .update ().
Spojrzałem na pomysł Custom Managera.
Planuję zastąpić metodę Manager _update wywoływaną przez Model.save_base () w celu przeprowadzenia aktualizacji.
To jest obecny kod w Django 1.3
def _update(self, values, **kwargs): return self.get_query_set()._update(values, **kwargs)
Co należy zrobić IMHO to coś takiego:
def _update(self, values, **kwargs): #TODO Get version field value v = self.get_version_field_value(values[0]) return self.get_query_set().filter(Q(version=v))._update(values, **kwargs)
Podobnie musi się stać przy usuwaniu. Jednak usuwanie jest nieco trudniejsze, ponieważ Django implementuje sporo voodoo w tym obszarze poprzez django.db.models.deletion.Collector.
To dziwne, że modrenowe narzędzie, takie jak Django, nie ma wskazówek dotyczących Optimictic Concurency Control.
Zaktualizuję ten post, gdy rozwiążę zagadkę. Miejmy nadzieję, że rozwiązanie będzie w przyjemny, pythonowy sposób, który nie będzie wymagał mnóstwa kodowania, dziwnych widoków, pomijania podstawowych fragmentów Django itp.
źródło
Aby była bezpieczna, baza danych musi obsługiwać transakcje .
Jeśli pola są „dowolne”, np. Tekst itp. I musisz pozwolić kilku użytkownikom na edytowanie tych samych pól (nie możesz mieć jednego prawa własności do danych), możesz przechowywać oryginalne dane w zmienna. Kiedy użytkownik zatwierdzi, sprawdź, czy dane wejściowe zmieniły się w stosunku do oryginalnych danych (jeśli nie, nie musisz zawracać sobie głowy DB, przepisując stare dane), czy oryginalne dane w porównaniu z bieżącymi danymi w bazie danych są takie same możesz zapisać, jeśli się zmieniło, możesz pokazać użytkownikowi różnicę i zapytać go, co ma zrobić.
Jeśli pola są liczbami np. Stan konta, ilość towarów w sklepie itp., Możesz obsłużyć to bardziej automatycznie, jeśli obliczysz różnicę między pierwotną wartością (zapisaną, gdy użytkownik zaczął wypełniać formularz) a nową wartością, rozpocznij transakcję odczytaj aktualną wartość i dodaj różnicę, a następnie zakończ transakcję. Jeśli nie możesz mieć wartości ujemnych, powinieneś przerwać transakcję, jeśli wynik jest ujemny, i poinformować o tym użytkownika.
Nie znam django, więc nie mogę dać ci cod3s ..;)
źródło
Stąd:
Jak zapobiec nadpisaniu obiektu zmodyfikowanego przez kogoś innego
Zakładam, że znacznik czasu będzie przechowywany jako ukryte pole w formularzu, w którym próbujesz zapisać szczegóły.
def save(self): if(self.id): foo = Foo.objects.get(pk=self.id) if(foo.timestamp > self.timestamp): raise Exception, "trying to save outdated Foo" super(Foo, self).save()
źródło