2015-01-08 2 views
2

Соотношение между Foo и Bar через Baz следующим образом:O (1) Джанго ОРМ стратегия для запроса связанных объектов связанных объектов

class Foo(Model): 
    # stuff 

class Bar(Model) 
    # stuff 

class Baz(Model): 
    foos = ManyToManyField("Foo") 
    bar = ForeignKey("Bar") 

я в основном необходимо сгенерировать следующий Dict, представляющий Bars, которые связаны каждый Foo через Baz (в Словаре понимание псевдокод):

{ foo.id: [список уникальных баров, связанных с обув через любой Baz] for foo in all foos}

В настоящее время я могу сгенерировать свою структуру данных с помощью запросов O (N) (1 запрос на Foo), но с большим количеством данных это узкое место, и мне нужно, чтобы он оптимизировался на O (1) (ни один запрос сам по себе , но фиксированное количество запросов независимо от размера данных любой из моделей), а также минимизация итераций данных в python.

+0

Как бы вы его написали в SQL? – thebjorn

+0

Если бы я знал это, я мог бы перевести его в Django. Я владею базовым SQL, но, увы, не является ниндзя OUTER JOIN. :) –

ответ

2

Использование select_related и prefetch_related, я думаю, что вы можете создать необходимую структуру данных с 2-запросов:

out = {} 
bazes = Baz.objects.select_related('bar').prefetch_related('foos') 
for baz in bazes: 
    for foo in baz.foos.all(): 
     out.setdefault(foo.id, set()).add(baz.bar) 

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

+0

Почему бы не использовать 'collection.defaultdict (set)'? – thebjorn

+0

В случае использования [по умолчанию dict в шаблоне Django] (http://stackoverflow.com/questions/4764110/django-template-cant-loop-defaultdict) появляется сообщение. Никакой другой причины. Использование дефолта по умолчанию было бы неплохо, и после этого вы могли бы преобразовать его в обычный dict. – Alasdair

+1

интересный бит из документов 1.7 относительно больших наборов данных: «prefetch_related в большинстве случаев будет реализован с использованием SQL-запроса, который использует оператор« IN ». Это означает, что для большого QuerySet может быть создано большое предложение« IN », которое, в зависимости от базы данных, может иметь проблемы с производительностью, когда дело доходит до разбора или выполнения SQL-запроса. Всегда профиль для вашего случая использования!« – sthzg

3

Если вы можете упасть в SQL, вы можете использовать единый запрос (имя_приложения должны префикс все имена таблиц):

select distinct foo.id, bar.id 
from baz_foos 
join baz on baz_foos.baz_id = baz.id 
join foo on baz_foos.foo_id = foo.id 
join bar on baz.bar_id = bar.id 

baz_foos является многие-ко-многим таблице создает Django.

@ Решение Alasdair возможно/возможно более читаемо (хотя, если вы делаете это по причинам производительности, которые могут быть не самыми важными). Его решение использует ровно два запроса (что вряд ли имеет значение). Единственная проблема, которую я вижу, если у вас есть большое количество Баз объектов, так как генерируемый SQL выглядит следующим образом:

SELECT "foobar_baz"."id", "foobar_baz"."bar_id", "foobar_bar"."id" 
FROM "foobar_baz" 
INNER JOIN "foobar_bar" ON ("foobar_baz"."bar_id" = "foobar_bar"."id") 

SELECT 
    ("foobar_baz_foos"."baz_id") AS "_prefetch_related_val", 
    "foobar_foo"."id" 
FROM "foobar_foo" 
INNER JOIN "foobar_baz_foos" ON ("foobar_foo"."id" = "foobar_baz_foos"."foo_id") 
WHERE "foobar_baz_foos"."baz_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 
    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 
    55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 
    75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 
    95, 96, 97, 98, 99, 100, 101) 

Если у вас есть только несколько бар-х и несколько сотен Foo, я бы сделать:

from django.db import connection 
from collections import defaultdict 

# foos = {f.id: f for f in Foo.objects.all()} 
bars = {b.id: b for b in Bar.objects.all()} 

c = connection.cursor() 
c.execute(sql) # from above 
d = defaultdict(set) 
for f_id, b_id in c.fetchall(): 
    d[f_id].add(bars[b_id]) 
+0

Да, к сожалению, наборы данных, baz, как правило, самый большой и наименее ограниченный (обычно не более нескольких баров и, возможно, 30-100 фосов), поэтому я могу идти вперед и писать sql, если я могу выяснить как заставить его играть хорошо с питоном. –

+0

Я обновил ответ (решение в основном делает ручную «prefetch_related» на «Bar»). – thebjorn

+0

Мне нравится прямолинейность и чистота метода Аласдайра, но это отлично подходит для ситуаций, когда оптимизация является ключевой, а у Baz большой набор данных. Выучили некоторые хорошие вещи здесь. Спасибо, thebjorn. –

Смежные вопросы