Skip to content

Commit 537c5d9

Browse files
Fix home repo n+1 issue (#1630)
* Replace update_or_create loop with bulk_update to fix N+1 Refreshing stale repo metadata fired a SELECT + UPDATE per repo (12 queries for 6 repos). Now fetches all existing records in one query and bulk_updates them, keeping the total at 2-3 queries regardless of repo count. * Add RepoAPIData TypedDict to tighten _get_api_data return type Replaces the loose dict[str, dict[str, str]] annotation so Pylance can narrow forks_count/stargazers_count to int without casts. * Pleasure style dictator
1 parent 23e888c commit 537c5d9

1 file changed

Lines changed: 36 additions & 15 deletions

File tree

‎pydis_site/apps/home/views.py‎

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from json.decoder import JSONDecodeError
3+
from typing import TypedDict
34

45
import httpx
56
from django.core.handlers.wsgi import WSGIRequest
@@ -14,6 +15,20 @@
1415
log = logging.getLogger(__name__)
1516

1617

18+
class RepoAPIData(TypedDict):
19+
"""
20+
Partial typing for the GitHub API response for a repository.
21+
22+
Only the fields we care about are included here.
23+
"""
24+
25+
full_name: str
26+
description: str
27+
language: str
28+
forks_count: int
29+
stargazers_count: int
30+
31+
1732
class HomeView(View):
1833
"""The main landing page for the website."""
1934

@@ -45,7 +60,7 @@ def __init__(self):
4560
else:
4661
self.headers = {}
4762

48-
def _get_api_data(self) -> dict[str, dict[str, str]]:
63+
def _get_api_data(self) -> dict[str, RepoAPIData]:
4964
"""
5065
Call the GitHub API and get information about our repos.
5166
@@ -139,26 +154,32 @@ def _get_repo_data(self) -> list[RepositoryMetadata]:
139154
api_repositories = self._get_api_data()
140155

141156
if not api_repositories:
142-
return RepositoryMetadata.objects.all()
157+
return list(RepositoryMetadata.objects.all())
143158

144-
# Update or create all RepoData objects in self.repos
159+
# Build updated objects and bulk_update in two queries (fetch + update)
160+
existing = {r.repo_name: r for r in RepositoryMetadata.objects.all()}
145161
database_repositories = []
162+
new_repos = []
146163
for api_data in api_repositories.values():
147-
repo_data, _created = RepositoryMetadata.objects.update_or_create(
148-
repo_name=api_data["full_name"],
149-
defaults={
150-
'repo_name': api_data["full_name"],
151-
'description': api_data["description"],
152-
'forks': api_data["forks_count"],
153-
'stargazers': api_data["stargazers_count"],
154-
'language': api_data["language"],
155-
}
156-
)
157-
database_repositories.append(repo_data)
164+
repo = existing.get(api_data["full_name"])
165+
if repo is None:
166+
repo = RepositoryMetadata(repo_name=api_data["full_name"])
167+
new_repos.append(repo)
168+
repo.description = api_data["description"]
169+
repo.forks = api_data["forks_count"]
170+
repo.stargazers = api_data["stargazers_count"]
171+
repo.language = api_data["language"]
172+
database_repositories.append(repo)
173+
RepositoryMetadata.objects.bulk_update(
174+
[r for r in database_repositories if r not in new_repos],
175+
fields=["description", "forks", "stargazers", "language"],
176+
)
177+
if new_repos:
178+
RepositoryMetadata.objects.bulk_create(new_repos)
158179
return database_repositories
159180

160181
# Otherwise, if the data is fresher than 2 minutes old, we should just return it.
161-
return RepositoryMetadata.objects.all()
182+
return list(RepositoryMetadata.objects.all())
162183

163184
def get(self, request: WSGIRequest) -> HttpResponse:
164185
"""Collect repo data and render the homepage view."""

0 commit comments

Comments
 (0)