Django обогащает набор запросов

У меня есть приложение Django, которое запускается только локально на моем компьютере.

У меня есть две разные таблицы, с транзакцией и ценой. Теперь я хочу дополнить свою транзакцию ценой.

Я уже писал нечто подобное для расчета средней цены:

for transaction in transactions:
    filter_date = transaction.timestamp.date()
    price = Price.objects.filter(fiat=cur_id, date=filter_date).first()
    if not price:
        price = PriceTextBackup.objects.filter(fiat=cur_id, date=filter_date.strftime("%d-%m-%Y")).first()
    if price:
        if transaction.amount > 0:
            sats_price=price.price*transaction.amount
            price_list.append(sats_price)
            transaction_counter += transaction.amount

Я отображаю цену во всплывающем окне при наведении курсора мыши, html-страница выглядит следующим образом

            {% if transaction.amount >= 0 %}
                <td onmouseover="showPopup({{forloop.counter}}0000)" onmouseout="hidePopup()" class="text-success">{{transaction.fiat_CHF|floatformat:2 |intcomma }}</td>
            {% else %}
                <td class="text-danger">{{transaction.fiat_CHF|floatformat:2 |intcomma }}</td>
            {% endif %}

Как я могу применить ту же логику для цены в наборе запросов?

Таким образом, вы можете использовать Django annotate() с Subquery для эффективного обогащения вашего набора запросов вместо зацикливания.

Вот как это сделать:

from django.db.models import OuterRef, Subquery, Case, When, F
from django.db.models.functions import Cast
from django.db.models import DateField

# Create subqueries for price lookup
price_subquery = Price.objects.filter(
    fiat=cur_id,
    date__date=OuterRef('timestamp__date')
).values('price')[:1]

# Backup price subquery (convert string date to match)
backup_price_subquery = PriceTextBackup.objects.filter(
    fiat=cur_id,
    date=Cast(OuterRef('timestamp__date'), output_field=models.CharField())
).values('price')[:1]

# Enrich transactions with price data
transactions = Transactions.objects.annotate(
    current_price=Subquery(price_subquery),
    backup_price=Subquery(backup_price_subquery),
    # Use current_price if available, otherwise backup_price
    new_price=Case(
        When(current_price__isnull=False, then=F('current_price')),
        default=F('backup_price')
    ),
    # Calculate sats_price (only for positive amounts)
    sats_price=Case(
        When(amount__gt=0, then=F('new_price') * F('amount')),
        default=0
    )
)

# Now you can access the enriched data directly
for transaction in transactions:
    if transaction.new_price:
        print(f"Transaction: {transaction.amount}, Price: {transaction.new_price}")
        print(f"Sats price: {transaction.sats_price}")

Счастливого строительства! Дайте мне знать, если это сработает!

Сначала я бы посоветовал заменить date на PriceTextBackup на DateTimeField или DateField. Это потребует небольшой работы, но главное преимущество заключается в том, что это упростит выполнение запросов и, вероятно, уменьшит размер базы данных и, таким образом, сделает запросы (немного) быстрее.

Мы можем сделать это, изменив тип поля на:

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 # don't migrate yet!

но пока не выполняйте миграцию , а не . Теперь мы можем изменить файл миграции на что-то вроде:

# Generated by Django 5.2.0 on 2025-09-04 17:28

from django.db import migrations, models
from django.db.models import DateTimeField


def forwards_func(apps, schema_editor):
    PriceTextBackup = apps.get_model("app_name", "PriceTextBackup")
    to_update = []
    for item in PriceTextBackup.objects.iterator():
        to_update.append(item)
        item.date_as_date = datetime.strptime(item.date, '%d-%m-%Y')
        if len(to_update) > 100:
            PriceTextBackup.objects.bulk_update(to_update, fields=('date_as_date',))
            to_update = []

    PriceTextBackup.objects.bulk_update(to_update, 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(forwards_func),
        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'),
        ),
    ]

Я бы настоятельно посоветовал протестировать эту миграцию на копии базы данных, чтобы убедиться, что она работает корректно. Если не все date в виде строки отформатированы как %d-%m-%Y, возможно, разберите их по-другому, или, если временная метка действительно непригодна для использования, удалите запись из базы данных (и, возможно, сохраните ее в дампе).

После того, как это будет сделано, вы можете дополнить набор запросов:

cur_id = 1234

Transactions.objects.annotate(
  price=Coalesce(
    Subquery(
      Price.objects.filter(
        date__date=OuterRef('date__date')), fiat_id=cur_id
      ).values('price')[:1]
    ),
Subquery(
      PriceTextBackup.objects.filter(
        date__date=OuterRef('date__date')), fiat_id=cur_id
      ).values('price')[:1]
    ),
)

Объекты Transaction, возникающие из этого набора запросов, будут иметь дополнительный атрибут .price, который будет результатом поиска в Price и PriceTextBackup, если он не удается найти для этой транзакции.

Вернуться на верх