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
, если он не удается найти для этой транзакции.