Как эффективно выполнять итерацию по многим объектам foreignkey с помощью Django ORM?

Допустим, у меня есть model, например:

class User(AbstractUser):

  ...
  seller = models.ForeignKey(Seller, related_name="user", on_delete=models.SET_NULL, null=True)
  ...

Я пытаюсь получить seller адрес электронной почты, используя этот код:

from app.models import User
from django_print_sql import print_sql

with print_sql(count_only=False):
  users = User.objects.filter(is_active=True, seller_id__isnull=False).select_related().only('seller__email')
  for u in users.iterator():
    email = u.seller.email
    send_email(email)

В этом случае я вижу SQL-запросы типа:

SELECT `user`.`id`,
       `user`.`seller_id`
FROM `user`
WHERE (`user`.`is_active`
       AND `user`.`seller_id` IS NOT NULL)

...

SELECT `seller`.`id`,
       ...
       `seller`.`email`,
       ...
FROM `seller`
WHERE `seller`.`id` = 1

...

Проблема в том, что Django ORM обращается к БД на каждой итерации (Select seller... where seller.id = ...). Так что это будет слишком много запросов (== соединений с БД), если у нас много продавцов.

Другим способом можно заменить only на values:

from app.models import User
from django_print_sql import print_sql

with print_sql(count_only=False):
  users = User.objects.filter(is_active=True, seller_id__isnull=False).select_related().values('seller__email')
  for u in users.iterator():
    email = u['seller__email']
    send_email(email)

И я могу видеть SQL запрос типа:

SELECT `seller`.`email`
FROM `user`
INNER JOIN `seller` ON (`user`.`seller_id` = `seller`.`id`)
WHERE (`user`.`is_active`
       AND `user`.`seller_id` IS NOT NULL)

Это немного лучше, и мы можем получить электронные письма одним запросом к БД, но iterator здесь бесполезно, потому что мы загружаем все электронные письма один раз в объект dict и все данные существуют в памяти.

Вопрос такой
. Можно ли сделать итерацию по кускам данных (скажем, по 500 писем/одному запросу) без создания ручных подциклов, ограничивающих User.objects.filter количество запросов? Или, другими словами, какой итератор наиболее эффективен в данном случае?

Похоже, что select_related работает для этого случая:

from app.models import User
from django_print_sql import print_sql

with print_sql(count_only=False):
  users = User.objects.filter(is_active=True, seller_id__isnull=False).select_related('seller').only('seller__email').exclude(seller__email__exact='')
  for u in users.iterator():
    send_email(u.seller.email)
SELECT `user`.`id`,
       `user`.`owner_id`,
       `seller`.`id`,
       `seller`.`email`
FROM `user`
INNER JOIN `seller` ON (`user`.`seller_id` = `seller`.`id`)
WHERE (`user`.`is_active`
       AND `user`.`seller_id` IS NOT NULL
       AND NOT (`seller`.`email` =
                AND `seller`.`email` IS NOT NULL))

В вашем случае вы не указываете параметров для select_related, я предлагаю вам поместить seller поле в select_related следующим образом:

from app.models import User
from django_print_sql import print_sql

with print_sql(count_only=False):
  users = User.objects.select_related('seller').filter(is_active=True, seller_id__isnull=False).only('seller__email')
  for u in users.iterator():
    email = u.seller.email
    send_email(email)

помещая select_related перед objects менеджером. Делая это, django получает записи модели продавца как поле для каждого итерируемого пользователя и не будет обращаться к базе данных в каждой итерации.

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