2008-11-26 5 views
70

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

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

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

+4

Если один объект может быть обновлен несколькими одновременными пользователями, у вас может возникнуть проблема с большим дизайном. Возможно, стоит подумать о ресурсах, специфичных для пользователя, или разделить этапы обработки на отдельные таблицы, чтобы это не было проблемой. – 2008-11-26 17:03:46

ответ

-1

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

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

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

Я не знаю, Джанго, так что я не могу дать вам Teh cod3s ..;)

27

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

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

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

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

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

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

UPDATE ... WHERE version = 'version_from_user'; 

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

+1

Этот же вопрос также появился на Slashdot. Оптимистичная блокировка, которую вы предлагаете, также была предложена там, но объяснила немного лучше imho: http://hardware.slashdot.org/comments.pl?sid=1381511&cid=29536367 – hopla 2009-09-29 09:35:21

+5

Также обратите внимание, что вы хотите использовать транзакции поверх этого , чтобы избежать этой ситуации: http://hardware.slashdot.org/comments.pl?sid=1381511&cid=29536613 Django предоставляет промежуточное программное обеспечение для автоматического переноса каждого действия в базе данных в транзакции, начиная с первоначального запроса и только фиксируя после успешного ответа: http://docs.djangoproject.com/en/dev/topics/db/transactions/ (заметьте: промежуточное программное обеспечение транзакций помогает избежать этой проблемы с оптимистичной блокировкой, она не обеспечивает блокировку) – hopla 2009-09-29 12:12:58

+0

Я также ищу подробную информацию о том, как это сделать. Пока не повезло. – seanyboy 2009-12-09 15:48:32

0

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

+0

Я не хочу выполнять транзакцию или блокировку нескольких запросов, так как это может занять какое-то время (и, возможно, никогда не закончится вообще) – Ber 2008-11-26 10:57:06

+0

Если транзакция начинается, она должна завершиться. Вы должны только заблокировать запись (или начать транзакцию или все, что вы решите сделать) после того, как пользователь нажмет «отправить», а не когда они откроют запись для просмотра. – 2008-11-26 11:01:20

+0

Да, но проблема у меня другая, поскольку два пользователя открывают одну и ту же форму, а затем они оба фиксируют свои изменения. Я не думаю, что блокировка - это решение для этого. – Ber 2008-11-26 16:24:53

1

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

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

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

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

-6

Отсюда:
How to prevent overwriting an object someone else has modified

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

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

Это, как я оптимистическая блокировка в 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.

я делаю следующие предположения:.

  • фильтра() обновление() приведет один запрос к базе данных, так как фильтр ленив
  • запрос к базе данных является атомарной

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

ПРЕДУПРЕЖДЕНИЕDjango Doc:

Имейте в виду, что метод обновления() является преобразуется непосредственно в SQL заявление. Это массовая операция для прямых обновлений. Она не работает какой-либо Save() методы на вашей модели, или испускают в pre_save или post_save сигналы

3

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

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

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

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

Я рассмотрел идею пользовательского менеджера.

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

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

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

33

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

select_for_update(nowait=True) 

см docs

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

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

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

6

Django 1.11 имеет three convenient options справиться с этой ситуацией в зависимости от бизнес-логики требований:

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