保存外键“子级”而不先保存“父级”?

时间:2019-12-30 15:46:40

标签: python django

假设我有一个用于根据购物车内容将订单保存到数据库的视图:

def cart_checkout(request):
    order = Order()
    order.first_name = 'x'
    order.last_name = 'y'
    order.address = 'z'
    order.save()

    cart = Cart(request)
    for product_id, product_quantity in cart:
        product = Product.objects.get(pk=product_id)
        order_item = OrderItem()
        order_item.order = order
        order_item.name = product.name
        order_item.price = product.price
        order_item.amount = product_quantity
        order_item.save()
    order.update_total_price() # updates the Order total price field with the sum of order items prices
    order.save()
    return HttpResponse('Checked-out!')

如您所见,我在此视图中两次调用order.save():首先创建一个Order实例,可以在OrderItems循环中附加for,然后根据订单中的订单项更新订单总价。如果我删除了第一个.save(),则第二个错误会告诉我order需要先保存。

两次调用.save()方法对我来说似乎还不够干。有办法只做一次吗?


请注意,我没有继承ModelForm的子类,因此无法使用.save(commit=False)。另外,我不想只在save()方法中隐藏update_total_price()方法。

Models.py:

from django.db import models
from .mixins import DisplayNameMixin

class Product(DisplayNameMixin, models.Model):
    name = models.CharField(max_length=255)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    amount = models.IntegerField()

class Order(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    address = models.CharField(max_length=255)
    total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    def update_total_price(self):
        order_items = self.orderitem_set.all()
        self.total_price = sum([
            x.price * x.amount
            for x in order_items
        ])

class OrderItem(models.Model):
    order = models.ForeignKey('Order', on_delete=models.CASCADE)
    name = models.CharField(max_length=255)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    amount = models.IntegerField()

3 个答案:

答案 0 :(得分:2)

我认为,您不禁将订单保存两次,因为您需要具有order_id才能创建OrderItems,然后使用商品的金额更新订单。 我有一些建议。

  • 您可以将total_price设为计算所得的属性,这样就不必保存订单:

    class Order(models.Model):
        first_name = models.CharField(max_length=255)
        last_name = models.CharField(max_length=255)
        address = models.CharField(max_length=255)
        total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    
        @property
        def total_price(self):
            return sum([
                x.price * x.amount
                for x in self.orderitem_set.all()
            ])
    

答案 1 :(得分:1)

从数据库理论的角度来看,您的数据库结构是错误的。首先需要对其进行标准化。

为什么错了?

Order.total_price是冗余表列。可以通过聚合找到该信息。在数据库级别,没有保护措施可以防止数据库用户(在您的情况下为Django应用)输入受到破坏的数据。因此,您的数据库可以同时告知两个不同的总价格(Order.total_price != SUM(OrderItem.price * OrderItem.amount))。

因此,要安抚数据库规范化问题,您需要删除total_price字段,并在需要访问时使用Django聚合/注释(https://docs.djangoproject.com/en/3.0/topics/db/aggregation/)。


说,将total_price放在Order表中可能是完全有道理的。那个原因通常是性能。有时SQL查询的复杂性(按聚合列进行过滤非常烦人。)

但是有价格。这个价格就是您数据库的非规范化。对于您而言,您要付出违反DRY原则的责任。

只需确保您在同一事务中同时呼叫两个save()

答案 2 :(得分:1)

要扩展petraszd's answer(即删除total_price字段)和engin_ipek's answer(即添加total_price作为计算的属性),您可以尝试将{ {1}} cached property ,以避免传递相同的值,只要传递相同的total_price实例即可。

如petraszd所述,如果您使用汇总来计算总价,则可能会使计算的成本降低一些。 Orderprice中的adding the products