我有 2 个模型:Product
和Order
.
Product
有一个用于股票的整数字段,而Order
有一个状态和一个外键Product
:
class Product(models.Model):
name = models.CharField(max_length=30)
stock = models.PositiveSmallIntegerField(default=1)
class Order(models.Model):
product = models.ForeignKey('Product')
DRAFT = 'DR'; INPROGRESS = 'PR'; ABORTED = 'AB'
STATUS = ((INPROGRESS, 'In progress'),(ABORTED, 'Aborted'),)
status = models.CharField(max_length = 2, choices = STATUS, default = DRAFT)
我的目标是让每个新订单的产品库存减少 1 个,并为每个订单取消增加 1 个。为此,我重载了模型的save
方法Order
(受Django 启发:保存时,如何检查字段是否已更改?):
from django.db.models import F
class Order(models.Model):
product = models.ForeignKey('Product')
status = models.CharField(max_length = 2, choices = STATUS, default = DRAFT)
EXISTING_STATUS = set([INPROGRESS])
__original_status = None
def __init__(self, *args, **kwargs):
super(Order, self).__init__(*args, **kwargs)
self.__original_status = self.status
def save(self, *args, **kwargs):
old_status = self.__original_status
new_status = self.status
has_changed_status = old_status != new_status
if has_changed_status:
product = self.product
if not old_status in Order.EXISTING_STATUS and new_status in Order.EXISTING_STATUS:
product.stock = F('stock') - 1
product.save(update_fields=['stock'])
elif old_status in Order.EXISTING_STATUS and not new_status in Order.EXISTING_STATUS:
product.stock = F('stock') + 1
product.save(update_fields=['stock'])
super(Order, self).save(*args, **kwargs)
self.__original_status = self.status
使用 RestFramework,我创建了 2 个视图,一个用于创建新订单,一个用于取消现有订单。两者都使用简单的序列化程序:
class OrderSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = (
'id',
'product',
'status',
)
read_only_fields = (
'status',
)
class OrderList(generics.ListCreateAPIView):
model = Order
serializer_class = OrderSimpleSerializer
def pre_save(self, obj):
super(OrderList,self).pre_save(obj)
product = obj.product
if not product.stock > 0:
raise ConflictWithAnotherRequest("Product is not available anymore.")
obj.status = Order.INPROGRESS
class OrderAbort(generics.RetrieveUpdateAPIView):
model = Order
serializer_class = OrderSimpleSerializer
def pre_save(self, obj):
obj.status = Order.ABORTED
以下是访问这两个视图的方法:
from myapp.views import *
urlpatterns = patterns('',
url(r'^order/$', OrderList.as_view(), name='order-list'),
url(r'^order/(?P<pk>[0-9]+)/abort/$', OrderAbort.as_view(), name='order-abort'),
)
我正在使用 Django 1.6b4、Python 3.3、Rest Framework 2.7.3 和 PostgreSQL 9.2。
我的问题是并发请求可以增加产品的库存高于原始库存!
这是我用来演示的脚本:
import sys
import urllib.request
import urllib.parse
import json
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor)
def create_order():
url = 'http://127.0.0.1:8000/order/'
values = {'product':1}
data = urllib.parse.urlencode(values).encode('utf-8')
request = urllib.request.Request(url, data)
response = opener.open(request)
return response
def cancel_order(order_id):
abort_url = 'http://127.0.0.1:8000/order/{}/abort/'.format(order_id)
values = {'product':1,'_method':'PUT'}
data = urllib.parse.urlencode(values).encode('utf-8')
request = urllib.request.Request(abort_url, data)
try:
response = opener.open(request)
except Exception as e:
if (e.code != 403):
print(e)
else:
print(response.getcode())
def main():
response = create_order()
print(response.getcode())
data = response.read().decode('utf-8')
order_id = json.loads(data)['id']
time.sleep(1)
for i in range(2):
p = Process(target=cancel_order, args=[order_id])
p.start()
if __name__ == '__main__':
main()
对于库存为 1 的产品,此脚本提供以下输出:
201 # means it creates an order for Product, thus decreasing stock from 1 to 0
200 # means it cancels the order for Product, thus increasing stock from 0 to 1
200 # means it cancels the order for Product, thus increasing stock from 1 to 2 (shouldn't happen)
编辑
我添加了一个示例项目来重现该错误: https ://github.com/ThinkerR/django-concurrency-demo