2012-11-29 8 views
5

Ho i seguenti modelli:Prevenzione O (n) Le query con i modelli di intermediazione

class Artist(models.Model): 
    name = models.CharField() 

    def primary_group(self): 
     return self.memberships.select_related('group').get(is_primary=True) 

class Group(models.Model): 
    name = models.CharField() 
    members = models.ManyToManyField(Artist, through='Membership') 

class Membership(models.Model): 
    artist = models.ForeignKey(Artist, related_name='memberships') 
    group = models.ForeignKey(Group) 
    is_primary = models.BooleanField() 

Artist e Group sono collegati tramite un modello di intermediazione, Membership. Gli artisti possono avere solo un gruppo principale, che è contrassegnato tramite

In un modello in cui elenco gli artisti, elenco le informazioni di base sull'artista in aggiunta al loro gruppo principale, richiamato dal metodo precedente. Tuttavia, questa è un'operazione O (n) e ho circa 160 artisti a cui fare questo. Lo SQL che il Django-debug-barra degli strumenti offre è la seguente:

SELECT ••• FROM "people_membership" 
      LEFT OUTER JOIN "people_group" ON ("people_membership"."group_id" = "people_group"."id") 
      WHERE ("people_membership"."artist_id" = xx AND "people_membership"."is_primary" = true) 

Lasciatemi aggiungere che questo accade per ogni artista elencati, così ottengo circa 160 di questi.

È O (n) il meglio che si può fare, visto che chiamo un metodo modello? O c'è qualcos'altro che posso fare per migliorare questo (a corto di denormalizzare primary_group)? Questo sembra essere un problema con qualsiasi tipo di informazione che è memorizzata in un modello intermedio che vorrei chiamare dalla fonte o dalla destinazione.

risposta

6

Si può facilmente farlo con due query, che nonostante quello che diranno eventuali nemici, non importa affatto:

artists = list(Artist.objects.all()) 
primary_memberships = {m.artist_id: m for m in Group.objects.filter(is_primary=True, membership__artist__in=artists).extra(select={'artist_id': '%s.artist_id' % (Membership._meta.db_table,)})} 
for artist in artists: 
    artist.primary_membership = primary_memberships.get(artist.id) 

(La clausola in più potrebbe non essere corretto, ma si ottiene l'idea)

In aggiunta a questo, vorrei cambiare la funzione primaria di fare come:

if hasattr(self, '_primary_membership_cache'): 
    return self._primary_membership_cache 

e poi se si allega le informazioni, si legano a quella variabile, e basta usare la funzione stessa ca ll.

(Seguiamo questo tipo di modello in tutto il luogo a Disqus per vari unisce/query dispari)

+1

Abbastanza sicuro che questo è per lo più corretto ora :) –

0

Hai provato di iniziare la query con l'appartenenza, piuttosto che l'artista?

class Artist(models.Model): 
    ... 
    def primary_group(self): 
     return Membership.objects.filter(artist=self).get(is_primary=True).group 
+0

La query risultante è quasi la stessa. –

4

lo farei come David Cramer dice ma invece di più:

primary_memberships = {m.artist_id: m.group for m in Membership.objects.filter(group__isprimary=True, artist__in=artists).select_related('group')} 
for artist in artists: 
    artists.primary_membership = primary_memberships.get(artist.id) 

per i punti bonus fanno di questo un metodo su manager di adesione in modo da poter applicare a qualsiasi lista degli artisti con facilità!

1

Che ne dici di inserire un indice a due colonne su membership (artist_id, is_primary)? If you've already upgraded to 1.5b1 puoi farlo all'interno dei tuoi modelli, ma nulla ti impedisce di farlo sul back-end se non lo hai fatto. Ciò dovrebbe ridurre la ricerca di appartenenza a un tempo costante. Se il tuo DB lo supporta, puoi renderlo un partial index, ma con solo 160 artisti, non sembra tutto ciò che è necessario.

Problemi correlati