Upgrade to Pro — share decks privately, control downloads, hide ads and more …

But Why is the (Django) Admin Slow? - PyCon 2019

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

But Why is the (Django) Admin Slow? - PyCon 2019

Slides from my PyCon 2019 talk discussing common Django Admin performance hiccups and ways to debug them using the Django Debug Toolbar.

Video for this presentation can be found here: https://www.youtube.com/watch?v=f8cFjiyxQuQ

Avatar for Jacinda Shelly

Jacinda Shelly

May 04, 2019
Tweet

More Decks by Jacinda Shelly

Other Decks in Programming

Transcript

  1. ABOUT ME • CTO @ Doctor On Demand • Long-time

    Python and Django user • Love exercise, reading, and family time
  2. class LibraryUser(AbstractUser): ''' Custom user for our library project. Inherits

    from AbstractUser, which has the following fields: - username - first_name - last_name - email - is_staff - is_active - date_joined ''' birthdate = models.DateField( 'Date of Birth', null=True, blank=False ) gender = models.CharField( "Gender", max_length=1, choices=GENDER_CHOICES, blank=False ) books = models.ManyToManyField( 'stacks.Book', through=‘stacks.LoanedBook' ) def __str__(self): return self.get_full_name()
  3. class Author(models.Model): """ Model representing the author of a work.

    """ last_name = models.CharField(max_length=32) first_name = models.CharField(max_length=32) def __str__(self): return self.full_name() def full_name(self): return ", “.join( [self.last_name, self.first_name] )
  4. class Book(AbstractItem): title = models.CharField(max_length=4000) page_count = models.PositiveIntegerField() daily_fine =

    models.DecimalField( max_digits=4, decimal_places=2, default="0.25", help_text=_( "Amount the patron will be fined per day" " for an overdue library book." ), ) def __str__(self): return self.title
  5. class LoanedBook(models.Model): patron = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE ) book =

    models.ForeignKey(Book, on_delete=models.CASCADE) checkout_date = models.DateField() due_date = models.DateField() return_date = models.DateField(null=True, blank=True) times_renewed = models.IntegerField(default=0) fine_paid = models.NullBooleanField() def __str__(self): return ": ".join([str(self.patron), str(self.book)])
  6. https://django-debug-toolbar.readthedocs.io/ INSTALLED_APPS = [ # … 'debug_toolbar' ] INTERNAL_IPS =['127.0.0.1',]

    MIDDLEWARE_CLASSES = [ # … 'debug_toolbar.middleware.DebugToolbarMiddleware' ]
  7. First, we get all the loaned book entries Then, we

    get related objects ONE. BY. ONE.
  8. def __str__(self): return ": ".join([str(self.patron), str(self.book)]) Problem: Solution 1: @admin.register(LoanedBook)

    class LoanedBookAdmin(admin.ModelAdmin): list_display = ('patron', 'book') Solution 2a: @admin.register(LoanedBook) class LoanedBookAdmin(admin.ModelAdmin): list_select_related = True Solution 2b: @admin.register(LoanedBook) class LoanedBookAdmin(admin.ModelAdmin): list_select_related = ('patron', 'book')
  9. @admin.register(LoanedBook) class LoanedBookAdmin(admin.ModelAdmin): list_select_related = ('patron', 'book') def get_queryset(self, request):

    qs = super(LoanedBookAdmin, self).get_queryset(request) qs = qs.only("patron__first_name", "patron__last_name", "book__daily_fine") return qs Well, let’s see…
  10. @admin.register(LoanedBook) class LoanedBookAdmin(admin.ModelAdmin): list_select_related = ('patron', 'book') def get_queryset(self, request):

    qs = super(LoanedBookAdmin, self).get_queryset(request) qs = qs.only("patron__first_name", "patron__last_name", "book__daily_fine") return qs
  11. @admin.register(LoanedBook) class LoanedBookAdmin(admin.ModelAdmin): list_select_related = ('patron', 'book') def get_queryset(self, request):

    qs = super(LoanedBookAdmin, self).get_queryset(request) qs = qs.only("patron__first_name", "patron__last_name", "book__title") return qs Fixed!
  12. @admin.register(LoanedBook) class LoanedBookAdmin(admin.ModelAdmin): list_select_related = ('patron', 'book') def get_queryset(self, request):

    qs = super(LoanedBookAdmin, self).get_queryset(request) qs = qs.only("patron__first_name", "patron__last_name", "book__title") return qs
  13. Now, we want to try something else # models.py class

    Book(AbstractItem): authors = models.ManyToManyField(Author, related_name=“works") def authors_display(self): return "; ".join([str(author) for author in self.authors.all()]) # admin.py @admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display = ("__str__", "authors_display") list_select_related = True
  14. Solution @admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display = ("__str__", "authors_display") def get_queryset(self,

    request): qs = super(BookAdmin, self).get_queryset(request) qs = qs.prefetch_related("authors") return qs
  15. Next Up - Counting! # models.py class Author(models.Model): # ...

    def book_count(self): return self.works.count() # admin.py @admin.register(Author) class AuthorAdmin(admin.ModelAdmin): fields = ("first_name", "last_name") list_display = ("__str__", "book_count")
  16. Custom Querysets to the Rescue (again)! from django.db.models import Count

    @admin.register(Author) class AuthorAdmin(admin.ModelAdmin): fields = ("first_name", "last_name") list_display = ("__str__", "book_count") def get_queryset(self, request): qs = super(AuthorAdmin, self).get_queryset(request) qs = qs.annotate( book_count=Count("works") ) return qs