Подтвердить что ты не робот

Django: как я могу защитить от одновременной модификации записей в базе данных

Если существует способ защитить от одновременных модификаций одной и той же записи базы данных двумя или более пользователями?

Было бы приемлемо показать сообщение об ошибке пользователю, выполняющему вторую операцию фиксации/сохранения, но данные не должны быть перезаписаны без изменения.

Я думаю, что блокировка записи не является вариантом, так как пользователь может использовать кнопку "Назад" или просто закрыть свой браузер, оставив блокировку навсегда.

4b9b3361

Ответ 1

Вот как я делаю оптимистичную блокировку в 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

Ответ 2

Этот вопрос немного устарел, и мой ответ немного запоздалый, но после того, что я понимаю, это было исправлено в Django 1.4, используя:

select_for_update(nowait=True)

см. docs

Возвращает набор запросов, который блокирует строки до конца транзакции, генерируя инструкцию SELECT... FOR UPDATE SQL для поддерживаемых баз данных.

Обычно, если другая транзакция уже приобрела блокировку на одной из выбранных строк, запрос будет блокироваться до тех пор, пока блокировка не будет отпущена. Если это не то поведение, которое вы хотите, вызовите select_for_update (nowait = True). Это сделает вызов неблокирующим. Если конфликтная блокировка уже получена другой транзакцией, DatabaseError будет поднят при оценке запроса.

Конечно, это будет работать только в том случае, если внутренняя поддержка поддерживает функцию "выбрать для обновления", которая, например, sqlite, не работает. К сожалению: nowait=True не поддерживается MySql, там вам нужно использовать: nowait=False, который будет блокироваться только до тех пор, пока блокировка не будет выпущена.

Ответ 3

Фактически, транзакции не помогут вам здесь... если вы не хотите, чтобы транзакции выполнялись по нескольким HTTP-запросам (что вам, скорее всего, не нужно).

То, что мы обычно используем в этих случаях, - "Оптимистичное блокирование". Насколько я знаю, ORM Django не поддерживает это. Но было несколько обсуждений по поводу добавления этой функции.

Итак, вы сами. В основном, что вам нужно сделать, это добавить в свою модель "версию" и передать ее пользователю как скрытое поле. Обычный цикл обновления:

  • прочитайте данные и покажите их пользователю
  • пользователь изменяет данные
  • пользователь публикует данные
  • приложение сохраняет его обратно в базу данных.

Чтобы реализовать оптимистичную блокировку, когда вы сохраняете данные, вы проверяете, соответствует ли версия, полученная вами от пользователя, той же, что и в базе данных, а затем обновляет базу данных и увеличивает версию. Если это не так, это означает, что произошли изменения с момента загрузки данных.

Вы можете сделать это с помощью одного вызова SQL с чем-то вроде:

UPDATE ... WHERE version = 'version_from_user';

Этот вызов будет обновлять базу данных только в том случае, если версия остается прежней.

Ответ 4

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 позволяет мне молча пропустить дублированные триггеры.

Ответ 5

В дальнейшем обратитесь к https://github.com/RobCombs/django-locking. Он блокируется способом, который не оставляет вечных замков, путем разблокировки javascript, когда пользователь покидает страницу, и блокирует таймауты (например, в случае сбоя браузера пользователя). Документация довольно полная.

Ответ 6

Вероятно, вы должны использовать промежуточное программное обеспечение django, даже несмотря на эту проблему.

Что касается вашей реальной проблемы, когда несколько пользователей редактируют одни и те же данные... да, используйте блокировку. ИЛИ:

Проверьте, в какой версии пользователь обновляется (сделайте это безопасно, чтобы пользователи не могли просто взломать систему, чтобы сказать, что они обновляют последнюю копию!) и обновлять только в том случае, если эта версия актуальна. В противном случае отправьте пользователю новую страницу с оригинальной версией, которую они редактировали, с их переданной версией и новой версией, написанной другими. Попросите их объединить изменения в одну, полностью обновленную версию. Вы можете попытаться автоматически объединить их с помощью набора инструментов, такого как diff + patch, но в любом случае вам понадобится метод ручного слияния, работающий в случае сбоя, поэтому начните с этого. Кроме того, вам нужно сохранить историю версий и позволить администраторам возвращать изменения, если кто-то непреднамеренно или намеренно испортил слияние. Но, вероятно, вы все равно должны это иметь.

Скорее всего, приложение или библиотека django, которая делает большую часть этого для вас.

Ответ 7

Еще одна вещь, которую нужно искать - это слово "атомный". Атомная операция означает, что изменение базы данных произойдет либо успешно, либо не будет выполнено. Быстрый поиск показывает этот вопрос с вопросом об атомных операциях в Django.

Ответ 8

Идея выше

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

выглядит великолепно и должен работать нормально даже без сериализуемых транзакций.

Проблема заключается в том, как увеличить поведение deafult.save(), поскольку не нужно выполнять ручную настройку, чтобы вызвать метод .update().

Я рассмотрел идею Custom Manager.

Мой план - переопределить метод Manager _update, который вызывается Model.save_base() для выполнения обновления.

Это текущий код в 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 реализует довольно некоторое voodoo в этой области через django.db.models.deletion.Collector.

Странно, что у modren-инструмента, такого как Django, отсутствует руководство для Optimictic Concurency Control.

Я буду обновлять этот пост, когда решу загадку. Надеемся, что решение будет в хорошем питоническом ключе, которое не будет включать в себя тонны кодирования, странные виды, пропуская основные части Django и т.д.

Ответ 9

Чтобы быть в безопасности, база данных должна поддерживать транзакции.

Если поля являются "свободными формами", например. текст и т.д., и вы должны разрешить нескольким пользователям редактировать одни и те же поля (у вас не может быть права доступа к одному пользователю), вы можете сохранить исходные данные в переменной. Когда пользователь совершает транзакции, проверьте, не изменились ли исходные данные из исходных данных (если нет, вам не нужно беспокоиться о DB путем перезаписи старых данных) если исходные данные по сравнению с текущими данными в db одинаковы, вы можете сохранить, если он изменился, вы можете показать пользователю разницу и спросить пользователя, что делать.

Если поля являются цифрами, например. баланс счета, количество элементов в магазине и т.д., вы можете обрабатывать его более автоматически, если вы подсчитаете разницу между исходным значением (сохраненным, когда пользователь начал заполнять форму), и новое значение, которое вы можете начать транзакцию, читать текущую значение и добавить разницу, а затем завершить транзакцию. Если вы не можете иметь отрицательные значения, вы должны прервать транзакцию, если результат отрицательный, и сообщите пользователю.

Я не знаю django, поэтому я не могу дать вам cod3s..;)

Ответ 10

Отсюда:
Как предотвратить перезапись объекта, который кто-то еще изменил

Я предполагаю, что временная метка будет храниться как скрытое поле в форме, которую вы пытаетесь сохранить.

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