Re: Updating a model instance with extra checks.

48 views
Skip to first unread message

Alexis Roda

unread,
Aug 20, 2012, 1:35:43 PM8/20/12
to django...@googlegroups.com
Al 20/08/12 18:53, En/na Sebastien Flory ha escrit:
> Hi everyone,
>
> I'm looking for the proper django way to do an update of an attribute on
> my model instance, but only if the attribute current value is checked
> agains't a condition, in an atomic way, something like this:
>
> def use_money(self, value):
> begin_transaction()
> real_money = F('money')
> if real_money >= value:
> self.money = F('money') - value
> self.save()
> end_transaction()
>
> I want to make sure that I avoid race condition so money never goes below 0.

Take a look at:

https://docs.djangoproject.com/en/1.4/topics/db/transactions/

Tying transactions to HTTP requests
===================================

The recommended way to handle transactions in Web requests is to tie
them to the request and response phases via Django�s TransactionMiddleware.

It works like this: When a request starts, Django starts a transaction.
If the response is produced without problems, Django commits any pending
transactions. If the view function produces an exception, Django rolls
back any pending transactions.

...

However, if you need more fine-grained control over how transactions are
managed, you can use a set of functions in django.db.transaction to
control transactions on a per-function or per-code-block basis.




HTH

Thomas Orozco

unread,
Aug 20, 2012, 1:37:22 PM8/20/12
to django...@googlegroups.com

A few suggestions :

Circumvent the problem with smarter design: don't store the money on the object, make the user's money the sum of all their transactions (credit - debit).
You get lesser performance, but you also get history!

Maybe you could try (not sure about that):

MyModel.objects.filter(money__gte = value, pk = self.pk).update(F...)

and inspect the return value (number of updated rows!).
Now, you'd need to make sure django does that in a single statement.

If that doesn't work, I think update is the way to go anyway, but it might get a bit messy.

F... is an F object whose syntax I don't have off the top of my head.

Le 20 août 2012 18:54, "Sebastien Flory" <sfl...@gmail.com> a écrit :
Hi everyone,

I'm looking for the proper django way to do an update of an attribute on my model instance, but only if the attribute current value is checked agains't a condition, in an atomic way, something like this:

def use_money(self, value):
  begin_transaction()
  real_money = F('money')
  if real_money >= value:
    self.money = F('money') - value
    self.save()
  end_transaction()

I want to make sure that I avoid race condition so money never goes below 0.

Can you help me out?

Thanks,

Sebastien

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To view this discussion on the web visit https://groups.google.com/d/msg/django-users/-/hr1fBuAcX3kJ.
To post to this group, send email to django...@googlegroups.com.
To unsubscribe from this group, send email to django-users...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/django-users?hl=en.

Melvyn Sopacua

unread,
Aug 20, 2012, 7:26:31 PM8/20/12
to django...@googlegroups.com
On 20-8-2012 19:37, Thomas Orozco wrote:

> Circumvent the problem with smarter design: don't store the money on the
> object, make the user's money the sum of all their transactions (credit -
> debit).
> You get lesser performance, but you also get history!

This does not circumvent the problem but aggravates it. The problem is
how to determine if a withdrawal is allowed before doing the withdrawal.
Not having the current balance available but instead having to do
complex queries on a possibly huge set of rows, increases the chances of
transactions being OK'd that should not be when multiple withdrawals are
sent in parallel.

Alexis has mentioned some options, but the real safeguard is to make
current balance field a positive decimal field and propagate this to the
database layer by ensuring a constraint is created. Even if two
transactions in parallel are working with the same initial balance, the
constraint will deny at least one of them. This is also why you should
enclose the entire process (add withdrawal to statement, decrease the
balance) in a single transaction. Possibly the
@transaction.commit_on_success decorator may prove useful.

--
Melvyn Sopacua

Thomas Orozco

unread,
Aug 20, 2012, 7:41:38 PM8/20/12
to django...@googlegroups.com

I think I didn't make what I meant clear enough:

What do you think about the following:

. Insert record
. Calculate balance by summing all records before (including) the one you just inserted (and I think you will agree this is not an extremely complex query)
. If balance is positive, it's approved (and you'd probably want to change some status field to reflect that)
. If balance is negative, it's refused - and you can change status (or delete, though I wouldn't recommend that)

Nothing prevents us from differentiating inserting a record and approving the transaction, right?

Depending on whether you use a status field or not, and which transactions you take into account to know whether you will approve, you can get on the safe side.

Just assuming that a pending transaction (that is, a transaction that has been inserted but not approved yet) will be approved should prevent approving a withdrawal you should be refusing (but could lead you to refuse one you should be approving if your process is too long)

The day performance becomes an issue, you can look into alternate solutions, such as indeed storing the current balance somewhere.

--
You received this message because you are subscribed to the Google Groups "Django users" group.

Thomas Orozco

unread,
Aug 20, 2012, 8:11:42 PM8/20/12
to django...@googlegroups.com
As a followup to the suggestion of MyModel.objects.filter(money__gte = value, pk = self.pk).update(F...)

Here's an example:

We have testapp/models.py:

from django.db import models
class TestModel(models.Model):
    balance =  models.IntegerField()


>>> from django.db.models import F
>>> TestModel.objects.create(balance = 5) #Pk will be 1 I just create one. 
>>> import logging
>>> l = logging.getLogger('django.db.backends')
>>> l.setLevel(logging.DEBUG)
>>> l.addHandler(logging.StreamHandler())
>>> TestModel.objects.filter(balance__gte = 4, pk = 1).update(balance = F('balance') - 4)
(0.001) UPDATE "testapp_testmodel" SET "balance" = "testapp_testmodel"."balance" - 4 WHERE ("testapp_testmodel"."balance" >= 4  AND "testapp_testmodel"."id" = 1 ); args=(4, 4, 1)
1
>>> TestModel.objects.filter(balance__gte = 4, pk = 1).update(balance = F('balance') - 4)
(0.000) UPDATE "testapp_testmodel" SET "balance" = "testapp_testmodel"."balance" - 4 WHERE ("testapp_testmodel"."balance" >= 4  AND "testapp_testmodel"."id" = 1 ); args=(4, 4, 1)
0



So this seems to generate a single SQL statement.

I'm not totally familiar with database administration though, so as Melvyn rightly pointed out, it's always better if you can get the extra security of having an SQL constraint into your dabatase and wrap your queries in a transaction (as you'll probably be adding a line to the statement if the withdrawal succeeds).


2012/8/20 Thomas Orozco <g.orozc...@gmail.com>

Sebastien Flory

unread,
Aug 21, 2012, 4:18:16 AM8/21/12
to django...@googlegroups.com
That was my idea too, but using this way, the pre_save / post_save signals wont be triggered.
It seems strange to me to be using the objects manager instead of the model instance directly, no?

Seb

Kurtis Mullins

unread,
Aug 21, 2012, 11:06:48 AM8/21/12
to django...@googlegroups.com
I remember running into similar situations in a different domain (collision detection in physics engines). It was a pain in the butt :) I'd say first, figure out what you want to do if the process does reach a point where there isn't sufficient funds to perform the transaction. Then take multiple steps to catch that state. Finally, implement it in a way that will leave no side effects.

Possibly using a queue may be good for this. Let the user perform an action which will attempt to use/move the funds. Then, actually try to perform the operation behind the scenes. Finally, update the user's information so they can see the change or any errors that occurred. This should (hypothetically, assuming you only have one queue per user or account) make sure that the transactions are performed in order and should also eliminate race conditions. This would be similar in implementation to how many bank web-sites let you transfer funds between accounts but it's typically not an update you see immediately.

To view this discussion on the web visit https://groups.google.com/d/msg/django-users/-/x8CZ3-F30PsJ.

Thomas Orozco

unread,
Aug 21, 2012, 11:29:41 AM8/21/12
to django...@googlegroups.com

I think  you could always send the signals yourself.

Wrap that along in a model method, and I don't see any issue with using the manager!

To view this discussion on the web visit https://groups.google.com/d/msg/django-users/-/x8CZ3-F30PsJ.
Reply all
Reply to author
Forward
0 new messages