2025, Sep 26 15:33
Django ORM में queryset से दिन‑वार फिएट कीमत annotate करें
जानें कैसे Django ORM में queryset स्तर पर transactions को दिन‑स्तरीय फिएट कीमतों से समृद्ध करें: डेटा नॉर्मलाइज़ेशन, माइग्रेशन, Subquery व Coalesce के व्यावहारिक तरीके।
एनालिटिक्स पर केंद्रित Django प्रोजेक्ट्स में ट्रांजैक्शनल डेटा को दिन-स्तरीय फिएट कीमतों से समृद्ध करना अक्सर जरूरी होता है। मुश्किल तब बढ़ जाती है जब ऐप में एक तरफ वास्तविक datetime मानों वाली नॉर्मलाइज़्ड टेबल हो और दूसरी तरफ ऐसी लिगेसी टेबल, जिसमें तारीखें स्ट्रिंग के रूप में रखी गई हों। मकसद है यह एनरिचमेंट Python में नहीं, सीधे queryset स्तर पर करना — और उसे तेज, एकरूप और संभालने में आसान रखना।
बेसलाइन Python तरीका
नीचे दी गई लॉजिक ट्रांजैक्शनों पर इटरेट करती है, Price से संबंधित दिन की कीमत ढूँढती है, जरूरत पड़ने पर PriceTextBackup पर फॉलबैक करती है और मानों को जोड़ती है। यह काम करती तो है, लेकिन सब कुछ Python के लूप्स में होता है और पंक्तियों को जोड़ने तथा एनोटेट करने के लिए डेटाबेस की क्षमताओं का लाभ नहीं लेता।
for entry in transactions:
    day_marker = entry.timestamp.date()
    rate_obj = Price.objects.filter(fiat=cur_id, date=day_marker).first()
    if not rate_obj:
        rate_obj = PriceTextBackup.objects.filter(
            fiat=cur_id,
            date=day_marker.strftime("%d-%m-%Y")
        ).first()
    if rate_obj:
        if entry.amount > 0:
            subtotal = rate_obj.price * entry.amount
            price_list.append(subtotal)
            transaction_counter += entry.amount
टेम्पलेट का यह अंश दिखाता है कि hover पॉप‑अप के साथ समृद्ध फिएट मान को कैसे प्रदर्शित किया जाता है:
{% if row.amount >= 0 %}
    <td onmouseover="showPopup({{ forloop.counter }}0000)"
        onmouseout="hidePopup()"
        class="text-success">{{ row.fiat_CHF|floatformat:2|intcomma }}</td>
{% else %}
    <td class="text-danger">{{ row.fiat_CHF|floatformat:2|intcomma }}</td>
{% endif %}
क्यों ORM queryset वाला तरीका वैसे‑का‑वैसा काम नहीं करता
रुकावट डेटा की बनावट है। Price.date एक DateTimeField है, जबकि PriceTextBackup.date स्ट्रिंग है। जब तक किसी एक तरफ तारीख %d-%m-%Y जैसे फ़ॉर्मैट में CharField के रूप में रहेगी, आप केवल ORM अभिव्यक्तियों से Transactions पर भरोसेमंद तरीके से दिन की कीमत annotate नहीं कर पाएंगे। टाइमस्टैम्प को दिन‑वार कीमतों से मिलाने के लिए सिर्फ date भाग की तुलना करनी पड़ती है। तारीखें नॉर्मलाइज़ होने तक हर queryset समाधान बोझिल या धीमा हो जाएगा।
पहले माइग्रेट करें: दो सुरक्षित रास्ते
सबसे साफ़ रास्ता है डेटा मॉडल को ठीक करना। या तो PriceTextBackup की उपयोगी पंक्तियाँ Price में ले आएँ, या बैकअप टेबल की date को वास्तविक समय-प्रकार में बदलें। Django की डेटा माइग्रेशन्स से दोनों विकल्प सीधे‑सादे हैं।
विकल्प 1: PriceTextBackup से उपयोगी पंक्तियाँ Price में ले जाएँ
एक खाली माइग्रेशन बनाइए और बैकअप से पार्स की जा सकने वाली पंक्तियों से Price को भरिए। इससे दिन‑वार कीमतों के लिए Price एकमात्र सत्य स्रोत बना रहता है।
manage.py makemigrations --empty my_appइसके बाद माइग्रेशन को इस तरह लागू करें:
# Django 5.2.0 द्वारा 2025-09-04 17:28 को जनरेट किया गया
from django.db import migrations, models
from django.db.models import DateTimeField
from datetime import datetime
def seed_forward(apps, schema_editor):
    Price = apps.get_model('app_name', 'Price')
    PriceTextBackup = apps.get_model('app_name', 'PriceTextBackup')
    present = {
        (p.date.date(), p.fiat_id) for p in Price.objects.only('date', 'fiat_id')
    }
    stash = []
    for rec in PriceTextBackup.objects.all():
        parsed = None
        try:
            parsed = datetime.strptime(rec.date, '%d-%m-%Y').date()
        except ValueError:
            print('problem for {item.date!r}')
            continue
        key = (parsed, rec.fiat_id)
        if key not in present:
            present.add(key)
            stash.append(
                Price(price=rec.price, date=parsed, fiat_id=rec.fiat_id)
            )
        Price.objects.bulk_create(stash)
class Migration(migrations.Migration):
    dependencies = [
        ('app_name', '1234_previous_migration_file'),
    ]
    operations = [migrations.RunPython(seed_forward)]
इसे पहले डेटाबेस की एक कॉपी पर चलाकर नतीजा जाँच लें। इसके बाद आपकी क्वेरीज़ एक ही Price टेबल के खिलाफ काम कर सकती हैं।
विकल्प 2: बैकअप की date को वास्तविक DateTimeField में बदलें
वैकल्पिक रूप से, PriceTextBackup में स्ट्रिंग फ़ील्ड को DateTimeField में माइग्रेट करके date को नॉर्मलाइज़ करें। मॉडल बदलने से शुरू करें:
from datetime import datetime
class PriceTextBackup(models.Model):
    date = models.DateTimeField(default=datetime(1970, 1, 1))
    price = models.FloatField()
    fiat = models.ForeignKey(Currencies, on_delete=models.DO_NOTHING)
माइग्रेशन बनाएँ, लेकिन अभी लागू न करें:
python manage.py makemigrationsफिर जनरेट हुई माइग्रेशन को इस तरह संशोधित करें कि वह पुरानी स्ट्रिंग्स को एक अस्थायी फ़ील्ड में पार्स करे, फ़ील्ड्स की अदला‑बदली करे और पार्स किया हुआ मान सुरक्षित रखे:
# Django 5.2.0 द्वारा 2025-09-04 17:28 को जनरेट किया गया
from django.db import migrations, models
from django.db.models import DateTimeField
from datetime import datetime
def convert_forward(apps, schema_editor):
    PriceTextBackup = apps.get_model('app_name', 'PriceTextBackup')
    buffer = []
    for row in PriceTextBackup.objects.iterator():
        buffer.append(row)
        row.date_as_date = datetime.strptime(row.date, '%d-%m-%Y')
        if len(buffer) > 100:
            PriceTextBackup.objects.bulk_update(
                buffer, fields=('date_as_date',)
            )
            buffer = []
    PriceTextBackup.objects.bulk_update(buffer, fields=('date_as_date',))
class Migration(migrations.Migration):
    dependencies = [
        ('app_name', '1234_previous_migration_file'),
    ]
    operations = [
        migrations.AddField(
            model_name='pricetextbackup',
            name='date_as_date',
            field=models.DateTimeField(
                blank=True, null=True, verbose_name='Date'
            ),
        ),
        migrations.RunPython(convert_forward),
        migrations.RemoveField(
            model_name='pricetextbackup',
            name='date',
        ),
        migrations.RenameField(
            model_name='currencyrates',
            old_name='date_as_date',
            new_name='date',
        ),
        migrations.AddField(
            model_name='pricetextbackup',
            name='date',
            field=models.DateTimeField(verbose_name='Date'),
        ),
    ]
पहले की तरह, इसे भी डेटाबेस की कॉपी पर जाँचें। यदि कुछ स्ट्रिंग तारीखें %d-%m-%Y का पालन नहीं करतीं, तो उन्हें अलग तरह से पार्स करना होगा या अनुपयोगी पंक्तियाँ छोड़नी होंगी।
Subquery और Coalesce के साथ क्वेरी करना
एक बार कीमतें नॉर्मलाइज़ हो जाने पर, आप queryset में ही ट्रांजैक्शनों पर मेल खाती दिन‑कीमत annotate कर सकते हैं। यदि Price को एकल सत्य स्रोत रखना है, तो Subquery से annotate करें जो दिन और मुद्रा के आधार पर खोज कर पहला परिणाम ले आए:
from django.db.models import OuterRef, Subquery
currency_id = 1234
Transactions.objects.annotate(
    price=Subquery(
        Price.objects.filter(
            date__date=OuterRef('date__date'),
            fiat_id=currency_id,
        ).values('price')[:1]
    )
)
अगर दोनों टेबल बनाए रखना चाहते हैं और फॉलबैक चाहिए, तो दो Subquery अभिव्यक्तियों को Coalesce में लपेटें ताकि पहली में मेल न मिलने पर ही दूसरी का उपयोग हो:
from django.db.models import OuterRef, Subquery
from django.db.models.functions import Coalesce
currency_id = 1234
Transactions.objects.annotate(
    price=Coalesce(
        Subquery(
            Price.objects.filter(
                date__date=OuterRef('date__date'),
                fiat_id=currency_id,
            ).values('price')[:1]
        ),
        Subquery(
            PriceTextBackup.objects.filter(
                date__date=OuterRef('date__date'),
                fiat_id=currency_id,
            ).values('price')[:1]
        ),
    ),
)
queryset से मिलने वाले हर Transaction इंस्टैंस पर annotate किया गया price गुण उपलब्ध होगा। यह डेटाबेस फ़ील्ड नहीं है; इसे SELECT के जरिए गणना किया जाता है और टेम्पलेट्स में सामान्य फ़ील्ड्स की तरह ही दिखाया जा सकता है।
यह क्यों मायने रखता है
एनरिचमेंट को डेटाबेस लेयर में ले जाने से बड़े डेटा‑सेट्स पर Python के लूप्स की जरूरत खत्म होती है और N+1 क्वेरी पैटर्न से बचाव होता है। समय‑संबंधी मानों का नॉर्मलाइज़ेशन तुलना को एकरूप रखता है और date पार्ट्स के साथ काम करना या एग्रीगेट्स चलाना आसान बनाता है। अंत में, दिन‑वार कीमतों के लिए एक ही आधिकारिक टेबल रखने से कोड में शाखाएँ कम होती हैं और स्रोतों के बीच अस्पष्टता दूर होती है।
व्यावहारिक निष्कर्ष
सबसे पहले अपनी कीमतों का डेटा नॉर्मलाइज़ करें। या तो डेटा माइग्रेशन से PriceTextBackup की वैध पंक्तियों को Price में कॉपी करें, या PriceTextBackup.date को वास्तविक DateTimeField में बदलें और पुरानी स्ट्रिंग्स को पार्स करें। इसके बाद Subquery का उपयोग कर Transactions को annotate करें; यदि दोनों टेबल रहें तो चाहें तो Coalesce फॉलबैक जोड़ें। इसका नतीजा एक साफ, डेटाबेस‑चालित एनरिचमेंट होगा जिसे आप अपनी views और टेम्पलेट्स में सीधे इस्तेमाल कर सकते हैं, बिना हर पंक्ति पर अतिरिक्त लुकअप के।
यह लेख StackOverflow पर प्रश्न (लेखक: donbolli) और willeM_ Van Onsem द्वारा दिए गए उत्तर पर आधारित है।