Django F Expression Practices
- An
F()
object represents the value of a model field, transformed value of a model field, or annotated column. - It makes it possible to refer to model field values and perform database operations using them without actually having to pull them out of the database into Python memory.
- Instead, Django uses the
F()
object to generate an SQL expression that describes the required operation at the database level. - F objects or expression used to reference fields
- Helps to avoid loading objects into memory, operations are performed in Database
- Django use F expression to generate an SQL expression
- Another useful benefit of
F()
is that having the database - rather than Python - update a field’s value avoids a race condition.
models.py
x
class Product(models.Model):
name = models.CharField(max_length=200)
stock = models.PositiveIntegerField()
python manage.py shell
x
In [1]: from app_f.models import Product
In [2]: from django.db.models import F
# create 100 product
In [3]: for i in range(100):
...: p = Product.objects.create(name=f"my product {i}", stock=(i+1)*5)
In [4]: Product.objects.count()
Out[4]: 100
# we are getting data from DB and putting into the memory
# when you scale the app this can be costly
In [5]: p = Product.objects.get(pk=1) # 1. query
In [6]: p.stock
Out[6]: 5
In [7]: p.stock += 1
In [8]: p.save() # 2. query
In [9]: p.stock
Out[9]: 6
# get last product
In [34]: p = Product.objects.last()
In [35]: p.stock
Out[35]: 500
# this happens in DB side, python doesn't know about this !!!
In [36]: p.stock = F('stock') + 333
#looks like a normal Python assignment of value to an instance attribute, in fact it’s an
# SQL construct describing an operation on the database.
In [37]: p.save()
In [38]: p.stock
Out[38]: <CombinedExpression: F(stock) + Value(333)>
In [39]: p.refresh_from_db() #access the new value, the object must be reloaded from db
In [40]: p.stock
Out[40]: 833
# When Django encounters an instance of F(), it overrides the standard Python operators to
# create an encapsulated SQL expression; in this case, one which instructs the database to
# increment the database field represented by p.stock
# Whatever value is or was on p.stock, Python never gets to know about it -
# it is dealt with entirely by the database. All Python does, through Django’s F() class,
# is create the SQL syntax to refer to the field and describe the operation.
#
In [41]: p1 = Product.objects.filter(id=1)
In [42]: p1
Out[42]: <QuerySet [<Product: Product object (1)>]>
In [43]: p1.stock
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-43-f0ef1b6980ab> in <module>
----> 1 p1.stock
AttributeError: 'QuerySet' object has no attribute 'stock'
In [44]: p1[0].stock
Out[44]: 6
In [45]: p1.update(stock=F('stock')+1000)
Out[45]: 1
In [46]: p1[0].stock
Out[46]: 1006
# first 5 product names and stocks
id name stock
---------------------------
1 my product 0 1006
2 my product 1 10
3 my product 2 15
4 my product 3 20
5 my product 4 25
# update all stock values, by multiplying 2
In [47]: Product.objects.all().update(stock=F('stock')*2)
Out[47]: 100
# first 5 product names and stocks
id name stock
---------------------------
1 my product 0 2012
2 my product 1 20
3 my product 2 30
4 my product 3 40
5 my product 4 50
F()
objects assigned to model fields persist after saving the model instance and will be applied on eachsave()
. So be careful !!!
x
In [57]: p1 = Product.objects.get(id=1)
In [58]: p1.stock
Out[58]: 2014
In [59]: p1.name
Out[59]: 'my first product 0'
In [60]: p1.stock = F("stock")+1 # First incrementation
In [61]: p1.save()
In [62]: p1.stock
Out[62]: <CombinedExpression: F(stock) + Value(1)>
In [63]: p1.name = "my first product 0 updated"
In [64]: p1.save() # Second incerementation due to F object persisted model instance!!!
In [65]: p1.stock
Out[65]: <CombinedExpression: F(stock) + Value(1)>
In [66]: p1.refresh_from_db()
In [67]: p1.stock
Out[67]: 2016 # incremented 2 times
- new models and fields
xxxxxxxxxx
class Store(models.Model):
name = models.CharField(max_length=40)
class Material(models.Model):
name = models.CharField(max_length=100)
class Product(models.Model):
name = models.CharField(max_length=200)
stock = models.PositiveIntegerField()
materials = models.ManyToManyField(Material)
stores = models.ManyToManyField(Store)
shell
xxxxxxxxxx
In [1]: from app_f.models import *
In [2]: from django.db.models import *
In [3]: for i in range(100):
...: m = Material.objects.create(name=f"material{i}")
...:
In [4]: for i in range(100):
...: s = Store.objects.create(name=f"store{i}")
In [5]: p1 = Product.objects.get(pk=1)
In [10]: p1.materials.set([i for i in Material.objects.filter(id__lt=5)])
In [13]: p1.stores.set([i for i in Store.objects.filter(id__lt=3)])
In [15]: p2 = Product.objects.get(pk=2)
In [16]: p2.materials.set([i for i in Material.objects.filter(id__in=[6,7,8,9])])
In [17]: p2.stores.set([i for i in Store.objects.filter(id__in=[4,5,6,7])])
In [43]: qpm = Product.objects.annotate(num_materials=Count('materials'))
In [44]: qps = Product.objects.annotate(num_stores=Count('stores'))
In [45]: qps[0].num_stores
Out[45]: 2
In [46]: qpm[0].num_materials
Out[46]: 4
edit models.py
xxxxxxxxxx
class Material(models.Model):
name = models.CharField(max_length=100)
price = models.PositiveIntegerField(default=10) # added
shell
x
In [3]: m1 = Material.objects.first()
In [4]: m1.price
Out[4]: 10
In [5]: m1.name
Out[5]: 'material0'
In [6]: p1 = Product.objects.first()
In [7]: p1.name
Out[7]: 'my first product 0 updated'
In [8]: p1.stock
Out[8]: 2016
In [10]: qs = Product.objects.annotate(stock_price=F('stock')*F('materials__price'))
In [12]: p1.materials.count()
Out[12]: 4
In [13]: qs[0].stock_price
Out[13]: 20160
# If the fields that you’re combining are of different types you’ll need to tell Django
# what kind of field will be returned. Since F() does not directly support output_field
# you will need to wrap the expression with ExpressionWrapper:
In [16]: qs = Product.objects.annotate(
...: stock_price_decimal = ExpressionWrapper(
...: F('stock')*F('materials__price'), output_field=DecimalField()
...: )
...: )
In [17]: qs[0].stock_price_decimal
Out[17]: Decimal('20160')
In [18]: qs[1].stock_price_decimal
Out[18]: Decimal('20160')
In [19]: qs[2].stock_price_decimal
Out[19]: Decimal('20160')
In [20]: qs[3].stock_price_decimal
Out[20]: Decimal('20160')
In [21]: qs[4].stock_price_decimal
Out[21]: Decimal('200')
# Func using with F expression
In [22]: qs = Product.objects.annotate(
...: uppername=Func(F('name'), function="Upper")
...: )
In [23]: qs[0].name, qs[0].uppername
Out[23]: ('my first product 0 updated', 'MY FIRST PRODUCT 0 UPDATED')
In [24]: qs[1].name, qs[1].uppername
Out[24]: ('my product 1', 'MY PRODUCT 1')
# Sum with F
In [34]: qs = Product.objects.annotate(total=Sum(F('materials__price'))) # Sum for material prices
In [35]: qs[0].total
Out[35]: 40
In [36]: qs = Product.objects.annotate(total=Sum(F('materials'))) # Sum for ID's
In [37]: qs[0].total
Out[37]: 10
In [38]: qs[0].materials.all()
Out[38]: <QuerySet [<Material: Material object (1)>, <Material: Material object (2)>, <Material: Material object (3)>, <Material: Material object (4)>]>
- When referencing relational fields such as
ForeignKey
,F()
returns the primary key value rather than a model instance.
Yorumlar