On 02/19/2015 01:04 AM, Mario Gudelj wrote:
Great reply. You're a champion, Carl!
Couldn't say more. Very thorough answer and interesting :)
On 19 February 2015 at 05:31, Carl Meyer <c...@oddbird.net
<mailto:c...@oddbird.net>> 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
<mailto:django-users%2bunsubscr...@googlegroups.com>.
To post to this group, send email to django-users@googlegroups.com
<mailto: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/54E4DA7F.5090902%40oddbird.net.
For more options, visit https://groups.google.com/d/optout.
--
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
<mailto:django-users+unsubscr...@googlegroups.com>.
To post to this group, send email to django-users@googlegroups.com
<mailto: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/CAHqTbjmfzZLSFc0Bmo3irt4X2594BkobVZvn_8brLXTV1YkkYQ%40mail.gmail.com
<https://groups.google.com/d/msgid/django-users/CAHqTbjmfzZLSFc0Bmo3irt4X2594BkobVZvn_8brLXTV1YkkYQ%40mail.gmail.com?utm_medium=email&utm_source=footer>.
For more options, visit https://groups.google.com/d/optout.
--
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/54E59C30.8060104%40arkade.info.
For more options, visit https://groups.google.com/d/optout.