27 июня 2013 г.

django: что-то типа select_related для m2m

Заметка о проблемах со множественными выборками из БД при обходе списка сущностей в случае many-to-many связей.

Проблема

У нас есть такая схема:
# категория афиши
class CategoryPlaybill(models.Model):
    ...

# афиша
class Playbill(models.Model):
    categories = models.ManyToManyField(CategoryPlaybill, ...)
    ...

# проведение афиши
class ConductingPlaybill(models.Model):
    playbill = models.ForeignKey(Playbill, ...)
    ...
Итак, есть типы событий. Есть события по многие-ко-многим (событие может входить в несколько категорий). А есть их так называемые проведения — событие с конкретной датой, временем, местом. Мне надо получить проведения событий с разбивкой по категориям: категория1=>(список проведений), категория2 =>(список проведений).

Приходит в голову решение в лоб:
cond_playbill_list = ConductingPlaybill.objects.select_related().all()
cond_playbill_map = {}
for cond_playbill in cond_playbill_list:
    cats = cond_playbill.playbill.categories.all()
    for cat in cats:
        if cat not in cond_playbill_map:
            cond_playbill_map[cat] = []
        cond_playbill_map[cat].append(cond_playbill)
Но это решение ожидаемо даёт кучу запросов в БД на предмет SELECT FROM categoryplaybill WHERE playbill_categories.playbill_id = N для каждой итерации внешнего цикла. Дело в том, что select_related не тянет playbill_categories и ниже.

Мысли

Далее думаем: чтобы сделать одним запросом, надо в каждой строке resulset иметь 'playbill__categories__*', но при условии m2m получится дубли по числу связей playbill<->categories, очевидно. django orm такое не умеет, походу.

На raw-sql я бы написал что-то типа такого:
SELECT
clazz_conductingplaybill.id as 'conducting-id', clazz_conductingplaybill.date_from as 'conducting-date_from',
clazz_playbill.id as 'playbill-id', clazz_playbill.title as 'playbill-title',
clazz_playbill_categories.id as 'playbill-categories-id',
clazz_categoryplaybill.title as 'categoryplaybill-title'
FROM clazz_conductingplaybill, clazz_playbill_categories
JOIN clazz_playbill ON clazz_playbill.id=clazz_conductingplaybill.playbill_id
LEFT JOIN clazz_categoryplaybill ON clazz_categoryplaybill.id=clazz_playbill_categories.categoryplaybill_id
WHERE clazz_playbill_categories.playbill_id=clazz_playbill.id
ORDER BY clazz_playbill.id, clazz_conductingplaybill.id
Потом прошёл и собрал просто в map. В том числе все дубли clazz_conductingplaybill для каждой категории.

Решение

А решение тут простое: сначала собрать все связки playbill__categories, а потом обойти ConductingPlaybill, не вытягивая cond_playbill.playbill.categories и всё что дальше. Заодно используем defaultdict, крайне удобный и эффективный в нашем случае.
cond_playbill_map = defaultdict(list)
# связи выбираем категория-событие
byplaybill = defaultdict(list)
for pr in Playbill.categories.through.objects.select_related().all():
    byplaybill[pr.playbill.id].append(pr.categoryplaybill)
# выбираем проведения
cond_playbill_list = ConductingPlaybill.objects.select_related().all()
for cond_playbill in cond_playbill_list:
    cats = byplaybill[cond_playbill.playbill.id]
    for cat in cats:
        cond_playbill_map[cat].append(cond_playbill)
Если Playbill.categories.through - явно указанная модель, то можно использовать её. Не забудьте принять меры перед использованием defaultdict в django шаблонах :)

Комментариев нет:

Отправить комментарий