Se você já dedicou muito tempo ao gerenciamento de transações de banco de dados do Django, sabe como isso pode ser confuso. No passado, a documentação fornecia um pouco de profundidade, mas a compreensão só vinha através da construção e experimentação.
Havia uma infinidade de decoradores para trabalhar, como
commit_on_success
, commit_manually
, commit_unless_managed
, rollback_unless_managed
, enter_transaction_management
, leave_transaction_management
, apenas para citar alguns. Felizmente, com o Django 1.6, tudo isso sai pela porta. Você realmente precisa saber apenas sobre algumas funções agora. E chegaremos a eles em apenas um segundo. Primeiro, vamos abordar estes tópicos:- O que é gerenciamento de transações?
- O que há de errado com o gerenciamento de transações antes do Django 1.6?
Antes de pular em:
- O que há de certo sobre o gerenciamento de transações no Django 1.6?
E então lidando com um exemplo detalhado:
- Exemplo de faixa
- Transações
- A maneira recomendada
- Usando um decorador
- Transação por solicitação HTTP
- SavePoints
- Transações aninhadas
O que é uma transação?
De acordo com o SQL-92, “uma transação SQL (às vezes simplesmente chamada de “transação”) é uma sequência de execuções de instruções SQL que é atômica em relação à recuperação”. Em outras palavras, todas as instruções SQL são executadas e confirmadas juntas. Da mesma forma, quando revertidas, todas as instruções são revertidas juntas.
Por exemplo:
# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT
Portanto, uma transação é uma única unidade de trabalho em um banco de dados. E essa única unidade de trabalho é demarcada por uma transação inicial e, em seguida, um commit ou um rollback explícito.
O que há de errado com o gerenciamento de transações antes do Django 1.6?
Para responder completamente a esta pergunta, devemos abordar como as transações são tratadas no banco de dados, bibliotecas de clientes e dentro do Django.
Bancos de dados
Cada instrução em um banco de dados deve ser executada em uma transação, mesmo que a transação inclua apenas uma instrução.
A maioria dos bancos de dados tem um
AUTOCOMMIT
configuração, que geralmente é definida como True como padrão. Este AUTOCOMMIT
envolve cada instrução em uma transação que é imediatamente confirmada se a instrução for bem-sucedida. Claro que você pode chamar manualmente algo como START_TRANSACTION
que suspenderá temporariamente o AUTOCOMMIT
até você chamar COMMIT_TRANSACTION
ou ROLLBACK
. No entanto, a vantagem aqui é que o
AUTOCOMMIT
configuração aplica um commit implícito após cada declaração . Bibliotecas de cliente
Depois, há as bibliotecas de cliente do Python como sqlite3 e mysqldb, que permitem que programas Python façam interface com os próprios bancos de dados. Tais bibliotecas seguem um conjunto de padrões de como acessar e consultar os bancos de dados. Esse padrão, o DB API 2.0, é descrito no PEP 249. Embora possa ser uma leitura um pouco seca, uma observação importante é que o PEP 249 afirma que o banco de dados
AUTOCOMMIT
deve estar DESLIGADO por padrão. Isso claramente entra em conflito com o que está acontecendo no banco de dados:
- As instruções SQL sempre precisam ser executadas em uma transação, que o banco de dados geralmente abre para você via
AUTOCOMMIT
. - No entanto, de acordo com o PEP 249, isso não deveria acontecer.
- As bibliotecas de cliente devem espelhar o que acontece no banco de dados, mas como não podem ativar o
AUTOCOMMIT
ativados por padrão, eles simplesmente envolvem suas instruções SQL em uma transação, assim como o banco de dados.
OK. Fique comigo mais um pouco.
Django
Entra Django. Django também tem algo a dizer sobre gerenciamento de transações. No Django 1.5 e anteriores, o Django basicamente rodava com uma transação aberta e confirmava automaticamente essa transação quando você gravava dados no banco de dados. Então, toda vez que você chama algo como
model.save()
ou model.update()
, o Django gerou as instruções SQL apropriadas e confirmou a transação. Também no Django 1.5 e anteriores, foi recomendado que você usasse o
TransactionMiddleware
para vincular transações a solicitações HTTP. Cada solicitação recebeu uma transação. Se a resposta retornasse sem exceções, o Django confirmaria a transação, mas se sua função de visualização gerasse um erro, ROLLBACK
seria chamado. Com efeito, desativou o AUTOCOMMIT
. Se você quisesse um gerenciamento de transações padrão no estilo de autocommit em nível de banco de dados, você teria que gerenciar as transações sozinho - geralmente usando um decorador de transações em sua função de visualização, como @transaction.commit_manually
, ou @transaction.commit_on_success
. Respire. Ou dois.
O que isso significa?
Sim, há muita coisa acontecendo lá, e acontece que a maioria dos desenvolvedores só quer os autocommits de nível de banco de dados padrão - o que significa que as transações ficam nos bastidores, fazendo suas coisas, até que você precise ajustá-las manualmente.
O que há de certo sobre o gerenciamento de transações no Django 1.6?
Agora, bem-vindo ao Django 1.6. Faça o seu melhor para esquecer tudo o que acabamos de falar e simplesmente lembre-se que no Django 1.6, você usa o banco de dados
AUTOCOMMIT
e gerencie as transações manualmente quando necessário. Essencialmente, temos um modelo muito mais simples que basicamente faz o que o banco de dados foi projetado para fazer em primeiro lugar. Chega de teoria. Vamos codificar.
Exemplo de faixa
Aqui temos esta função de visualização de exemplo que lida com o registro de um usuário e a chamada para o Stripe para processamento de cartão de crédito.
def register(request):
user = None
if request.method == 'POST':
form = UserForm(request.POST)
if form.is_valid():
customer = Customer.create("subscription",
email = form.cleaned_data['email'],
description = form.cleaned_data['name'],
card = form.cleaned_data['stripe_token'],
plan="gold",
)
cd = form.cleaned_data
try:
user = User.create(cd['name'], cd['email'], cd['password'],
cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
else:
request.session['user'] = user.pk
return HttpResponseRedirect('/')
else:
form = UserForm()
return render_to_response(
'register.html',
{
'form': form,
'months': range(1, 12),
'publishable': settings.STRIPE_PUBLISHABLE,
'soon': soon(),
'user': user,
'years': range(2011, 2036),
},
context_instance=RequestContext(request)
)
Essa visualização primeiro chama
Customer.create
que na verdade chama o Stripe para lidar com o processamento do cartão de crédito. Em seguida, criamos um novo usuário. Se recebermos uma resposta do Stripe, atualizaremos o cliente recém-criado com o stripe_id
. Se não conseguirmos um cliente de volta (o Stripe está desativado), adicionaremos uma entrada ao UnpaidUsers
tabela com o e-mail do cliente recém-criado, para que possamos pedir que tentem novamente os detalhes do cartão de crédito mais tarde. A ideia é que, mesmo que o Stripe esteja fora do ar, o usuário ainda possa se registrar e começar a usar nosso site. Vamos apenas pedir-lhes novamente em uma data posterior para as informações do cartão de crédito.
Eu entendo que isso pode ser um exemplo um pouco artificial, e não é a maneira como eu implementaria essa funcionalidade se precisasse, mas o objetivo é demonstrar transações.
Avante. Pensando em transações, e tendo em mente que por padrão o Django 1.6 nos dá
AUTOCOMMIT
comportamento para nosso banco de dados, vamos examinar o código relacionado ao banco de dados um pouco mais. cd = form.cleaned_data
try:
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
# ...
Você consegue identificar algum problema? Bem, o que acontece se o
UnpaidUsers(email=cd['email']).save()
linha falha? Você terá um usuário, cadastrado no sistema, que o sistema acha que verificou seu cartão de crédito, mas na realidade não verificou o cartão.
Queremos apenas um dos dois resultados:
- O usuário é criado (no banco de dados) e tem um
stripe_id
. - O usuário foi criado (no banco de dados) e não possui um
stripe_id
E uma linha associada emUnpaidUsers
é gerada uma tabela com o mesmo endereço de e-mail.
O que significa que queremos que as duas instruções de banco de dados separadas sejam confirmadas ou revertidas. Um caso perfeito para a transação humilde.
Primeiro, vamos escrever alguns testes para verificar se as coisas se comportam da maneira que queremos.
@mock.patch('payments.models.UnpaidUsers.save', side_effect = IntegrityError)
def test_registering_user_when_strip_is_down_all_or_nothing(self, save_mock):
#create the request used to test the view
self.request.session = {}
self.request.method='POST'
self.request.POST = {'email' : '[email protected]',
'name' : 'pyRock',
'stripe_token' : '...',
'last_4_digits' : '4242',
'password' : 'bad_password',
'ver_password' : 'bad_password',
}
#mock out stripe and ask it to throw a connection error
with mock.patch('stripe.Customer.create', side_effect =
socket.error("can't connect to stripe")) as stripe_mock:
#run the test
resp = register(self.request)
#assert there is no record in the database without stripe id.
users = User.objects.filter(email="[email protected]")
self.assertEquals(len(users), 0)
#check the associated table also didn't get updated
unpaid = UnpaidUsers.objects.filter(email="[email protected]")
self.assertEquals(len(unpaid), 0)
O decorador no topo do teste é uma simulação que lançará um 'IntegrityError' quando tentarmos salvar no
UnpaidUsers
tabela. Isso é para responder à pergunta:“O que acontece se o
UnpaidUsers(email=cd['email']).save()
linha falha?” O próximo pedaço de código apenas cria uma sessão simulada, com as informações apropriadas que precisamos para nossa função de registro. E então o with mock.patch
força o sistema a acreditar que o Stripe está fora do ar... finalmente chegamos ao teste. resp = register(self.request)
A linha acima apenas chama nossa função de visualização de registro passando a solicitação simulada. Em seguida, apenas verificamos para garantir que as tabelas não estejam atualizadas:
#assert there is no record in the database without stripe_id.
users = User.objects.filter(email="[email protected]")
self.assertEquals(len(users), 0)
#check the associated table also didn't get updated
unpaid = UnpaidUsers.objects.filter(email="[email protected]")
self.assertEquals(len(unpaid), 0)
Portanto, deve falhar se executarmos o teste:
======================================================================
FAIL: test_registering_user_when_strip_is_down_all_or_nothing (tests.payments.testViews.RegisterPageTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/j1z0/.virtualenvs/django_1.6/lib/python2.7/site-packages/mock.py", line 1201, in patched
return func(*args, **keywargs)
File "/Users/j1z0/Code/RealPython/mvp_for_Adv_Python_Web_Book/tests/payments/testViews.py", line 266, in test_registering_user_when_strip_is_down_all_or_nothing
self.assertEquals(len(users), 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Legal. Parece engraçado dizer isso, mas é exatamente o que queríamos. Lembre-se:estamos praticando TDD aqui. A mensagem de erro nos diz que o usuário está realmente sendo armazenado no banco de dados - o que é exatamente o que não queremos porque eles não pagaram!
Transações para o resgate…
Transações
Na verdade, existem várias maneiras de criar transações no Django 1.6.
Vamos passar por alguns.
A maneira recomendada
De acordo com a documentação do Django 1.6:
“O Django fornece uma única API para controlar as transações do banco de dados. […] A atomicidade é a propriedade definidora das transações de banco de dados. atomic nos permite criar um bloco de código dentro do qual a atomicidade do banco de dados é garantida. Se o bloco de código for concluído com êxito, as alterações serão confirmadas no banco de dados. Se houver uma exceção, as alterações serão revertidas.”
Atomic pode ser usado como um decorador ou como um context_manager. Então, se o usarmos como gerenciador de contexto, o código em nossa função de registro ficaria assim:
from django.db import transaction
try:
with transaction.atomic():
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
Observe a linha
with transaction.atomic()
. Todo código dentro desse bloco será executado dentro de uma transação. Então, se re-executarmos nossos testes, todos eles devem passar! Lembre-se de que uma transação é uma única unidade de trabalho, portanto, tudo dentro do gerenciador de contexto é revertido quando o UnpaidUsers
falha na chamada. Usando um decorador
Também podemos tentar adicionar atomic como decorador.
@transaction.atomic():
def register(request):
# ...snip....
try:
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
Se executarmos novamente nossos testes, eles falharão com o mesmo erro que tivemos antes.
Por que é que? Por que a transação não foi revertida corretamente? A razão é porque
transaction.atomic
está procurando por algum tipo de exceção e, bem, pegamos esse erro (ou seja, o IntegrityError
em nosso bloco try except), então transaction.atomic
nunca o vi e, portanto, o padrão AUTOCOMMIT
funcionalidade assumiu. Mas é claro que remover o try except fará com que a exceção seja lançada na cadeia de chamadas e provavelmente exploda em outro lugar. Então também não podemos fazer isso.
Portanto, o truque é colocar o gerenciador de contexto atômico dentro do bloco try except, que foi o que fizemos em nossa primeira solução. Olhando para o código correto novamente:
from django.db import transaction
try:
with transaction.atomic():
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
Quando
UnpaidUsers
dispara o IntegrityError
a transaction.atomic()
gerenciador de contexto irá pegá-lo e executar a reversão. No momento em que nosso código é executado no manipulador de exceção, (ou seja, o form.addError
line) a reversão será feita e poderemos fazer chamadas de banco de dados com segurança, se necessário. Observe também quaisquer chamadas de banco de dados antes ou depois do transaction.atomic()
gerenciador de contexto não será afetado, independentemente do resultado final do gerenciador de contexto. Transação por solicitação HTTP
O Django 1.6 (assim como o 1.5) também permite operar no modo “Transação por solicitação”. Neste modo, o Django irá automaticamente envolver sua função de visualização em uma transação. Se a função lançar uma exceção, o Django reverterá a transação, caso contrário, ele confirmará a transação.
Para configurá-lo, você deve definir
ATOMIC_REQUEST
para True na configuração do banco de dados para cada banco de dados que você deseja que tenha esse comportamento. Então em nosso “settings.py” fazemos a mudança assim:DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(SITE_ROOT, 'test.db'),
'ATOMIC_REQUEST': True,
}
}
Na prática, isso se comporta exatamente como se você colocasse o decorador em nossa função de visualização. Portanto, não serve aos nossos propósitos aqui.
No entanto, vale a pena notar que tanto com
ATOMIC_REQUESTS
e o @transaction.atomic
decorador é possível ainda capturar/tratar esses erros depois que eles são lançados da view. Para detectar esses erros, você teria que implementar algum middleware personalizado, ou substituir urls.hadler500 ou criar um modelo 500.html. Salvar pontos
Embora as transações sejam atômicas, elas podem ser divididas em pontos de salvamento. Pense nos pontos de salvamento como transações parciais.
Portanto, se você tiver uma transação que precise de quatro instruções SQL para ser concluída, poderá criar um ponto de salvamento após a segunda instrução. Uma vez que o ponto de salvamento é criado, mesmo que a 3ª ou 4ª instrução falhe, você pode fazer uma reversão parcial, livrando-se da 3ª e da 4ª instrução, mas mantendo as duas primeiras.
Portanto, é basicamente como dividir uma transação em transações leves menores, permitindo que você faça rollbacks parciais ou commits.
Mas lembre-se se a transação principal for revertida (talvez por causa de umIntegrityError
que foi gerado e não capturado, todos os pontos de salvamento também serão revertidos).
Vejamos um exemplo de como funcionam os pontos de salvamento.
@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
Aqui toda a função está em uma transação. Após criar um novo usuário, criamos um ponto de salvamento e obtemos uma referência ao ponto de salvamento. As próximas três declarações-
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
-não fazem parte do savepoint existente, então eles têm a chance de fazer parte do próximo
savepoint_rollback
, ou savepoint_commit
. No caso de um savepoint_rollback
, a linha user = User.create('jj','inception','jj','1234')
ainda será confirmado no banco de dados, mesmo que o restante das atualizações não. Dito de outra forma, estes dois testes a seguir descrevem como os pontos de salvamento funcionam:
def test_savepoint_rollbacks(self):
self.save_points(False)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#note the values here are from the original create call
self.assertEquals(users[0].stripe_id, '')
self.assertEquals(users[0].name, 'jj')
def test_savepoint_commit(self):
self.save_points(True)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#note the values here are from the update calls
self.assertEquals(users[0].stripe_id, '4')
self.assertEquals(users[0].name, 'starting down the rabbit hole')
Além disso, depois de confirmar ou reverter um ponto de salvamento, podemos continuar a trabalhar na mesma transação. E esse trabalho não será afetado pelo resultado do ponto de salvamento anterior.
Por exemplo, se atualizarmos nossos
save_points
funcionar como tal:@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.save()
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
user.create('limbo','illbehere@forever','mind blown',
'1111')
Independentemente de
savepoint_commit
ou savepoint_rollback
foi chamado o usuário 'limbo' ainda será criado com sucesso. A menos que algo mais faça com que toda a transação seja revertida. Transações aninhadas
Além de especificar manualmente os pontos de salvamento, com
savepoint()
, savepoint_commit
e savepoint_rollback
, a criação de uma transação aninhada criará automaticamente um ponto de salvamento para nós e o reverterá se recebermos um erro. Estendendo um pouco mais nosso exemplo, temos:
@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.save()
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
try:
with transaction.atomic():
user.create('limbo','illbehere@forever','mind blown',
'1111')
if not save: raise DatabaseError
except DatabaseError:
pass
Aqui podemos ver que depois de lidarmos com nossos savepoints, estamos usando o
transaction.atomic
gerenciador de contexto para encerrar nossa criação do usuário 'limbo'. Quando esse gerenciador de contexto é chamado, ele está criando um ponto de salvamento (porque já estamos em uma transação) e esse ponto de salvamento será confirmado ou revertido ao sair do gerenciador de contexto. Assim, os dois testes a seguir descrevem seu comportamento:
def test_savepoint_rollbacks(self):
self.save_points(False)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#savepoint was rolled back so we should have original values
self.assertEquals(users[0].stripe_id, '')
self.assertEquals(users[0].name, 'jj')
#this save point was rolled back because of DatabaseError
limbo = User.objects.filter(email="illbehere@forever")
self.assertEquals(len(limbo),0)
def test_savepoint_commit(self):
self.save_points(True)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#savepoint was committed
self.assertEquals(users[0].stripe_id, '4')
self.assertEquals(users[0].name, 'starting down the rabbit hole')
#save point was committed by exiting the context_manager without an exception
limbo = User.objects.filter(email="illbehere@forever")
self.assertEquals(len(limbo),1)
Então, na realidade, você pode usar
atomic
ou savepoint
para criar pontos de salvamento dentro de uma transação. Com atomic
você não precisa se preocupar explicitamente com o commit/rollback, como com savepoint
você tem controle total sobre quando isso acontece. Conclusão
Se você teve alguma experiência anterior com versões anteriores de transações do Django, você pode ver o quanto o modelo de transação é mais simples. Também tendo
AUTOCOMMIT
on por padrão é um ótimo exemplo de padrões “sanos” que o Django e o Python se orgulham de entregar. Para muitos sistemas, você não precisará lidar diretamente com transações, apenas deixe AUTOCOMMIT
fazer o seu trabalho. Mas se você fizer isso, espero que este post tenha lhe dado as informações que você precisa para gerenciar transações no Django como um profissional.