2012-09-17 19 views
12

Załóżmy, że mam model Box z GenericForeignKey, który wskazuje na instancję Apple lub instancję Chocolate. Apple i Chocolate, z kolei mają klucze ForeignKeys odpowiednio do Farm i Factory. Chcę wyświetlić listę Box es, dla których muszę uzyskać dostęp do Farm i Factory. Jak to zrobić w jak najmniejszej liczbie zapytań DB?django: obiekty związane z prefetch z GenericForeignKey

Minimal przykład ilustracyjny:

class Farm(Model): 
    ... 

class Apple(Model): 
    farm = ForeignKey(Farm) 
    ... 

class Factory(Model): 
    ... 

class Chocolate(Model): 
    factory = ForeignKey(Factory) 
    ... 

class Box(Model) 
    content_type = ForeignKey(ContentType) 
    object_id = PositiveIntegerField() 
    content_object = GenericForeignKey('content_type', 'object_id') 
    ... 

    def __unicode__(self): 
     if self.content_type == ContentType.objects.get_for_model(Apple): 
      apple = self.content_object 
      return "Apple {} from Farm {}".format(apple, apple.farm) 
     elif self.content_type == ContentType.objects.get_for_model(Chocolate): 
      chocolate = self.content_object 
      return "Chocolate {} from Factory {}".format(chocolate, chocolate.factory) 

Oto kilka rzeczy próbowałem. We wszystkich tych przykładach liczba pudełek to N. Liczba zapytań zakłada, że ​​numery ContentType dla Apple i Chocolate zostały już zbuforowane, więc wywołania get_for_model() nie trafiają w DB.

1) naiwnego

print [box for box in Box.objects.all()]

ten sposób (pobranie Pudełka) + N (pobranie jabłkowy lub czekolady na każdym pudełku) + N (pobranie Farm dla każdego Apple i Fabryka dla każdej czekolady).

2) select_related nie pomaga tutaj, ponieważ Box.content_object to GenericForeignKey.

3) Od django 1.4, prefetch_related można pobrać GenericForeignKey s.

print [box for box in Box.objects.prefetch_related('content_object').all()]

ten sposób (pobranie skrzynek) + (pobierania jabłka czekolady dla wszystkich skrzynek) + N (pobranie Farm każdego Apple i fabrycznego każdej czekolady) zapytania.

4) Podobno prefetch_related nie jest wystarczająco inteligentny, aby śledzić ForeignKeys z GenericForeignKeys. Gdy próbuję:

print [box for box in Box.objects.prefetch_related( 'content_object__farm', 'content_object__factory').all()]

to słusznie twierdzi, że Chocolate obiekty nie mają pole farm, i vice versa.

5) można zrobić:

apple_ctype = ContentType.objects.get_for_model(Apple) 
chocolate_ctype = ContentType.objects.get_for_model(Chocolate) 
boxes_with_apples = Box.objects.filter(content_type=apple_ctype).prefetch_related('content_object__farm') 
boxes_with_chocolates = Box.objects.filter(content_type=chocolate_ctype).prefetch_related('content_object__factory') 

ten sposób (pobranie skrzynek) + (pobierania jabłka czekolady wszystkich ramkach) + (pobranie Farms dla jabłka i Fabryki dla wszystkich czekoladek). Minusem jest to, że muszę scalić i sortować dwa zestawy zapytań (boxes_with_apples, boxes_with_chocolates) ręcznie. W mojej prawdziwej aplikacji wyświetlam te Ramki w paginowanym ModelAdmin. Nie jest oczywiste, jak zintegrować to rozwiązanie. Może mógłbym napisać niestandardowy Paginator, aby to buforowanie było przejrzyste?

6) Mogłem połączyć coś na podstawie this, które również wykonuje zapytania O (1). Ale wolałbym nie zadzierać z internals (_content_object_cache), jeśli mogę tego uniknąć.

Podsumowując: Drukowanie skrzynki wymaga dostępu do przycisków ForeignKeys klucza GenericForeignKey. Jak mogę wydrukować skrzynki N w zapytaniach O (1)? Czy (5) najlepsze, co mogę zrobić, czy jest prostsze rozwiązanie?

Punkty bonusowe: Jak można zmienić ten schemat bazy danych, aby ułatwić takie zapytania?

+0

przypadku zmiany nazwy 'farm' /' factory' pewnym wspólną nazwą, jak 'creator', będzie prefetch_related pracę? – Igor

+0

Rzeczywiście, 'prefetch_related ('content_object__creator')' działa po twojej sugerowanej zmianie nazwy. Niestety, zmiana nazwy może być lub nie mieć sensu w zależności od rzeczywistych modeli, które masz na miejscu Apple/Farm i Chocolate/Factory. – cberzan

Odpowiedz

8

Możesz ręcznie zaimplementować coś takiego jak prefetch_selected i użyć metody Django select_related, która spowoduje dołączenie do zapytania bazy danych.

apple_ctype = ContentType.objects.get_for_model(Apple) 
chocolate_ctype = ContentType.objects.get_for_model(Chocolate) 
boxes = Box.objects.all() 
content_objects = {} 
# apples 
content_objects[apple_ctype.id] = Apple.objects.select_related(
    'farm').in_bulk(
     [b.object_id for b in boxes if b.content_type == apple_ctype] 
    ) 
# chocolates 
content_objects[chocolate_ctype.id] = Chocolate.objects.select_related(
    'factory').in_bulk(
     [b.object_id for b in boxes if b.content_type == chocolate_ctype] 
    ) 

ten powinien zrobić tylko 3 pytania (get_for_model zapytania są pomijane). Metoda in_bulk zwraca ci dict w formacie {id: model}. Tak, aby uzyskać content_object trzeba kod jak:

content_obj = content_objects[box.content_type_id][box.object_id] 

Jednak nie jestem pewien, czy ten kod będzie szybciej wówczas O (5) rozwiązanie, ponieważ wymaga dodatkowego iteracji na polach queryset a także to generuje zapytanie z WHERE id IN (...) oświadczeniem

Ale jeśli sortujesz pola tylko według pól z modelu Box, możesz wypełnić dyktando content_objects po paginacji. Ale trzeba zdać content_objects do __unicode__ jakoś

Jak byś byłaby tego schematu DB, aby takie pytania łatwiejsze?

Mamy podobną strukturę. Przechowujemy content_object w Box, ale zamiast object_id i content_object używamy ForeignKey(Box) w Apple i Chocolate. W wersji Box mamy metodę zwracania Apple lub Chocolate modelu get_object. W takim przypadku możemy użyć select_related, ale w większości przypadków użycia filtrujemy pola według content_type. Mamy więc takie same problemy, jak twoja piąta opcja. Ale rozpoczęliśmy projekt na Django 1.2, gdy nie było żadnego parametru prefetch_selected.

Jeśli zmienisz nazwę farmy/fabryki na jakąś pospolitą nazwę, np. Twórca, zostanie wstępnie pobrane zadanie?

O swojej opcji

mogę powiedzieć nic przeciwko napełniania _content_object_cache. Jeśli nie lubisz zajmować się wewnętrznymi można wypełnić właściwość niestandardową, a następnie użyć

apple = getattr(self, 'my_custop_prop', None) 
if apple is None: 
    apple = self.content_object 
+0

Właśnie zauważyłem, że moja odpowiedź jest bardzo zbliżona do twojej * opcji 6 *, ale z mniejszą automatyzacją. Nigdy wcześniej nie czytałem tego artykułu.Również to nie wygląda na O (1), to raczej O (2 + number_of_unique_ctypes) – Igor

Powiązane problemy