[django] 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()

위에 나열된 코드는 Custom Manager 에서 메서드로 구현할 수 있습니다 .

다음과 같은 가정을하고 있습니다.

  • filter (). update ()는 필터가 게으 르기 때문에 단일 데이터베이스 쿼리를 생성합니다.
  • 데이터베이스 쿼리는 원자 적입니다.

이러한 가정은 다른 사람이 이전에 항목을 업데이트하지 않았 음을 확인하기에 충분합니다. 이 방법으로 여러 행이 업데이트되면 트랜잭션을 사용해야합니다.

경고 Django Doc :

update () 메서드는 SQL 문으로 직접 변환됩니다. 직접 업데이트를위한 대량 작업입니다. 모델에서 save () 메서드를 실행하지 않거나 pre_save 또는 post_save 신호를 방출하지 않습니다.


답변

이 질문은 약간 오래되었고 내 대답은 조금 늦었지만 내가 이해 한 후에 는 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”필드를 추가하고이를 숨겨진 필드로 사용자에게 전달하는 것입니다. 일반적인 업데이트주기는 다음과 같습니다.

  1. 데이터를 읽고 사용자에게 보여줍니다.
  2. 사용자 수정 데이터
  3. 사용자가 데이터를 게시
  4. 앱은 데이터베이스에 다시 저장합니다.

낙관적 잠금을 구현하려면 데이터를 저장할 때 사용자로부터받은 버전이 데이터베이스의 버전과 동일한 지 확인한 다음 데이터베이스를 업데이트하고 버전을 증가시킵니다. 그렇지 않은 경우 데이터가로드 된 이후 변경 사항이 있음을 의미합니다.

다음과 같은 단일 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의 원자 연산에 대해 묻는 이 질문을 보여줍니다 .