Django: Как я могу защитить от одновременного изменения записей базы данных - PullRequest
75 голосов
/ 26 ноября 2008

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

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

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

Ответы [ 10 ]

46 голосов
/ 30 января 2010

Вот как я делаю оптимистическую блокировку в 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 () в ваших моделях или emit сигналы pre_save или post_save

35 голосов
/ 21 июня 2012

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

select_for_update(nowait=True)

см. документы

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

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

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

28 голосов
/ 26 ноября 2008

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

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

Так что ты сам по себе. По сути, вам нужно добавить поле «версия» в модель и передать его пользователю как скрытое поле. Обычный цикл обновления:

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

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

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

UPDATE ... WHERE version = 'version_from_user';

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

12 голосов
/ 18 мая 2017

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

3 голосов
/ 02 июня 2010

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

1 голос
/ 18 сентября 2009

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

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

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

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

0 голосов
/ 01 августа 2011

Идея выше

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

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

Проблема заключается в том, как расширить поведение по умолчанию .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)

Что нужно сделать ИМХО это что-то вроде:

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, не хватает руководства для Optimictic Concurency Control.

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

0 голосов
/ 09 декабря 2009

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

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

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()
0 голосов
/ 26 ноября 2008

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

0 голосов
/ 26 ноября 2008

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

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

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

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

...