J'ai un site Web où les utilisateurs peuvent voir une liste de films et créer des critiques pour eux.
L'utilisateur doit pouvoir voir la liste de tous les films. De plus, S'ils ont revu le film, ils devraient pouvoir voir le score qu'ils lui ont donné. Sinon, le film est simplement affiché sans la partition.
Ils ne se soucient pas du tout des scores fournis par les autres utilisateurs.
Considérer ce qui suit models.py
from Django.contrib.auth.models import User
from Django.db import models
class Topic(models.Model):
name = models.TextField()
def __str__(self):
return self.name
class Record(models.Model):
user = models.ForeignKey(User)
topic = models.ForeignKey(Topic)
value = models.TextField()
class Meta:
unique_together = ("user", "topic")
Ce que je veux essentiellement c'est ceci
select * from bar_topic
left join (select topic_id as tid, value from bar_record where user_id = 1)
on tid = bar_topic.id
Considérer ce qui suit test.py
pour le contexte:
from Django.test import TestCase
from bar.models import *
from Django.db.models import Q
class TestSuite(TestCase):
def setUp(self):
t1 = Topic.objects.create(name="A")
t2 = Topic.objects.create(name="B")
t3 = Topic.objects.create(name="C")
# 2 for Johnny
johnny = User.objects.create(username="Johnny")
johnny.record_set.create(topic=t1, value=1)
johnny.record_set.create(topic=t3, value=3)
# 3 for Mary
mary = User.objects.create(username="Mary")
mary.record_set.create(topic=t1, value=4)
mary.record_set.create(topic=t2, value=5)
mary.record_set.create(topic=t3, value=6)
def test_raw(self):
print('\nraw\n---')
with self.assertNumQueries(1):
topics = Topic.objects.raw('''
select * from bar_topic
left join (select topic_id as tid, value from bar_record where user_id = 1)
on tid = bar_topic.id
''')
for topic in topics:
print(topic, topic.value)
def test_orm(self):
print('\norm\n---')
with self.assertNumQueries(1):
topics = Topic.objects.filter(Q(record__user_id=1)).values_list('name', 'record__value')
for topic in topics:
print(*topic)
LES DEUX tests devraient imprimer exactement la même sortie, cependant, seule la version brute crache le tableau de résultats correct:
brut --- A 1 B Aucun C 3
l'orm renvoie à la place ce
orm --- A 1 C 3
Toute tentative de rejoindre le reste des sujets, ceux qui n'ont aucun avis de l'utilisateur "johnny", se traduit par ce qui suit:
orm
---
A 1
A 4
B 5
C 3
C 6
Comment puis-je accomplir le comportement simple de la requête brute avec l'ORM Django?
edit: Ce genre de travaux mais semble très pauvre:
topics = Topic.objects.filter (record__user_id = 1) .values_list ('name', 'record__value') noned = Topic.objects.exclude (record__user_id = 1) .values_list ('name') pour le sujet dans la chaîne (sujets, non): ...
edit: Cela fonctionne un peu mieux, mais toujours mauvais:
topics = Topic.objects.filter (record__user_id = 1) .annotate (value = F ('record__value')) topics | = Topic.objects.exclude (pk__in = topics)
orm --- A 1 B 5 C 3
Tout d'abord, il n'y a pas moyen (atm Django 1.9.7) d'avoir une représentation avec l'ORM de Django de la requête brute que vous avez publiée, exactement comme vous le souhaitez; cependant, vous pouvez obtenir le même résultat souhaité avec quelque chose comme:
>>> Topic.objects.annotate(
f=Case(
When(
record__user=johnny,
then=F('record__value')
),
output_field=IntegerField()
)
).order_by(
'id', 'name', 'f'
).distinct(
'id', 'name'
).values_list(
'name', 'f'
)
>>> [(u'A', 1), (u'B', None), (u'C', 3)]
>>> Topic.objects.annotate(f=Case(When(record__user=may, then=F('record__value')), output_field=IntegerField())).order_by('id', 'name', 'f').distinct('id', 'name').values_list('name', 'f')
>>> [(u'A', 4), (u'B', 5), (u'C', 6)]
Voici le SQL généré pour la première requête:
>>> print Topic.objects.annotate(f=Case(When(record__user=johnny, then=F('record__value')), output_field=IntegerField())).order_by('id', 'name', 'f').distinct('id', 'name').values_list('name', 'f').query
>>> SELECT DISTINCT ON ("payments_topic"."id", "payments_topic"."name") "payments_topic"."name", CASE WHEN "payments_record"."user_id" = 1 THEN "payments_record"."value" ELSE NULL END AS "f" FROM "payments_topic" LEFT OUTER JOIN "payments_record" ON ("payments_topic"."id" = "payments_record"."topic_id") ORDER BY "payments_topic"."id" ASC, "payments_topic"."name" ASC, "f" ASC
distinct
avec des arguments positionnels est utilisé dans cette réponse, qui n'est disponible que pour PostgreSQL, atm. Dans la documentation, vous pouvez en savoir plus sur expressions conditionnelles .Ce que je veux essentiellement c'est ceci
select * from bar_topic left join (select topic_id as tid, value from bar_record where user_id = 1) on tid = bar_topic.id
... ou, peut-être cet équivalent qui évite une sous-requête ...
select * from bar_topic
left join bar_record
on bar_record.topic_id = bar_topic.id and bar_record.user_id = 1
Je veux savoir comment le faire efficacement ou, si c'est impossible, une explication de pourquoi c'est impossible ...
À moins que vous n'utilisiez des requêtes brutes, c'est impossible avec l'ORM de Django, et voici pourquoi.
QuerySet
objets (Django.db.models.query.QuerySet
) ont un attribut query
(Django.db.models.sql.query.Query
) qui est une représentation de la requête réelle qui sera exécutée. Ces objets Query
ont utilement une méthode __str__
, Vous pouvez donc l'imprimer pour voir de quoi il s'agit.
Commençons par un simple QuerySet
...
>>> from bar.models import *
>>> qs = Topic.objects.filter(record__user_id=1)
>>> print qs.query
SELECT "bar_topic"."id", "bar_topic"."name" FROM "bar_topic" INNER JOIN "bar_record" ON ("bar_topic"."id" = "bar_record"."topic_id") WHERE "bar_record"."user_id" = 1
... ce qui ne va évidemment pas fonctionner, à cause du INNER JOIN
.
En regardant de plus près à l'intérieur de l'objet Query
, il y a un attribut alias_map
Qui détermine quelles jointures de table seront effectuées ...
>>> from pprint import pprint
>>> pprint(qs.query.alias_map)
{u'bar_record': JoinInfo(table_name=u'bar_record', rhs_alias=u'bar_record', join_type='INNER JOIN', lhs_alias=u'bar_topic', lhs_join_col=u'id', rhs_join_col='topic_id', nullable=True),
u'bar_topic': JoinInfo(table_name=u'bar_topic', rhs_alias=u'bar_topic', join_type=None, lhs_alias=None, lhs_join_col=None, rhs_join_col=None, nullable=False),
u'auth_user': JoinInfo(table_name=u'auth_user', rhs_alias=u'auth_user', join_type='INNER JOIN', lhs_alias=u'bar_record', lhs_join_col='user_id', rhs_join_col=u'id', nullable=False)}
Notez que Django ne prend en charge que deux join_type
S, INNER JOIN
Et LEFT OUTER JOIN
.
Maintenant, nous pouvons utiliser les méthodes promote_joins
De l'objet Query
pour utiliser un LEFT OUTER JOIN
Sur la table bar_record
...
>>> qs.query.promote_joins(['bar_record'])
>>> pprint(qs.query.alias_map)
{u'bar_record': JoinInfo(table_name=u'bar_record', rhs_alias=u'bar_record', join_type='LEFT OUTER JOIN', lhs_alias=u'bar_topic', lhs_join_col=u'id', rhs_join_col='topic_id', nullable=True),
u'bar_topic': JoinInfo(table_name=u'bar_topic', rhs_alias=u'bar_topic', join_type=None, lhs_alias=None, lhs_join_col=None, rhs_join_col=None, nullable=False),
u'auth_user': JoinInfo(table_name=u'auth_user', rhs_alias=u'auth_user', join_type='LEFT OUTER JOIN', lhs_alias=u'bar_record', lhs_join_col='user_id', rhs_join_col=u'id', nullable=False)}
... qui changera la requête en ...
>>> print qs.query
SELECT "bar_topic"."id", "bar_topic"."name" FROM "bar_topic" LEFT OUTER JOIN "bar_record" ON ("bar_topic"."id" = "bar_record"."topic_id") WHERE "bar_record"."user_id" = 1
... cependant, cela n'est toujours pas utile, car la jointure correspondra toujours à une ligne, même si elle n'appartient pas à l'utilisateur correct, et la clause WHERE
la filtrera.
L'utilisation de values_list()
influence automatiquement le join_type
...
>>> qs = Topic.objects.filter(record__user_id=1).values_list('name', 'record__value')
>>> print qs.query
SELECT "bar_topic"."name", "bar_record"."value" FROM "bar_topic" LEFT OUTER JOIN "bar_record" ON ("bar_topic"."id" = "bar_record"."topic_id") WHERE "bar_record"."user_id" = 1
... mais souffre finalement du même problème.
Il y a, malheureusement, une limitation fondamentale dans les jointures générées par l'ORM, en ce qu'elles ne peuvent être que de la forme ...
(LEFT OUTER|INNER) JOIN <lhs_alias> ON (<lhs_alias>.<lhs_join_col> = <rhs_alias>.<rhs_join_col>)
... donc il n'y a vraiment aucun moyen d'atteindre le SQL souhaité, à part utiliser une requête brute.
Bien sûr, vous pouvez pirater des choses comme annotate()
et extra()
, mais elles généreront probablement des requêtes beaucoup moins performantes et sans doute pas plus lisibles que le SQL brut.
... et une alternative suggérée.
Personnellement, je voudrais simplement utiliser la requête brute ...
select * from bar_topic
left join bar_record
on bar_record.topic_id = bar_topic.id and bar_record.user_id = 1
... ce qui est assez simple pour être compatible avec tous les backends supportés par Django.
Voilà comment je le ferais. Deux requêtes, pas une:
class Topic(models.Model):
#...
@property
def user_value(self):
try:
return self.user_records[0].value
except IndexError:
#This topic does not have
#a review by the request.user
return None
except AttributeError:
raise AttributeError('You forgot to prefetch the user_records')
#or you can just
return None
#usage
topics = Topic.objects.all().prefetch_related(
models.Prefetch('record_set',
queryset=Record.objects.filter(user=request.user),
to_attr='user_records'
)
)
for topic in topics:
print topic.user_value
L'avantage est que vous obtenez tout l'objet Record
. Considérez donc une situation où vous voulez non seulement afficher le value
, mais le time-stamp
aussi.
Pour mémoire, je veux montrer une autre solution en utilisant .extra
. Je suis impressionné que personne ne l'ait mentionné, car il devrait produire les meilleures performances possibles.
topics = Topic.objects.all().extra(
select={
'user_value': """SELECT value FROM myapp_record
WHERE myapp_record.user_id = %s
AND myapp_record.topic_id = myapp_topic.id
"""
},
select_params=(request.user.id,)
)
for topic in topics
print topic.user_value
Les deux solutions peuvent être résumées dans une classe TopicQuerySet
personnalisée pour être réutilisables.
class TopicQuerySet(models.QuerySet):
def prefetch_user_records(self, user):
return self.prefetch_related(
models.Prefetch('record_set',
queryset=Record.objects.filter(user=request.user),
to_attr='user_records'
)
)
def annotate_user_value(self, user):
return self.extra(
select={
'user_value': """SELECT value FROM myapp_record
WHERE myapp_record.user_id = %s
AND myapp_record.topic_id = myapp_topic.id
"""
},
select_params=(user.id,)
)
class Topic(models.Model):
#...
objects = TopicQuerySet.as_manager()
#usage
topics = Topic.objects.all().annotate_user_value(request.user)
#or
topics = Topic.objects.all().prefetch_user_records(request.user)
for topic in topics:
print topic.user_value
Cette solution plus universelle inspirée de réponse de Trinchet fonctionne également avec d'autres bases de données:
>>> qs = Topic.objects.annotate(
... f=Max(Case(When(record__user=johnny, then=F('record__value'))))
... )
exemples de données
>>> print(qs.values_list('name', 'f'))
[(u'A', 1), (u'B', None), (u'C', 3)]
vérifier la requête
>>> print(qs.query) # formated and removed excessive double quotes
SELECT bar_topic.id, bar_topic.name,
MAX(CASE WHEN bar_record.user_id = 1 THEN bar_record.value ELSE NULL END) AS f
FROM bar_topic LEFT OUTER JOIN bar_record ON (bar_topic.id = bar_record.topic_id)
GROUP BY bar_topic.id, bar_topic.name
Avantages (par rapport aux solutions originales)
output_field
N'est nécessaire.values
ou values_list(*field_names)
sont utiles pour un GROUP BY
Plus simple, mais elles ne sont pas nécessaires.La jointure gauche peut être rendue plus lisible en écrivant une fonction:
from Django.db.models import Max, Case, When, F
def left_join(result_field, **lookups):
return Max(Case(When(then=F(result_field), **lookups)))
>>> Topic.objects.annotate(
... record_value=left_join('record__value', record__user=johnny),
... ).values_list('name', 'record_value')
Plus de champs de Record peuvent être ajoutés par la méthode anotate
aux résultats de cette façon avec des noms mnémoniques Nice.
Je suis d'accord avec d'autres auteurs qu'il peut être optimisé, mais la lisibilité compte .
[~ # ~] edit [~ # ~] : Le même résultat se produit si la fonction d'agrégation Max
est remplacée par Min
. Min et Max ignorent les valeurs NULL et peuvent être utilisés sur n'importe quel type, par ex. pour les cordes. L'agrégation est utile si la jointure gauche n'est pas garantie d'être unique. Si le champ est numérique, il peut être utile d'utiliser la valeur moyenne Avg
sur la jointure gauche.
topics = Topic.objects.raw('''
select * from bar_topic
left join (select topic_id as tid, value from bar_record where user_id = 1) AS subq
on tid = bar_topic.id
''')
Vous semblez connaître la réponse vous-même. Il n'y a rien de mal à utiliser une requête brute lorsque vous ne pouvez pas obtenir que la requête ORM se comporte exactement comme vous le souhaitez.
Un inconvénient principal des requêtes brutes est qu'elles ne sont pas mises en cache comme les requêtes ORM. Cela signifie que si vous parcourez le jeu de requêtes brut deux fois, la requête sera répétée. Un autre est que vous ne pouvez pas appeler .count () dessus.
Vous pouvez forcer l'ORM à utiliser LEFT OUTER JOIN EN définissant null=True
dans les clés étrangères. Faites-le avec les tableaux tels quels.
print Record.objects.filter(user_id=8).select_related('topic').query
Le résultat est
SELECT "bar_record"."id", "bar_record"."user_id", "bar_record"."topic_id", "bar_record"."value", "bar_topic"."id", "bar_topic"."name" FROM "bar_record"
INNER JOIN "bar_topic" ON ( "bar_record"."topic_id" = "bar_topic"."id" ) WHERE "bar_record"."user_id" = 8
Maintenant défini, null = True et effectuez la même requête ORM que ci-dessus. Le résultat est
SELECT "bar_record"."id", "bar_record"."user_id", "bar_record"."topic_id", "bar_record"."value", "bar_topic"."id", "bar_topic"."name" FROM "bar_record"
LEFT OUTER JOIN "bar_topic" ON ( "bar_record"."topic_id" = "bar_topic"."id" ) WHERE "bar_record"."user_id" = 8
Notez comment la requête est soudainement devenue LEFT OUTER JOIN
. Mais nous ne sommes pas encore sortis du bois car l'ordre des tables doit être inversé! Ainsi, à moins que vous ne puissiez restructurer vos modèles, un ORM LEFT OUTER JOIN peut ne pas être entièrement possible sans chaîner ou UNION que vous avez déjà essayé.