두 명 이상의 사용자가 동일한 데이터베이스 항목을 동시에 수정하지 못하도록 보호하는 방법이 있습니까?
두 번째 커밋 / 저장 작업을 수행하는 사용자에게 오류 메시지를 표시하는 것은 허용되지만 데이터를 자동으로 덮어 쓰면 안됩니다.
사용자가 "뒤로"버튼을 사용하거나 단순히 브라우저를 닫고 잠금을 영원히 남겨 둘 수 있으므로 항목을 잠그는 것은 옵션이 아니라고 생각합니다.
두 명 이상의 사용자가 동일한 데이터베이스 항목을 동시에 수정하지 못하도록 보호하는 방법이 있습니까?
두 번째 커밋 / 저장 작업을 수행하는 사용자에게 오류 메시지를 표시하는 것은 허용되지만 데이터를 자동으로 덮어 쓰면 안됩니다.
사용자가 "뒤로"버튼을 사용하거나 단순히 브라우저를 닫고 잠금을 영원히 남겨 둘 수 있으므로 항목을 잠그는 것은 옵션이 아니라고 생각합니다.
답변:
이것은 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()
위에 나열된 코드는 Custom Manager 에서 메서드로 구현할 수 있습니다 .
다음과 같은 가정을하고 있습니다.
이러한 가정은 다른 사람이 이전에 항목을 업데이트하지 않았 음을 확인하기에 충분합니다. 이 방법으로 여러 행이 업데이트되면 트랜잭션을 사용해야합니다.
경고 Django Doc :
update () 메서드는 SQL 문으로 직접 변환됩니다. 직접 업데이트를위한 대량 작업입니다. 모델에서 save () 메서드를 실행하지 않거나 pre_save 또는 post_save 신호를 방출하지 않습니다.
filter
하고 둘 다 수정되지 않은 동일한 목록을받은 e
다음 둘 다 동시에 호출하면 update
어떻게됩니까? 필터와 업데이트를 동시에 차단하는 세마포어가 없습니다. 편집 : 오 지금 게으른 필터를 이해합니다. 그러나 update ()가 원자 적이라고 가정하는 타당성은 무엇입니까? 확실히 DB는 동시 액세스를 처리합니다
이 질문은 약간 오래되었고 내 대답은 조금 늦었지만 내가 이해 한 후에 는 Django 1.4에서 다음을 사용하여 수정되었습니다 .
select_for_update(nowait=True)
참조 문서를
트랜잭션이 끝날 때까지 행을 잠그고 지원되는 데이터베이스에서 SELECT ... FOR UPDATE SQL 문을 생성하는 쿼리 세트를 반환합니다.
일반적으로 다른 트랜잭션이 선택한 행 중 하나에 대해 이미 잠금을 획득 한 경우 잠금이 해제 될 때까지 쿼리가 차단됩니다. 이것이 원하는 동작이 아닌 경우 select_for_update (nowait = True)를 호출합니다. 이렇게하면 통화가 차단되지 않습니다. 충돌하는 잠금이 이미 다른 트랜잭션에 의해 획득 된 경우 쿼리 세트가 평가 될 때 DatabaseError가 발생합니다.
물론 이것은 백엔드가 "업데이트를 위해 선택"기능을 지원하는 경우에만 작동합니다. 예를 들어 sqlite는 지원하지 않습니다. 불행히도 : nowait=True
는 MySql에서 지원되지 않습니다. nowait=False
여기서는 잠금이 해제 될 때까지만 차단됩니다 :를 사용해야 합니다.
실제로 트랜잭션은 여러 HTTP 요청을 통해 실행되는 트랜잭션을 원하지 않는 한 여기에서별로 도움이되지 않습니다.
이러한 경우에 일반적으로 사용하는 것은 "낙관적 잠금"입니다. Django ORM은 내가 아는 한 지원하지 않습니다. 그러나이 기능을 추가하는 것에 대해 몇 가지 논의가있었습니다.
그래서 당신은 당신 자신입니다. 기본적으로해야 할 일은 모델에 "version"필드를 추가하고이를 숨겨진 필드로 사용자에게 전달하는 것입니다. 일반적인 업데이트주기는 다음과 같습니다.
낙관적 잠금을 구현하려면 데이터를 저장할 때 사용자로부터받은 버전이 데이터베이스의 버전과 동일한 지 확인한 다음 데이터베이스를 업데이트하고 버전을 증가시킵니다. 그렇지 않은 경우 데이터가로드 된 이후 변경 사항이 있음을 의미합니다.
다음과 같은 단일 SQL 호출로이를 수행 할 수 있습니다.
UPDATE ... WHERE version = 'version_from_user';
이 호출은 버전이 여전히 동일한 경우에만 데이터베이스를 업데이트합니다.
Django 1.11에는 비즈니스 로직 요구 사항에 따라이 상황을 처리 할 수있는 세 가지 편리한 옵션 이 있습니다.
Something.objects.select_for_update()
모델이 해제 될 때까지 차단됩니다.Something.objects.select_for_update(nowait=True)
및 캐치 DatabaseError
모델은 현재 업데이트에 잠겨있는 경우Something.objects.select_for_update(skip_locked=True)
현재 잠겨있는 개체를 반환하지 않습니다.다양한 모델에 대한 대화 형 워크 플로와 배치 워크 플로가 모두있는 애플리케이션에서 동시 처리 시나리오 대부분을 해결하는 데이 세 가지 옵션을 찾았습니다.
"대기" select_for_update
는 순차 배치 프로세스에서 매우 편리합니다. 모두 실행하기를 원하지만 시간을 들여야합니다. 는 nowait
사용자가 현재 업데이트 잠겨 객체를 수정하고자 할 때 사용됩니다 - 난 그냥이 순간에 수정되는 것을 말할 것이다.
이것은 skip_locked
사용자가 개체의 재검색을 트리거 할 수있는 다른 유형의 업데이트에 유용하며, 트리거되는 한 누가 트리거하는지 상관하지 않으므로 skip_locked
중복 된 트리거를 자동으로 건너 뛸 수 있습니다.
향후 참조를 위해 https://github.com/RobCombs/django-locking을 확인 하십시오 . 사용자가 페이지를 떠날 때 자바 스크립트 잠금 해제와 잠금 시간 초과 (예 : 사용자의 브라우저가 충돌하는 경우)를 혼합하여 영구적 인 잠금을 남기지 않는 방식으로 잠금을 수행합니다. 문서는 꽤 완전합니다.
이 문제에 관계없이 적어도 django 트랜잭션 미들웨어를 사용해야합니다.
여러 사용자가 동일한 데이터를 편집하는 실제 문제에 관해서는 ... 예, 잠금을 사용하십시오. 또는:
사용자가 업데이트하는 버전을 확인하고 (안전하게 수행하여 사용자가 최신 사본을 업데이트한다고 말하기 위해 시스템을 해킹 할 수 없도록하십시오!) 해당 버전이 최신 버전 인 경우에만 업데이트하십시오. 그렇지 않으면 사용자에게 편집 중이던 원본 버전, 제출 된 버전 및 다른 사람이 작성한 새 버전이 포함 된 새 페이지를 다시 보냅니다. 변경 사항을 완전히 최신 버전으로 병합하도록 요청하십시오. diff + patch와 같은 도구 세트를 사용하여 자동 병합을 시도 할 수 있지만 어쨌든 실패 사례에 대해 작동하는 수동 병합 방법이 필요하므로 먼저 시작하십시오. 또한 누군가가 의도하지 않거나 의도적으로 병합을 엉망으로 만들 경우 버전 기록을 보존하고 관리자가 변경 사항을 되돌릴 수 있도록 허용해야합니다. 그러나 어쨌든 당신은 그것을 가져야 할 것입니다.
대부분의 작업을 수행하는 django 앱 / 라이브러리가있을 가능성이 높습니다.
찾아야 할 또 다른 것은 "원자"라는 단어입니다. 원자 적 작업은 데이터베이스 변경이 성공적으로 발생하거나 분명히 실패 함을 의미합니다. 빠른 검색은 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()
멋지게 보이고 직렬화 가능한 트랜잭션 없이도 잘 작동합니다.
문제는 .update () 메서드를 호출하기 위해 수동 배관을 수행 할 필요가 없도록 귀머거리 .save () 동작을 확장하는 방법입니다.
Custom Manager 아이디어를 살펴 보았습니다.
내 계획은 업데이트를 수행하기 위해 Model.save_base ()에서 호출하는 Manager _update 메서드를 재정의하는 것입니다.
이것은 Django 1.3의 현재 코드입니다.
def _update(self, values, **kwargs):
return self.get_query_set()._update(values, **kwargs)
수행해야 할 작업 IMHO는 다음과 같습니다.
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)
삭제시에도 비슷한 일이 발생해야합니다. 그러나 Django가 django.db.models.deletion.Collector를 통해이 영역에서 꽤 많은 부두를 구현하고 있기 때문에 삭제는 조금 더 어렵습니다.
Django와 같은 modren 도구에 Optimictic Concurency Control에 대한 지침이 없다는 것은 이상합니다.
수수께끼를 풀 때이 게시물을 업데이트하겠습니다. 바라건대 해결책은 수많은 코딩, 이상한 뷰, Django의 필수 부분을 건너 뛰는 등의 작업을 포함하지 않는 멋진 파이썬 방식이 될 것입니다.
안전을 위해 데이터베이스는 트랜잭션 을 지원해야 합니다 .
필드가 "자유 형식"(예 : 텍스트 등)이고 여러 사용자가 동일한 필드를 편집 할 수 있도록 허용해야하는 경우 (데이터에 대한 단일 사용자 소유권을 가질 수 없음) 원본 데이터를 변하기 쉬운. 사용자가 커밋 할 때 입력 데이터가 원본 데이터에서 변경되었는지 확인 (그렇지 않은 경우 기존 데이터를 다시 작성하여 DB를 귀찮게 할 필요가 없음), DB의 현재 데이터와 비교 한 원본 데이터가 동일한 경우 저장할 수 있으며 변경된 경우 사용자에게 차이점을 표시하고 사용자에게 무엇을해야하는지 물어볼 수 있습니다.
계좌 잔고, 매장 아이템 개수 등과 같이 필드가 숫자 인 경우 원래 값 (사용자가 양식 작성을 시작할 때 저장 됨)과 가능한 새 값의 차이를 계산하면 더 자동으로 처리 할 수 있습니다. 트랜잭션을 시작하고 현재 값을 읽고 차이를 추가 한 다음 트랜잭션을 종료합니다. 음수 값을 가질 수없는 경우 결과가 음수이면 트랜잭션을 중단하고 사용자에게 알려야합니다.
장고를 몰라서 cod3s를 줄 수 없습니다 ..;)
여기에서 :
다른 사람이 수정 한 개체를 덮어 쓰는 것을 방지하는 방법
타임 스탬프가 세부 정보를 저장하려는 형식의 숨겨진 필드로 유지 될 것이라고 가정합니다.
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()