Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit...

27
Pushing the ORM to its limits

Transcript of Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit...

Page 1: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Pushing the ORMto its limits

Page 2: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

👋 Hello, I’m SigurdDeveloper at Kolonial.no • github.com/ljodal/djangcon-eu-2019

Page 3: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Agenda

• Some tips and tricks

• Subqueries

• Constraints and indexes

• Window functions

• Customizing the ORM

Page 4: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Disclaimer:This talk is quite code heavy!

Page 5: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Useful tips and tricks

Page 6: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

CustomQuerySet

and Manager

class OrderManager(models.Manager): def create_order( self, customer, products ): ...

class OrderQuerySet(QuerySet): def undelivered(self): return self.filter(is_delivered=False)

class Order(models.Model): ... objects = OrderManager.from_queryset( OrderQuerySet )()

Order.objects.create_order(customer=...) Order.objects.undelivered()

Page 7: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Inspecting queries

>>> orders = Order.objects.all() <OrderQuerySet ...>

>>> str(orders.query) SELECT ... FROM «orders_order"

>>> print(order.explain(verbose=True)) Seq Scan on public.orders_order (cost=0.00..28.10 rows=1810 width=17) Output: id, customer_id, created_at, is_shipped

Page 8: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Avoiding extra

queries

for order in Order.objects.all(): # This triggers one query per order print(order.customer.name) # This also triggers one query per order for line in order.lines.all(): print(line)

Order.objects.select_related('customer')

Order.objects.prefetch_related('lines')

Page 9: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Avoiding race conditions

with transaction.atomic(): product = ( Product.objects .select_for_update() .get(id=1) ) product.inventory -= 1 product.save()

Page 10: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Subqueries

Page 11: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Latest order

customers = Customer.objects.annotate( latest_order_time=Subquery( Order.objects.filter( customer=OuterRef('pk'), ).order_by( '-created_at' ).values( 'created_at' )[:1] ) )

>>> customers.first().latest_order_time 1

Page 12: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

With aggregation

budgets = SalesTarget.objects.annotate( gross_total_sales=Subquery( Order.objects.filter( created_at__year=OuterRef('year'), created_at__month=OuterRef('month'), ).values_list( ExtractYear('created_at'), ExtractMonth('created_at') ).annotate( gross_total=Sum('lines__gross_amount'), ).value_lists( 'gross_total', ) ), )

>>> budgets.first().gross_total_sales 12.00

Page 13: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Aggregation without

grouping

id | week_day | lines__gross_amount 1 | 7 | 7.5 2 | 1 | 2.5 3 | 3 | 2.0

targets = SalesTarget.objects.annotate( weekend_revenue=Subquery( Order.objects.filter( created_at__week_day__in=[7, 1], ).values_list( Sum(‘lines__gross_amount'), ) ), )

>>> targest.first().weekend_revenue 7.50 # Oops, this is not what we wanted

Page 14: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Aggregation without

grouping

id | week_day | lines__gross_amount 1 | 7 | 7.5 2 | 1 | 2.5 3 | 3 | 2.0

targets = SalesTargets.objects.annotate( weekend_revenue=Subquery( Order.objects.filter( created_at__week_day__in=[7, 1], ).values_list( Func( 'lines__gross_amount', function='SUM', ), ) ), )

>>> targets.first().weekend_revenue 10.00

Page 15: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Custom constraints and indexes

Page 16: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Unique constraints

class SalesTarget(Model): year = models.IntegerField() month = models.IntegerField() target = models.DecimalField(...)

class Meta:

unique_together = [ ('year', 'month'), ]

Page 17: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Partial unique

constraint

class Order(Model): ...

class Meta:

constraints = [ UniqueConstraint( name='limit_pending_orders', fields=['customer', 'is_shipped'], condition=Q(is_shipped=False), ) ]

Page 18: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Check constraint

class MonthlyBudget(Model): ...

class Meta:

constraints = [ CheckConstraint( check=Q(month__in=range(1, 13)), name='check_valid_month', ) ]

Page 19: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Partial index

class Order(Model): ...

class Meta:

indexes = [ Index( name='unshipped_orders', fields=['pk', ], condition=Q(is_shipped=False), ) ]

Page 20: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Window functions

Page 21: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Previous order from

same customer

orders = Order.objects.annotate( prev_order_id=Window( expression=Lag('order_id', 1), partition_by=[F('customer_id')], order_by=F('created_at').asc(), ), )

>>> orders.first().prev_order_id 1

Page 22: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Extending with custom functionality

Page 23: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Custom functions

class Round(Func): function = 'ROUND'

Page 24: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

A more complex custom function

class AsDateTime(Func):

arity = 2 output_field = DateTimeField()

def as_postgresql( self, compiler, connection, **extra_context ):

extra_context['tz'] = settings.TIME_ZONE template = ( "(%(expressions)s || '%(tz)s')::timestamptz" )

return self.as_sql( compiler, connection, arg_joiner='+', template=template, **extra_context )

qs.annotate( datetime=AsDateTime('date_field', 'time_field') )

Page 25: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Writing custom SQL

Order.objects.annotate( age=RawSQL('age(created_at)'), )

Order.objects.extra( select={ 'age': 'age(created_at)', }, )

Order.objects.raw( ''' SELECT *, age(created_at) as age FROM orders_order ''' )

with connection.cursor() as cursor: cursor.execute('SELECT 2') cursor.fetchone()

Page 26: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

There’s so much moreTake a look at the Django documentation

Page 27: Pushing the Django ORM to its limit - DjangoCon Europe 2019 · Pushing the Django ORM to its limit Created Date: 4/12/2019 11:46:15 AM ...

Thanks!@sigurdlj • github.com/ljodal/djangocon-eu-2019

medium.com/kolonial-no-product-tech