@Carl,

I've heard of the term 'atomic operations' , but didn't realize it had 
support in django. Thank you for the detailed explanation!

It's an interesting idea - to mark the field in the database as a positive 
integer, but since it doesn't apply to all DBs, I think I'll keep it aside 
for now. 

@Waitman,

Don't think I've ever come across the term 'backorder', but its an 
interesting concept. And very appreciated, from the business angle. I need 
to think more on this.

Thanks for the help,
Abraham V.






On Thursday, February 19, 2015 at 12:03:57 AM UTC+5:30, Carl Meyer wrote:
>
> Hi Abraham, 
>
> On 02/17/2015 10:01 PM, Abraham Varricatt wrote: 
> > I'm trying to make an app where folks can order X quantity of an item. 
> > The condition is that the order should only be made if inventory exists. 
> > Assume that we have stock of Y items. This means that only if Y >= X 
> > should we allow the sale to go through. And once we accept an order, the 
> > inventory should be updated so that Y = Y - X. 
>
> In general, the construct you need is called a "transaction", which 
> ensures that a series of database operations will either all be 
> committed together, or rolled back if they can't be successfully 
> completed. The Django API for that is "django.db.transaction.atomic". 
>
> > The way I've currently implemented this is with a pair of getter/setter 
> > methods. I have a model 'ShopItem' which has two fields called 
> > 'quantity' and 'name' (this is unique). I've made a utility class 
> > (outside model source file) where I've made the following 2 functions; 
> > 
> > def get_quantity(name): 
> >   stuff_left = 0 
> >   try: 
> >     item = ShopItem.objects.get(name=name) 
> >     stuff_left = item.quantity 
> >   except ShopItem.DoesNotExist: 
> >     pass # return zero 
> >   return stuff_left 
> > 
> > def set_quantity(name, stuff_left): 
> >   item = ShopItem.objects.get(name=name) 
> >   item.quantity = stuff_left 
> >   item.save() 
> > 
> > 
> > Elsewhere in my project, if I need to display the remaining quantity of 
> > an item in a view, I'll pass the result from get_quantity(). At sale 
> > time here is how I update things; 
> > 
> > def customer_buy(name, number): 
> >   temp = get_quantity(name) 
> >   if (temp >= number): 
> >     temp = temp - number 
> >     set_quantity(name, temp) 
> >     // do other things 
> >   else: 
> >     // sale failed, do something else 
> > 
> > I tested this on my system and things appear to work. But I'm concerned 
> > about race conditions. What if two different customers came in to buy 
> > the same item? As in; if I only have 7 items in stock, but one customer 
> > came to buy 6nos and another 5nos. There is a chance (as per my 
> > understanding) that both orders will be accepted - even worse, my 
> > inventory will not accurately reflect the updated situation. 
> > 
> > Any ideas on how this issue can be resolved? I've come across this 
> > - 
> https://docs.djangoproject.com/en/1.6/ref/models/instances/#updating-attributes-based-on-existing-fields
>  
> > , but not sure how to apply it to the current scenario. 
>
> If we're only considering the changes to your ShopItem model, you don't 
> even need an explicit transaction to avoid race conditions, because (as 
> the docs you linked show) the operation can be completed in a single 
> database query, which is inherently atomic. This is how it would look to 
> do it in a single query: 
>
>     from django.db.models import F 
>
>     def customer_buy(name, number): 
>         ShopItem.objects.filter( 
>             name=name).update(quantity=F('quantity')-number) 
>
> You want this to fail if someone tries to purchase a larger quantity 
> than are available. The best way to do this is via a database-level 
> "check constraint" on the column, such that the database itself will 
> never permit a negative quantity. If you make 'quantity' a 
> PositiveIntegerField (the name is wrong, it actually allows zero too) on 
> your model, and your database is PostgreSQL (or Oracle), Django will add 
> this constraint for you automatically. Then if someone tries to purchase 
> more than are available, you'll get an IntegrityError, which you'd want 
> to catch and handle in some way: 
>
>     from django.db import IntegrityError 
>     from django.db.models import F 
>
>     class InsufficientInventory(Exception): 
>         pass 
>
>     def customer_buy(name, number): 
>         try: 
>             ShopItem.objects.filter( 
>                 name=name).update(quantity=F('quantity')-number) 
>         except IntegrityError: 
>             # signal to the calling code that the purchase failed - the 
>             # calling code should catch this exception and notify the 
>             # user that the purchase failed due to lack of inventory, 
>             # and tell them the updated available quantity 
>             raise InsufficientInventory() 
>
> You also want this function to handle the case where the given product 
> name doesn't exist. To help with this case, the `update` method returns 
> the number of rows updated: 
>
>     from django.db import IntegrityError 
>     from django.db.models import F 
>
>     class InsufficientInventory(Exception): 
>         pass 
>
>     def customer_buy(name, number): 
>         try: 
>             updated = ShopItem.objects.filter( 
>                 name=name).update(quantity=F('quantity')-number) 
>         except IntegrityError: 
>             raise InsufficientInventory() 
>         if not updated: 
>             # Here we reuse Django's built-in DoesNotExist exception; 
>             # you could define your own exception class instead. 
>             raise ShopItem.DoesNotExist() 
>
> With this code, you've solved the bad race conditions -- quantity will 
> never go negative, and a sale will never appear to succeed when it 
> should have failed, because of two users submitting an order 
> simultaneously. 
>
> There is still a sort of higher-level race condition that can happen 
> when two people both load the order page at the same time, and then one 
> of them orders first. This type of race condition is basically 
> impossible to avoid in a web app -- the best you can usually do is 
> simply let a user know that their order failed because someone else 
> ordered that item first while they were filling out the order form. 
>
> (In general, the term for this approach is "optimistic locking", because 
> you are "optimistically" allowing concurrent uses and hoping they don't 
> conflict, but if they do, you catch it and alert the user so they can 
> try again. The alternative is to lock everyone else out of the order 
> page while one user is ordering; this is called "pessimistic locking". 
> Usually optimistic locking is preferable, presuming the most common case 
> is "no conflict" -- e.g., enough inventory that both users can 
> successfully complete their order -- and you can handle conflicts in 
> such a way that the user whose transaction fails doesn't lose all their 
> work.) 
>
> In a real shopping-cart scenario, it's likely that you need to do more 
> than just update the quantity field on your ShopItem model -- you may 
> need to create an Order object too, and you want the Order object and 
> the ShopItem quantity decrement to both succeed or fail together; you 
> never want one of them to succeed and the other one to fail. This is the 
> scenario where you need to wrap both operations in `transaction.atomic`; 
> you can look it up in the docs to see how it's used. 
>
> Hope this helps, 
>
> Carl 
>
>

-- 
You received this message because you are subscribed to the Google Groups 
"Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-users+unsubscr...@googlegroups.com.
To post to this group, send email to django-users@googlegroups.com.
Visit this group at http://groups.google.com/group/django-users.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-users/14eccb6d-154f-45ce-8f37-f41b3b42bb6b%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to