Browse Source

perf(hadis): optimize sync api, eliminate N+1 queries, add redis caching and gzip

- Eliminate N+1 queries in hadis serializers by using in-memory prefetch cache instead of .first() and .order_by()

- Add select_related for hadis and category in HadisSyncView prefetches

- Apply cached_view (Redis 2h + Accept-Language) to HadisSyncView and public hadis endpoints

- Enable GZipMiddleware in Django settings to compress large API responses
master
Mohsen Taba 2 weeks ago
parent
commit
d1094a4725
  1. 25
      apps/hadis/serializers/hadis.py
  2. 86
      apps/hadis/urls.py
  3. 4
      apps/hadis/views/hadis.py
  4. 1
      config/settings/base.py

25
apps/hadis/serializers/hadis.py

@ -303,7 +303,8 @@ class HadisListSerializer(serializers.ModelSerializer):
formatted = format_address_output(obj.address)
if formatted:
return formatted
first_ref = obj.references.first()
refs = list(obj.references.all())
first_ref = refs[0] if refs else None
if first_ref and hasattr(first_ref, 'address') and first_ref.address:
return format_address_output(first_ref.address)
return []
@ -315,7 +316,7 @@ class HadisListSerializer(serializers.ModelSerializer):
request = self.context.get('request')
images_list = []
for ref in obj.references.all():
for img in ref.images.all().order_by('priority'):
for img in sorted(ref.images.all(), key=lambda x: getattr(x, 'priority', 0)):
url = None
if img.thumbnail:
url = absolute_https_url(img.thumbnail.url, request) if request else absolute_https_url(img.thumbnail.url)
@ -597,7 +598,8 @@ class TransmitterOriginalTextSerializer(serializers.ModelSerializer):
fields = ['id', 'title', 'text', 'translation', 'share_link', 'slug', 'address', 'images']
def get_address(self, obj):
return format_address_output(obj.references.first())
refs = list(obj.references.all())
return format_address_output(refs[0] if refs else None)
def get_images(self, obj):
request = self.context.get('request')
@ -1175,7 +1177,8 @@ class HadisCorrectionSerializer(serializers.ModelSerializer):
return book_mark.get('is_bookmarked', False)
def get_address(self, obj):
return format_address_output(obj.references.first())
refs = list(obj.references.all())
return format_address_output(refs[0] if refs else None)
def get_images(self, obj):
request = self.context.get('request')
@ -1468,7 +1471,8 @@ class HadisCorrectionDetailSerializer(serializers.ModelSerializer):
return {"id": obj.hadis.id, "number": obj.hadis.number, "slug": obj.hadis.slug}
def get_address(self, obj):
return format_address_output(obj.references.first())
refs = list(obj.references.all())
return format_address_output(refs[0] if refs else None)
def get_images(self, obj):
request = self.context.get('request')
@ -1505,7 +1509,8 @@ class HadisInterpretationDetailSerializer(serializers.ModelSerializer):
}
def get_address(self, obj):
return format_address_output(obj.references.first())
refs = list(obj.references.all())
return format_address_output(refs[0] if refs else None)
def get_images(self, obj):
request = self.context.get('request')
@ -1543,7 +1548,8 @@ class TransmitterOriginalTextDetailSerializer(serializers.ModelSerializer):
}
def get_address(self, obj):
return format_address_output(obj.references.first())
refs = list(obj.references.all())
return format_address_output(refs[0] if refs else None)
def get_images(self, obj):
request = self.context.get('request')
@ -1590,7 +1596,8 @@ class HadisSourceDetailsSerializer(serializers.ModelSerializer):
if formatted:
return formatted
# 2. Fallback: references address
first_ref = obj.references.first()
refs = list(obj.references.all())
first_ref = refs[0] if refs else None
if first_ref and hasattr(first_ref, 'address') and first_ref.address:
return format_address_output(first_ref.address)
return []
@ -1602,7 +1609,7 @@ class HadisSourceDetailsSerializer(serializers.ModelSerializer):
request = self.context.get('request')
images_list = []
for ref in obj.references.all():
for img in ref.images.all().order_by('priority'):
for img in sorted(ref.images.all(), key=lambda x: getattr(x, 'priority', 0)):
url = None
if img.thumbnail:
url = absolute_https_url(img.thumbnail.url, request) if request else absolute_https_url(img.thumbnail.url)

86
apps/hadis/urls.py

@ -70,80 +70,80 @@ for prefix, viewset, basename in admin_router.registry:
urlpatterns = [
path('sync/authors/', BookAuthorSyncView.as_view(), name='sync-author-list'),
path('authors/<str:author_slug>/references/', AuthorReferencesListView.as_view(), name='author-references'),
path('authors/<str:author_slug>/', BookAuthorDetailView.as_view(), name='author-detail'),
path('authors/', BookAuthorListView.as_view(), name='author-list'),
path('sync/authors/', cached_view(BookAuthorSyncView.as_view()), name='sync-author-list'),
path('authors/<str:author_slug>/references/', cached_view(AuthorReferencesListView.as_view()), name='author-references'),
path('authors/<str:author_slug>/', cached_view(BookAuthorDetailView.as_view()), name='author-detail'),
path('authors/', cached_view(BookAuthorListView.as_view()), name='author-list'),
# Admin endpoints
path('admin/', include(admin_router.urls)),
# Most specific first (with parameters)
path('pinned-collections/', PinnedHadisCollectionListView.as_view(), name='pinned-hadis-collection-list'),
path('collections/', HadisCollectionListView.as_view(), name='hadis-collection-list'),
path('sync/sects/', HadisCategorySectListView.as_view(), name='hadis-sect-list'),
path('sync/categories/tree/', HadisCategoryTreeView.as_view(), name='hadis-category-tree'),
path('sync/hadis/', HadisSyncView.as_view(), name='hadis-sync'),
path('sync/narrators/', TransmitterSyncView.as_view(), name='transmitter-sync'),
path('sync/references/', BookReferenceSyncView.as_view(), name='reference-sync'),
path('v2/sync/references/', BookReferenceV2SyncView.as_view(), name='reference-sync-v2'),
path('pinned-collections/', cached_view(PinnedHadisCollectionListView.as_view()), name='pinned-hadis-collection-list'),
path('collections/', cached_view(HadisCollectionListView.as_view()), name='hadis-collection-list'),
path('sync/sects/', cached_view(HadisCategorySectListView.as_view()), name='hadis-sect-list'),
path('sync/categories/tree/', cached_view(HadisCategoryTreeView.as_view()), name='hadis-category-tree'),
path('sync/hadis/', cached_view(HadisSyncView.as_view()), name='hadis-sync'),
path('sync/narrators/', cached_view(TransmitterSyncView.as_view()), name='transmitter-sync'),
path('sync/references/', cached_view(BookReferenceSyncView.as_view()), name='reference-sync'),
path('v2/sync/references/', cached_view(BookReferenceV2SyncView.as_view()), name='reference-sync-v2'),
path('sync/version/', ContentReleaseSyncView.as_view(), name='content-release-sync'),
path('sync/filters/', HadisFiltersSyncAPIView.as_view(), name='hadis-filters-sync'),
path('info/', HadisInfoView.as_view(), name='hadis-info'),
path('sync/filters/', cached_view(HadisFiltersSyncAPIView.as_view()), name='hadis-filters-sync'),
path('info/', cached_view(HadisInfoView.as_view()), name='hadis-info'),
# Category paths (more specific first)
path('categories/tree/', HadisCategoryTreeNormalView.as_view(), name='hadis-category-tree-normal'),
path('categories/<str:sect_type>/<str:slug>/<str:source_type>/', HadisCategorySelectBySectSourceView.as_view(), name='categories-tree-by-sect-source'),
path('categories/<str:sect_type>/<str:slug>/', HadisCategorySelectBySectView.as_view(), name='categories-tree-by-sect'),
path('categories/tree/', cached_view(HadisCategoryTreeNormalView.as_view()), name='hadis-category-tree-normal'),
path('categories/<str:sect_type>/<str:slug>/<str:source_type>/', cached_view(HadisCategorySelectBySectSourceView.as_view()), name='categories-tree-by-sect-source'),
path('categories/<str:sect_type>/<str:slug>/', cached_view(HadisCategorySelectBySectView.as_view()), name='categories-tree-by-sect'),
path('categories/<str:sect_type>/', CategoriesBySectView.as_view(), name='categories-by-sect'),
path('categories/', CategoriesView.as_view(), name='categories'), # ← Least specific LAST
path('categories/', cached_view(CategoriesView.as_view()), name='categories'), # ← Least specific LAST
# Hadis paths
path('category/<str:category_slug>/xmind/', HadisCategoryXMindView.as_view(), name='hadis-category-xmind'), # ← Must be before other category paths
path('category/<str:category_slug>/xmind/', cached_view(HadisCategoryXMindView.as_view()), name='hadis-category-xmind'), # ← Must be before other category paths
path('category/<str:category_slug>/corrections/', CategoryHadisCorrectionsView.as_view(), name='category-hadis-corrections'),
path('category/<str:category_slug>/', HadisListView.as_view(), name='hadis-list'),
path('arguments/filters/', HadisFiltersView.as_view(), name='hadis-filters'),
path('arguments/filters/', cached_view(HadisFiltersView.as_view()), name='hadis-filters'),
path('arguments/', HadisMainListView.as_view(), name='hadis-main-list'),
# Narrator paths
path('narrators/filters/', TransmitterFiltersView.as_view(), name='narrator-filters'),
path('narrators/<str:narrator_slug>/teachers/', NarratorTeachersView.as_view(), name='narrator-teachers'),
path('narrators/<str:narrator_slug>/students/', NarratorStudentsView.as_view(), name='narrator-students'),
path('narrators/<str:narrator_slug>/arguments/', NarratorArgumentsListView.as_view(), name='narrator-arguments'),
path('narrators/<str:narrator_slug>/opinions/', TransmitterOpinionView.as_view(), name='narrator-opinions'),
path('narrators/<str:narrator_slug>/original_texts/', TransmitterOriginalTextView.as_view(), name='narrator-original-texts'),
path('narrators/<str:narrator_slug>/', TransmitterDetailView.as_view(), name='narrator-detail'),
path('narrators/', TransmitterView.as_view(), name='narrators'),
path('narrators/filters/', cached_view(TransmitterFiltersView.as_view()), name='narrator-filters'),
path('narrators/<str:narrator_slug>/teachers/', cached_view(NarratorTeachersView.as_view()), name='narrator-teachers'),
path('narrators/<str:narrator_slug>/students/', cached_view(NarratorStudentsView.as_view()), name='narrator-students'),
path('narrators/<str:narrator_slug>/arguments/', cached_view(NarratorArgumentsListView.as_view()), name='narrator-arguments'),
path('narrators/<str:narrator_slug>/opinions/', cached_view(TransmitterOpinionView.as_view()), name='narrator-opinions'),
path('narrators/<str:narrator_slug>/original_texts/', cached_view(TransmitterOriginalTextView.as_view()), name='narrator-original-texts'),
path('narrators/<str:narrator_slug>/', cached_view(TransmitterDetailView.as_view()), name='narrator-detail'),
path('narrators/', cached_view(TransmitterView.as_view()), name='narrators'),
# Reference paths
path('v2/references/<str:reference_slug>/excerpts/', ReferenceExcerptsListView.as_view(), name='reference-excerpts-v2'),
path('v2/references/<str:slug>/', BookReferenceV2DetailView.as_view(), name='reference-detail-v2'),
path('v2/references/', BookReferenceV2ListView.as_view(), name='reference-list-v2'),
path('references/<str:reference_slug>/excerpts/', ReferenceExcerptsListView.as_view(), name='reference-excerpts'),
path('references/<str:reference_slug>/', BookDetailView.as_view(), name='reference-detail'),
path('references/<str:reference_slug>', BookDetailView.as_view(), name='reference-detail-no-slash'),
path('references/', BookReferencesView.as_view(), name='references'),
path('v2/references/<str:reference_slug>/excerpts/', cached_view(ReferenceExcerptsListView.as_view()), name='reference-excerpts-v2'),
path('v2/references/<str:slug>/', cached_view(BookReferenceV2DetailView.as_view()), name='reference-detail-v2'),
path('v2/references/', cached_view(BookReferenceV2ListView.as_view()), name='reference-list-v2'),
path('references/<str:reference_slug>/excerpts/', cached_view(ReferenceExcerptsListView.as_view()), name='reference-excerpts'),
path('references/<str:reference_slug>/', cached_view(BookDetailView.as_view()), name='reference-detail'),
path('references/<str:reference_slug>', cached_view(BookDetailView.as_view()), name='reference-detail-no-slash'),
path('references/', cached_view(BookReferencesView.as_view()), name='references'),
# Hadis detail paths (with slug, more specific)
path('<str:hadis_slug>/source-details/', HadisSourceDetailsView.as_view(), name='hadis-source-details'),
path('<str:hadis_slug>/sources/', HadisSourceDetailsView.as_view(), name='hadis-sources'),
path('<str:hadis_slug>/source-details/', cached_view(HadisSourceDetailsView.as_view()), name='hadis-source-details'),
path('<str:hadis_slug>/sources/', cached_view(HadisSourceDetailsView.as_view()), name='hadis-sources'),
path('<str:hadis_slug>/detail/', HadisDetailView.as_view(), name='hadis-detail'),
path('<str:hadis_slug>/transmitters/', HadisTransmittersView.as_view(), name='hadis-transmitters'),
path('<str:hadis_slug>/transmitters/layers/', HadisLayersView.as_view(), name='hadis-layers'),
path('<str:hadis_slug>/transmitters/', cached_view(HadisTransmittersView.as_view()), name='hadis-transmitters'),
path('<str:hadis_slug>/transmitters/layers/', cached_view(HadisLayersView.as_view()), name='hadis-layers'),
path('<str:hadis_slug>/corrections/', HadisCorrectionsView.as_view(), name='hadis-corrections'),
path('<str:hadis_slug>/interprets/', HadisInterpretsListView.as_view(), name='hadis-interprets'),
path('<str:hadis_slug>/interprets/', cached_view(HadisInterpretsListView.as_view()), name='hadis-interprets'),
path('<str:hadis_slug>/', HadisBasicView.as_view(), name='hadis-basic'), # ← Least specific LAST
# path('test/test-deploy',test_deploy , name='test'),
# path('debug-headers',debug_headers , name='headers'),
# Interpretation Detail Endpoint (تفسیر بر اساس آی‌دی)
path('interpretations/<str:interpretation_slug>/', HadisInterpretationDetailView.as_view(), name='interpretation-detail'),
path('interpretations/<str:interpretation_slug>/', cached_view(HadisInterpretationDetailView.as_view()), name='interpretation-detail'),
# Correction Detail Endpoint (تصحیح بر اساس اسلاگ)
path('corrections/<str:correction_slug>/', HadisCorrectionDetailView.as_view(), name='correction-detail'),
path('corrections/<str:correction_slug>/', cached_view(HadisCorrectionDetailView.as_view()), name='correction-detail'),
# Original Text Detail Endpoint (متن اصلی راوی بر اساس اسلاگ)
path('original-texts/<str:original_text_slug>/', TransmitterOriginalTextDetailView.as_view(), name='original-text-detail'),
path('original-texts/<str:original_text_slug>/', cached_view(TransmitterOriginalTextDetailView.as_view()), name='original-text-detail'),
]

4
apps/hadis/views/hadis.py

@ -146,7 +146,7 @@ class HadisSyncView(ListAPIView):
# 👇 اضافه شدن واکشی عمیق برای تصحیحات
Prefetch(
'hadiscorrection_set',
queryset=HadisCorrection.objects.prefetch_related(
queryset=HadisCorrection.objects.select_related('hadis').prefetch_related(
'references__book_reference__author',
'references__edition',
'references__book_volume',
@ -156,7 +156,7 @@ class HadisSyncView(ListAPIView):
# 👇 اضافه شدن واکشی عمیق برای تفاسیر
Prefetch(
'category__interpretations',
queryset=HadisInterpretation.objects.prefetch_related(
queryset=HadisInterpretation.objects.select_related('category').prefetch_related(
'references__book_reference__author',
'references__edition',
'references__book_volume',

1
config/settings/base.py

@ -117,6 +117,7 @@ AUTH_USER_MODEL = "account.User"
MIDDLEWARE = [
'config.middleware.site_middleware.SiteMiddleware', # Must be first to route by domain
'django.middleware.security.SecurityMiddleware',
'django.middleware.gzip.GZipMiddleware',
"whitenoise.middleware.WhiteNoiseMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',

Loading…
Cancel
Save