Skip to content

Commit

Permalink
Fix N+1 query problem for dandiset star info
Browse files Browse the repository at this point in the history
To fix this behavior for the dandiset list endpoint, create a new method
on the DandisetViewSet class, `_get_dandiset_star_context`, which
collects dandiset star information. This information is then passed to
the DandisetListSerializer as context.
  • Loading branch information
jjnesbitt committed Dec 30, 2024
1 parent 988888d commit 80d6c0d
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 2 deletions.
47 changes: 45 additions & 2 deletions dandiapi/api/views/dandiset.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from dandiapi.api.asset_paths import get_root_paths_many
from dandiapi.api.mail import send_ownership_change_emails
from dandiapi.api.models import Dandiset, Version
from dandiapi.api.models.dandiset import DandisetStar
from dandiapi.api.services import audit
from dandiapi.api.services.dandiset import (
create_dandiset,
Expand Down Expand Up @@ -196,6 +197,36 @@ def get_object(self):

return dandiset

def _get_dandiset_star_context(self, dandisets):
# Default value for all relevant dandisets
dandisets_to_stars = {
d.id: {'total': 0, 'starred_by_current_user': False} for d in dandisets
}

# Group the stars for these dandisets by the dandiset ID,
# yielding pairs of (Dandiset ID, Star Count)
dandiset_stars = (
DandisetStar.objects.filter(dandiset__in=dandisets)
.values_list('dandiset')
.annotate(star_count=Count('id'))
.order_by()
)
for dandiset_id, star_count in dandiset_stars:
dandisets_to_stars[dandiset_id]['total'] = star_count

# Only annotate dandisets as starred by current user if user is logged in
user = self.request.user
if user.is_anonymous:
return dandisets_to_stars
user = typing.cast(User, user)

# Filter previous query to current user stars
user_starred_dandisets = dandiset_stars.filter(user=user)
for dandiset_id, _ in user_starred_dandisets:
dandisets_to_stars[dandiset_id]['starred_by_current_user'] = True

return dandisets_to_stars

@staticmethod
def _get_dandiset_to_version_map(dandisets):
"""Map Dandiset IDs to that dandiset's draft and most recently published version."""
Expand Down Expand Up @@ -299,8 +330,14 @@ def list(self, request, *args, **kwargs):
qs = self.get_queryset()
dandisets = self.paginate_queryset(self.filter_queryset(qs))
dandisets_to_versions = self._get_dandiset_to_version_map(dandisets)
dandiset_stars = self._get_dandiset_star_context(dandisets)
serializer = DandisetListSerializer(
dandisets, many=True, context={'dandisets': dandisets_to_versions}
dandisets,
many=True,
context={
'dandisets': dandisets_to_versions,
'stars': dandiset_stars,
},
)
return self.get_paginated_response(serializer.data)

Expand Down Expand Up @@ -565,7 +602,13 @@ def starred(self, request) -> Response:
dandisets = Dandiset.objects.filter(stars__user=request.user).order_by('-stars__created')
dandisets = self.paginate_queryset(dandisets)
dandisets_to_versions = self._get_dandiset_to_version_map(dandisets)
dandiset_stars = self._get_dandiset_star_context(dandisets)
serializer = DandisetListSerializer(
dandisets, many=True, context={'dandisets': dandisets_to_versions}
dandisets,
many=True,
context={
'dandisets': dandisets_to_versions,
'stars': dandiset_stars,
},
)
return self.get_paginated_response(serializer.data)
6 changes: 6 additions & 0 deletions dandiapi/api/views/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ def get_contact_person(self, dandiset):

return contact

def get_star_count(self, dandiset):
return self.context['stars'][dandiset.id]['total']

def get_is_starred(self, dandiset):
return self.context['stars'][dandiset.id]['starred_by_current_user']

most_recent_published_version = serializers.SerializerMethodField()
draft_version = serializers.SerializerMethodField()

Expand Down

0 comments on commit 80d6c0d

Please sign in to comment.