Program Club

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

proclub 2020. 10. 14. 21:27
반응형

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 트랜잭션 미들웨어를 사용해야합니다.

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

Check what version a user is updating against (do this securely, so users can't simply hack the system to say they were updating the latest copy!), and only update if that version is current. Otherwise, send the user back a new page with the original version they were editing, their submitted version, and the new version(s) written by others. Ask them to merge the changes into one, completely up-to-date version. You might try to auto-merge these using a toolset like diff+patch, but you'll need to have the manual merge method working for failure cases anyway, so start with that. Also, you'll need to preserve version history, and allow admins to revert changes, in case someone unintentionally or intentionally messes up the merge. But you should probably have that anyway.

There's very likely a django app/library that does most of this for you.


Another thing to look for is the word "atomic". An atomic operation means that your database change will either happen successfully, or fail obviously. A quick search shows this question asking about atomic operations in Django.


The idea above

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

looks great and should work fine even without serializable transactions.

The problem is how to augment the deafult .save() behavior as to not have to do manual plumbing to call the .update() method.

I looked at the Custom Manager idea.

My plan is to override the Manager _update method that is called by Model.save_base() to perform the update.

This is the current code in Django 1.3

def _update(self, values, **kwargs):
   return self.get_query_set()._update(values, **kwargs)

What needs to be done IMHO is something like:

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)

Similar thing needs to happen on delete. However delete is a bit more difficult as Django is implementing quite some voodoo in this area through django.db.models.deletion.Collector.

It is weird that modren tool like Django lacks guidance for Optimictic Concurency Control.

I will update this post when I solve the riddle. Hopefully solution will be in a nice pythonic way that does not involve tons of coding, weird views, skipping essential pieces of Django etc.


To be safe the database needs to support transactions.

If the fields is "free-form" e.g. text etc. and you need to allow several users to be able to edit the same fields (you can't have single user ownership to the data), you could store the original data in a variable. When the user committs, check if the input data has changed from the original data (if not, you don't need to bother the DB by rewriting old data), if the original data compared to the current data in the db is the same you can save, if it has changed you can show the user the difference and ask the user what to do.

If the fields is numbers e.g. account balance, number of items in a store etc., you can handle it more automatically if you calculate the difference between the original value (stored when the user started filling out the form) and the new value you can start a transaction read the current value and add the difference, then end transaction. If you can't have negative values, you should abort the transaction if the result is negative, and tell the user.

I don't know django, so I can't give you teh cod3s.. ;)


From here:
How to prevent overwriting an object someone else has modified

I'm assuming that the timestamp will be held as a hidden field in the form you're trying to save the details of.

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

참고URL : https://stackoverflow.com/questions/320096/django-how-can-i-protect-against-concurrent-modification-of-database-entries

반응형