Supposons que j'ai une vue pour enregistrer une commande dans une base de données basée sur le contenu du panier:
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()
Comme vous pouvez le voir, j'appelle order.save () code > deux fois dans cette vue: d'abord pour créer une instance de Order à laquelle les OrderItems peuvent être attachés dans la boucle for , puis pour mettre à jour le total prix de la commande en fonction des articles de la commande qu'elle contient. Si je supprimais le premier .save () , j'obtiendrais une erreur sur le second m'indiquant que la commande doit être enregistrée en premier.
Appeler la méthode .save () deux fois ne me semble pas assez DRY. Existe-t-il un moyen de le faire une seule fois?
Notez que je ne sous-classe pas ModelForm , donc je ne peux pas utiliser .save (commit = False) . De plus, je ne veux pas simplement masquer la méthode save () dans la méthode update_total_price () .
Models.py:
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!')
3 Réponses :
Du point de vue de la théorie DB, votre structure DB est fausse. Il doit d'abord être normalisé.
Pourquoi est-ce faux?
Order.total_price est une colonne de table redondante. Ces informations peuvent être trouvées avec agrégation. Au niveau de la base de données, aucune protection n'empêche les utilisateurs de la base de données (l'application Django dans votre cas) d'entrer des données compromises. Ainsi, votre base de données peut indiquer deux prix totaux différents ( Order.total_price! = SUM (OrderItem.price * OrderItem.amount) ) en même temps.
Donc, pour apaiser les dieux de la normalisation de la base de données, vous devez supprimer le champ total_price et utiliser les agrégations / annotations Django ( https://docs.djangoproject.com/en/3.0/topics/db/aggregation/ ) lorsque vous devez y accéder.
En disant cela, il pourrait y avoir une raison tout à fait valable de mettre total_price dans la table Order . Cette raison est généralement la performance. Parfois, complexité des requêtes SQL (il est très ennuyeux de filtrer par une colonne agrégée).
Mais il y a un prix. Et ce prix est la dénormalisation de votre base de données. Dans votre cas, vous payez en cassant le principe DRY.
Assurez-vous simplement que vous appelez les deux save () dans la même transaction.
Je pense que vous ne pouvez pas vous empêcher de sauvegarder la commande deux fois, car vous devez avoir un order_id pour créer les OrderItems, puis mettre à jour la commande avec le montant des articles. J'ai cependant quelques suggestions à faire.
Vous pouvez faire de total_price une propriété calculée, afin de ne pas avoir à enregistrer la commande:
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()
])
Pour développer la réponse de petraszd (c'est-à-dire supprimer le champ total_price ) et réponse d'engin_ipek (c'est-à-dire ajoutez total_price en tant que propriété calculée), vous pouvez essayer de créer total_price a propriété en cache a > , pour éviter de calculer la même valeur plus d'une fois - tant que la même instance de Order est transmise.
Vous rendriez également probablement le calcul un peu moins coûteux si vous utilisiez l'agrégation pour calculer le prix total, comme Petraszd l'a mentionné, par exemple. ajout des produits de prix et montant .