Django: Como posso proteger contra a modificação concorrente de entradas de banco de dados

votos
71

Se há uma maneira de proteger contra modificações concorrentes da mesma entrada de base de dados por dois ou mais usuários?

Seria aceitável para mostrar uma mensagem de erro para o usuário que executa a operação de segunda commit / salvar, mas os dados não devem ser silenciosamente substituídos.

Eu acho que o bloqueio a entrada não é uma opção, como um usuário pode usar o botão Voltar ou simplesmente fechar seu navegador, deixando o bloqueio para sempre.

Publicado 26/11/2008 em 10:00
fonte usuário
Em outras línguas...                            


10 respostas

votos
44

Isto é como eu fazer o bloqueio otimista em 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()

O código listado acima pode ser implementado como um método em Gerente Personalizado .

Eu estou fazendo as seguintes premissas:

  • filter (). update () resultará em uma consulta de banco de dados único, porque filtro é preguiçoso
  • uma consulta de banco de dados é atômica

Essas suposições são suficientes para garantir que ninguém mais actualiza a entrada antes. Se várias linhas são atualizadas desta forma você deve usar transações.

AVISO Django Doc :

Esteja ciente de que o método update () é convertida diretamente para uma instrução SQL. É uma operação em massa para atualizações diretas. Ele não é executado qualquer save () métodos em seus modelos, ou emite os sinais pre_save ou post_save

Respondeu 30/01/2010 em 01:41
fonte usuário

votos
33

Esta questão é um pouco velho e minha resposta um pouco tarde, mas depois que eu entendo isso foi corrigido no Django 1.4 usando:

select_for_update(nowait=True)

veja os docs

Retorna um queryset que irá bloquear linhas até o final da transação, gerando um SELECT ... FOR UPDATE instrução SQL em bancos de dados suportados.

Geralmente, se uma outra transação já adquiriu um bloqueio em uma das linhas selecionadas, a consulta irá bloquear até que o bloqueio seja liberado. Se este não é o comportamento que você quiser, chame select_for_update (nowait = True). Isso fará com que a chamada non-blocking. Se um bloqueio em conflito já é adquirida por outra transação, DatabaseError será levantada quando o queryset é avaliada.

Claro que isto só irá funcionar se o back-end suporta o recurso "selecionar para atualização", que, por exemplo SQLite não. Infelizmente: nowait=Truenão é suportado pelo MySql, lá você tem que usar: nowait=False, que só irá bloquear até que o bloqueio seja liberado.

Respondeu 21/06/2012 em 08:31
fonte usuário

votos
27

Na verdade, as transações não ajudar muito aqui ... a menos que você quer ter transações em execução ao longo de várias solicitações HTTP (que você provavelmente não quer).

O que costumamos usar nesses casos é "bloqueio otimista". é que o Django ORM não suporta que, tanto quanto eu sei. Mas tem havido alguma discussão sobre adicionar esse recurso.

Então você está em seu próprio país. Basicamente, o que você deve fazer é adicionar um campo "versão" para seu modelo e passá-lo para o usuário como um campo oculto. O ciclo normal de uma atualização é:

  1. ler os dados e mostrá-lo para o usuário
  2. utilizador modificar dados
  3. usuário postar os dados
  4. o aplicativo salva-lo de volta no banco de dados.

Para implementar o bloqueio otimista, quando você salvar os dados, verifique se a versão que você voltou do usuário é o mesmo que o do banco de dados e atualizar o banco de dados e incrementar a versão. Se não estiverem, isso significa que houve uma mudança desde os dados foram carregados.

Você pode fazer isso com uma única chamada SQL com algo como:

UPDATE ... WHERE version = 'version_from_user';

Esta chamada irá atualizar o banco de dados apenas se a versão ainda é o mesmo.

Respondeu 26/11/2008 em 11:16
fonte usuário

votos
8

Django 1.11 tem três opções convenientes para lidar com esta situação, dependendo de suas necessidades de lógica de negócios:

  • Something.objects.select_for_update() irá bloquear até que o modelo se tornar livre
  • Something.objects.select_for_update(nowait=True)e pegar DatabaseErrorse o modelo está atualmente bloqueado para atualização
  • Something.objects.select_for_update(skip_locked=True) não vai devolver os objetos que estão atualmente bloqueadas

Na minha aplicação, que tem ambos os fluxos de trabalho interactivo e em lote em vários modelos, eu encontrei estas três opções para resolver a maioria dos meus cenários de processamento simultâneos.

A "espera" select_for_updateé muito conveniente em processos em batelada seqüenciais - Eu quero todos eles para executar, mas deixá-los tomar o seu tempo. A nowaité usado quando um usuário quer modificar um objeto que está atualmente bloqueado para atualização - Vou apenas dizer-lhes que ele está sendo modificado neste momento.

O skip_lockedé útil para um outro tipo de atualização, quando os usuários podem desencadear uma nova verificação de um objeto - e eu não me importo quem aciona-lo, contanto que ele é acionado, por isso skip_lockedme permite ignorar silenciosamente os gatilhos duplicados.

Respondeu 18/05/2017 em 10:20
fonte usuário

votos
3

Para referência futura, veja https://github.com/RobCombs/django-locking . Ele faz bloqueio de uma forma que não deixa fechaduras eternas, por uma mistura de javascript desbloqueio quando o usuário deixa a página, e tempos limite de bloqueio (por exemplo, no caso do navegador do usuário deixa de funcionar). A documentação é muito completa.

Respondeu 02/06/2010 em 16:33
fonte usuário

votos
1

Você provavelmente deve usar o middleware do Django, pelo menos, mesmo independentemente de este problema.

Quanto ao seu problema real de ter vários usuários editar os mesmos dados ... sim, fecho uso. OU:

Verifique qual a versão que um usuário está atualizando contra (fazer isso de forma segura, para que os usuários não podem simplesmente invadir o sistema para dizer que eles estavam atualizando a cópia mais recente!), E só atualizar, se essa versão é atual. Caso contrário, enviar o usuário de volta uma nova página com a versão original que estava editando, sua versão apresentada, ea nova versão (s) escrito por outros. Peça-lhes para mesclar as alterações em uma versão completamente up-to-date. Você pode tentar a auto-merge-los usando um conjunto de ferramentas como diff + patch, mas você precisa ter o método merge manual de trabalho para casos de falha de qualquer maneira, assim que começar com isso. Além disso, você vai precisar para preservar a história de versão, e permitir que os administradores para reverter as alterações, no caso de alguém intencionalmente ou não, mexe-se a mesclagem. Mas você provavelmente deve ter isso de qualquer maneira.

Não é muito provável um aplicativo django / biblioteca que faz a maioria do presente para você.

Respondeu 18/09/2009 em 11:42
fonte usuário

votos
0

A idéia acima

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

parece ótimo e deve funcionar bem, mesmo sem transações serializáveis.

O problema é como aumentar o comportamento .Save deafult () para não ter que fazer o encanamento manual para chamar o método .Update ().

Olhei para a ideia personalizada Manager.

Meu plano é substituir o método _update Manager que é chamado por Model.save_base () para realizar a atualização.

Este é o código atual em Django 1.3

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

O que precisa ser feito IMHO é algo como:

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)

Semelhante coisa precisa acontecer em delete. No entanto excluir é um pouco mais difícil como Django está implementando algum voodoo nesta área através django.db.models.deletion.Collector.

É estranho que ferramenta modren como Django carece de orientação para Optimictic Concurency controle.

Vou atualizar este post quando eu resolver o enigma. Esperemos solução estará em uma maneira Python agradável que não envolve toneladas de codificação, vistas estranhas, ignorando peças essenciais de Django etc.

Respondeu 31/07/2011 em 23:40
fonte usuário

votos
0

Outra coisa é a olhar para a palavra "atômica". Uma operação atômica significa que sua mudança de banco de dados será ou acontecer com sucesso, ou não, obviamente. Uma rápida pesquisa mostra esta questão perguntando sobre operações atômicas em Django.

Respondeu 26/11/2008 em 11:20
fonte usuário

votos
-1

Para ser seguro o banco de dados precisa suportar transações .

Se os campos é "de forma livre", por exemplo texto etc. e você precisa para permitir que vários usuários possam editar os mesmos campos (você não pode ter a propriedade usuário único para os dados), você pode armazenar os dados originais em um variável. Quando o usuário committs, verifique se os dados de entrada mudou a partir dos dados originais (se não, você não precisa se preocupar o DB por reescrever os dados antigos), se os dados originais em comparação com os dados atuais no db é o mesmo você pode salvar, se ele mudou você pode mostrar ao usuário a diferença e perguntar ao usuário o que fazer.

Se os campos são os números eg saldo da conta, número de itens em uma loja etc., você pode lidar com isso mais automaticamente se você calcular a diferença entre o valor original (armazenados quando o usuário começou a preencher o formulário) eo novo valor que você pode iniciar uma transação ler o valor atual e adicione a diferença, em seguida, terminar a transação. Se você não pode ter valores negativos, você deve abortar a transação se o resultado for negativo, e informar ao usuário.

Eu não sei Django, então não posso dar-lhe teh cod3s ..;)

Respondeu 26/11/2008 em 10:23
fonte usuário

votos
-6

A partir daqui:
Como evitar a substituição de um objeto que alguém tenha modificado

Estou assumindo que o timestamp será realizada como um campo oculto no formulário que você está tentando salvar os detalhes.

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()
Respondeu 09/12/2009 em 16:52
fonte usuário

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more