0

I have this accounts model in my django project, which stores the account balance(available money) of all users. Almost every deduction from the account of users is preceded by an amount check i.e. check if the user has x amount of money or more. If yes then go ahead and deduct the amount.

account = AccountDetails.objects.get(user=userid) if int(account.amount) >= fare: account.amount = account.amount-fare account.save() 

Now I want to put a lock in the first .get() statement, so that race conditions can be avoided. A user makes a request twice, and the application executes the above code twice simultaneously, causing one of the requests to override the other.

I found out that select_for_update() does exactly what I want. It locks the row until the end of the transaction.

account = AccountDetails.objects.select_for_update().get(user=userid) 

But it's only available in Django 1.4 or higher and I'm still using Django 1.3 and moving to a new version can't be done right now. Any ideas how can I achieve this in my present Django version?

1 Answer 1

1

Looks like you'll have to use raw SQL. I had a look through the current code and I think it would be more hassle to try and backport this yourself than it would be to just write the SQL.

account = AccountDetails.objects.raw( "SELECT * FROM yourapp_accountdetails FOR UPDATE WHERE user = %s", [user.id] ) 

For convenience and to keep your code DRY you can add this as a method to your AccountDetails model or something.

class AccountDetails(models.Model): @classmethod def get_locked_for_update(cls, user): return cls.objects.raw( "SELECT * FROM yourapp_accountdetails FOR UPDATE WHERE user = %s", [user.id] ) 

yourapp is the name of your application that you would have given when you ran startapp. I'm assuming you have a foreign key relationship on your AccountDetails to a user model of some kind.

The current implementation of select_for_update on Django 1.5 looks like this:

def select_for_update(self, **kwargs): """ Returns a new QuerySet instance that will select objects with a FOR UPDATE lock. """ # Default to false for nowait nowait = kwargs.pop('nowait', False) obj = self._clone() obj.query.select_for_update = True obj.query.select_for_update_nowait = nowait return obj 

So that's pretty simple code. Just setting a few flags on the query object. However those flags won't mean anything when the query is executed. So at some point you'll need to write raw SQL. At this point you only need that query on your AccountDetails model. So just put it there for now. Maybe later you'll need it on another model. That's when you'll have to decide how to share the code between models.

Sign up to request clarification or add additional context in comments.

5 Comments

+1 for suggesting to add it as a method to the model :) But can I avoid using raw sql query all together?
See updated answer. I think the answer is no. Not without backporting a lot of stuff from later versions of Django.
@ayechedee Thanks for the explanation. I was wondering wouldn't it be better if I implemented the get_locked_for_update() as a class method rather than an instance method. It'll make more sense to call it on the class like: AccountDetails.get_locked_for_update(user) than AccountDetails.objects.get(user=userid).get_locked_for_update(). Right?
Yeah, that would make more sense. We can just add the classmethod decorator and pass in the user.
I have a follow up question to this solution. Kindly go through the question and see if you can help.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.