Django : 데이터베이스 항목의 동시 수정으로부터 어떻게 보호 할 수 있습니까?


81

두 명 이상의 사용자가 동일한 데이터베이스 항목을 동시에 수정하지 못하도록 보호하는 방법이 있습니까?

두 번째 커밋 / 저장 작업을 수행하는 사용자에게 오류 메시지를 표시하는 것은 허용되지만 데이터를 자동으로 덮어 쓰면 안됩니다.

사용자가 "뒤로"버튼을 사용하거나 단순히 브라우저를 닫고 잠금을 영원히 남겨 둘 수 있으므로 항목을 잠그는 것은 옵션이 아니라고 생각합니다.


4
여러 동시 사용자가 하나의 개체를 업데이트 할 수있는 경우 더 큰 디자인 문제가있을 수 있습니다. 사용자 별 리소스를 고려하거나 처리 단계를 별도의 테이블로 분리하여 이것이 문제가되지 않도록하는 것이 좋습니다.
S.Lott

답변:


48

이것은 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 신호를 방출하지 않습니다.


12
좋은! 그래도 '&&'대신 '&'가되어야하지 않나요?
Giles Thomas

1
재정의 된 save () 메서드 내부에 'update'호출을 넣어 save () 메서드를 실행하지 않는 'update'문제를 피할 수 있습니까?
Jonathan Hartley 2011 년

1
두 스레드가 동시에 호출 filter하고 둘 다 수정되지 않은 동일한 목록을받은 e다음 둘 다 동시에 호출하면 update어떻게됩니까? 필터와 업데이트를 동시에 차단하는 세마포어가 없습니다. 편집 : 오 지금 게으른 필터를 이해합니다. 그러나 update ()가 원자 적이라고 가정하는 타당성은 무엇입니까? 확실히 DB는 동시 액세스를 처리합니다
totowtwo 2011-06-24

1
@totowtwo ACID의 I는 주문을 보장합니다 ( en.wikipedia.org/wiki/ACID ). UPDATE가 동시 (그러나 나중에 시작된) SELECT와 관련된 데이터에서 실행되는 경우 UPDATE가 완료 될 때까지 차단됩니다. 그러나 여러 개의 SELECT를 동시에 실행할 수 있습니다.
Kit Sunde 2013

1
이것이 자동 커밋 모드 (기본값)에서만 제대로 작동하는 것 같습니다. 그렇지 않으면 최종 COMMIT가이 업데이트 SQL 문과 분리되어 동시 코드가 둘 사이에서 실행될 수 있습니다. 그리고 Django에 ReadCommited 격리 수준이 있으므로 이전 버전을 읽습니다. (이 업데이트와 함께 다른 테이블에 행을 만들고 싶기 때문에 여기서 수동 트랜잭션을 원하는 이유는 무엇입니까?) 그래도 좋은 생각입니다.
Alex Lokk 2013

39

이 질문은 약간 오래되었고 내 대답은 조금 늦었지만 내가 이해 한 후에 는 Django 1.4에서 다음을 사용하여 수정되었습니다 .

select_for_update(nowait=True)

참조 문서를

트랜잭션이 끝날 때까지 행을 잠그고 지원되는 데이터베이스에서 SELECT ... FOR UPDATE SQL 문을 생성하는 쿼리 세트를 반환합니다.

일반적으로 다른 트랜잭션이 선택한 행 중 하나에 대해 이미 잠금을 획득 한 경우 잠금이 해제 될 때까지 쿼리가 차단됩니다. 이것이 원하는 동작이 아닌 경우 select_for_update (nowait = True)를 호출합니다. 이렇게하면 통화가 차단되지 않습니다. 충돌하는 잠금이 이미 다른 트랜잭션에 의해 획득 된 경우 쿼리 세트가 평가 될 때 DatabaseError가 발생합니다.

물론 이것은 백엔드가 "업데이트를 위해 선택"기능을 지원하는 경우에만 작동합니다. 예를 들어 sqlite는 지원하지 않습니다. 불행히도 : nowait=True는 MySql에서 지원되지 않습니다. nowait=False여기서는 잠금이 해제 될 때까지만 차단됩니다 :를 사용해야 합니다.


2
이것은 좋은 대답이 아닙니다.이 질문은 명시 적으로 (비관적) 잠금을 원하지 않았고, 두 개의 더 높은 투표 응답은 현재 이러한 이유로 낙관적 동시성 제어 ( "낙관적 잠금")에 초점을 맞추고 있습니다. 그러나 다른 상황에서는 업데이트 선택이 좋습니다.
RichVel 2014 년

@ giZm0 그것은 여전히 ​​비관적 잠금을 만듭니다. 잠금을 획득 한 첫 번째 스레드는이를 무기한 보유 할 수 있습니다.
knaperek 2014-06-24

6
나는 Django의 문서이며 타사의 아름다운 발명품이 아니기 때문에이 답변을 좋아합니다.
anizzomc

29

실제로 트랜잭션은 여러 HTTP 요청을 통해 실행되는 트랜잭션을 원하지 않는 한 여기에서별로 도움이되지 않습니다.

이러한 경우에 일반적으로 사용하는 것은 "낙관적 잠금"입니다. Django ORM은 내가 아는 한 지원하지 않습니다. 그러나이 기능을 추가하는 것에 대해 몇 가지 논의가있었습니다.

그래서 당신은 당신 자신입니다. 기본적으로해야 할 일은 모델에 "version"필드를 추가하고이를 숨겨진 필드로 사용자에게 전달하는 것입니다. 일반적인 업데이트주기는 다음과 같습니다.

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

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

다음과 같은 단일 SQL 호출로이를 수행 할 수 있습니다.

UPDATE ... WHERE version = 'version_from_user';

이 호출은 버전이 여전히 동일한 경우에만 데이터베이스를 업데이트합니다.


1
이 같은 질문이 Slashdot에도 나타났습니다. 당신이 제안한 낙관적 잠금도 거기에서 제안되었지만 조금 더 잘 설명했습니다 : hardware.slashdot.org/comments.pl?sid=1381511&cid=29536367
hopla

5
또한이 상황을 피하기 위해 트랜잭션을 사용하고 싶습니다. hardware.slashdot.org/comments.pl?sid=1381511&cid=29536613 Django는 트랜잭션에서 데이터베이스의 모든 작업을 자동으로 래핑하는 미들웨어를 제공합니다. 초기 요청에서 성공적으로 응답 한 후에 만 ​​커밋 : docs.djangoproject.com/en/dev/topics/db/transactions (주의 : 트랜잭션 미들웨어는 낙관적 잠금으로 위의 문제를 피하는 데만 도움이되며 잠금을 제공하지 않습니다.
단독으로

이 작업을 수행하는 방법에 대한 세부 정보도 찾고 있습니다. 지금까지 운이 없습니다.
seanyboy 2009

1
장고 대량 업데이트를 사용하면됩니다. 내 대답을 확인하십시오.
Andrei Savu

14

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중복 된 트리거를 자동으로 건너 뛸 수 있습니다.


1
transaction.atomic ()으로 업데이트 선택을 래핑해야합니까? 실제로 업데이트 결과를 사용한다면? select_for_update를 noop으로 만드는 전체 테이블을 잠그지 않습니까?
Paul Kenjora


1

이 문제에 관계없이 적어도 django 트랜잭션 미들웨어를 사용해야합니다.

여러 사용자가 동일한 데이터를 편집하는 실제 문제에 관해서는 ... 예, 잠금을 사용하십시오. 또는:

사용자가 업데이트하는 버전을 확인하고 (안전하게 수행하여 사용자가 최신 사본을 업데이트한다고 말하기 위해 시스템을 해킹 할 수 없도록하십시오!) 해당 버전이 최신 버전 인 경우에만 업데이트하십시오. 그렇지 않으면 사용자에게 편집 중이던 원본 버전, 제출 된 버전 및 다른 사람이 작성한 새 버전이 포함 된 새 페이지를 다시 보냅니다. 변경 사항을 완전히 최신 버전으로 병합하도록 요청하십시오. diff + patch와 같은 도구 세트를 사용하여 자동 병합을 시도 할 수 있지만 어쨌든 실패 사례에 대해 작동하는 수동 병합 방법이 필요하므로 먼저 시작하십시오. 또한 누군가가 의도하지 않거나 의도적으로 병합을 엉망으로 만들 경우 버전 기록을 보존하고 관리자가 변경 사항을 되돌릴 수 있도록 허용해야합니다. 그러나 어쨌든 당신은 그것을 가져야 할 것입니다.

대부분의 작업을 수행하는 django 앱 / 라이브러리가있을 가능성이 높습니다.


이것은 또한 Guillaume이 제안한 것처럼 낙관적 잠금입니다. 그러나 그는 모든 포인트를 얻는 것 같았습니다 :)
hopla

0

찾아야 할 또 다른 것은 "원자"라는 단어입니다. 원자 적 작업은 데이터베이스 변경이 성공적으로 발생하거나 분명히 실패 함을 의미합니다. 빠른 검색은 Django의 원자 연산에 대해 묻는 이 질문을 보여줍니다 .


시간이 오래 걸리고 전혀 완료되지 않을 수 있으므로 여러 요청에 대해 트랜잭션 또는 잠금을 수행하고 싶지 않습니다.
Ber

트랜잭션이 시작되면 완료되어야합니다. 사용자가보기 위해 레코드를 열 때가 아니라 사용자가 "제출"을 클릭 한 후에 만 ​​레코드를 잠 그거나 트랜잭션을 시작해야합니다.
Harley Holcombe

예,하지만 내 문제는 두 사용자가 동일한 양식을 열고 변경 내용을 커밋한다는 점에서 다릅니다. 잠금이 이것에 대한 해결책이라고 생각하지 않습니다.
Ber

당신 말이 맞지만 문제는 이것에 대한 해결책 없다는 것입니다. 한 사용자는 이기고 다른 사용자는 실패 메시지를받습니다. 나중에 레코드를 잠글수록 문제가 줄어 듭니다.
Harley Holcombe

나는 동의한다. 다른 사용자에 대한 실패 메시지를 완전히 수락합니다. 이 사례를 감지 할 수있는 좋은 방법을 찾고 있습니다 (매우 드물게 예상 됨).
Ber

0

위의 아이디어

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의 필수 부분을 건너 뛰는 등의 작업을 포함하지 않는 멋진 파이썬 방식이 될 것입니다.


-2

안전을 위해 데이터베이스는 트랜잭션 을 지원해야 합니다 .

필드가 "자유 형식"(예 : 텍스트 등)이고 여러 사용자가 동일한 필드를 편집 할 수 있도록 허용해야하는 경우 (데이터에 대한 단일 사용자 소유권을 가질 수 없음) 원본 데이터를 변하기 쉬운. 사용자가 커밋 할 때 입력 데이터가 원본 데이터에서 변경되었는지 확인 (그렇지 않은 경우 기존 데이터를 다시 작성하여 DB를 귀찮게 할 필요가 없음), DB의 현재 데이터와 비교 한 원본 데이터가 동일한 경우 저장할 수 있으며 변경된 경우 사용자에게 차이점을 표시하고 사용자에게 무엇을해야하는지 물어볼 수 있습니다.

계좌 잔고, 매장 아이템 개수 등과 같이 필드가 숫자 인 경우 원래 값 (사용자가 양식 작성을 시작할 때 저장 됨)과 가능한 새 값의 차이를 계산하면 더 자동으로 처리 할 수 ​​있습니다. 트랜잭션을 시작하고 현재 값을 읽고 차이를 추가 한 다음 트랜잭션을 종료합니다. 음수 값을 가질 수없는 경우 결과가 음수이면 트랜잭션을 중단하고 사용자에게 알려야합니다.

장고를 몰라서 cod3s를 줄 수 없습니다 ..;)


-6

여기에서 :
다른 사람이 수정 한 개체를 덮어 쓰는 것을 방지하는 방법

타임 스탬프가 세부 정보를 저장하려는 형식의 숨겨진 필드로 유지 될 것이라고 가정합니다.

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()

1
코드가 깨졌습니다. 경쟁 조건은 if 확인과 저장 쿼리 사이에 여전히 발생할 수 있습니다. objects.filter (id = .. & timestamp check) .update (...)를 사용하고 행이 업데이트되지 않은 경우 예외를 발생시켜야합니다.
Andrei Savu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.