Django ORM - LEFT OUTER JOIN有两列吗?

时间:2018-04-30 18:14:24

标签: python sql django django-models django-orm

这是相关代码:

class Book(models.Model):
    name = models.CharField(max_length=50)

class BookNote(models.Model):
    text = models.CharField(max_length=50)
    book = models.ForeignKey(Book)
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    class Meta:
        unique_together = [('book', 'user'), ]

现在,对于网站中的特定用户:
我想查询所有书籍(所有表格)。而且,
对于每个书籍对象,如果用户具有书籍的书本注释 - 获取它,否则书签应该为空。

这就是我用SQL(工程)来做的事情:

SELECT book.name, booknote.text 
FROM book 
LEFT OUTER JOIN booknote ON 
(book.id = booknote.book_id AND booknote.user_id = {user_id_here})

这是我尝试过的,工作:

qs = Book.objects.filter(Q(booknote__user_id=user_id_here) | Q(booknote__isnull=True))

我检查qs.query,我明白了为什么 - Django使用WHERE子句按user_id进行过滤,所以我没有得到所有书籍。
如何使用django ORM执行相同的查询?没有原始的sql?

1 个答案:

答案 0 :(得分:4)

您的查询不起作用的原因是您要求使用您的用户笔记的书籍或根本没有笔记:这不包括只有其他用户有笔记的书籍。

我认为您所寻找的内容最适合作为注释。在django 2.0+下,您可以使用新的FilteredRelation来执行LEFT OUTER JOIN ON (... AND ...),但是我在执行此操作并在ORM中维护ForeignKey时遇到了麻烦;您必须使用其他注释重新导出所需的字段。

q =  Book.objects.all().annotate(
        usernote=FilteredRelation('booknote', condition=Q(booknote__user=USER_ID)),
        usernote_text=F('usernote__text'),
        usernote_id=F('usernote'),
        )

结果查询:

SELECT "books_book"."id", "books_book"."name", usernote."text" AS "usernote_text", usernote."id" AS "usernote_id" FROM "books_book" LEFT OUTER JOIN "books_booknote" usernote ON ("books_book"."id" = usernote."book_id" AND (usernote."user_id" = <USER_ID>))

如果您仍在使用1.11,则可以使用Prefetch对象或case-when annotation 获得相同的结果(但性能和查询不同)。

在models.py中:

class Book(models.Model):
     # SNIP
     @property
     def usernote(self):
         # raises an error if not prefetched
         try:
             return self._usernote[0] if self._usernote else None
         except AttributeError:
             raise Exception("Book.usernote must be prefetched with prefetch_related(Book.usernote_prefetch(user)) before use")

     @staticmethod
     def usernote_prefetch(user):
         return Prefetch(
             'booknote_set',
             queryset=BookNote.objects.filter(user=user)
             to_attr='_usernote'
         )

通过您的查询:

    q = Book.objects.all().prefetch_related(Book.usernote_prefetch(USER))

完整tests.py

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.test import TestCase
from django.db.models import *
from books.models import Book, BookNote
from django.contrib.auth import get_user_model

class BookTest(TestCase):
    def setUp(self):
        User = get_user_model()
        self.u1 = User.objects.create(username="U1")
        self.u2 = User.objects.create(username="U2")
        self.b1 = Book.objects.create(name="B1")  # Has no notes
        self.b2 = Book.objects.create(name="B2")  # Has a note for U1 and U2
        self.b3 = Book.objects.create(name="B3")  # Has a note for just U2
        self.n1 = BookNote.objects.create(text="N1", book=self.b2, user=self.u1)
        BookNote.objects.create(text="N2", book=self.b2, user=self.u2)
        BookNote.objects.create(text="N3", book=self.b1, user=self.u2)

    def test_on_multiple(self):
        q =  Book.objects.all().annotate(
                usernote=FilteredRelation('booknote', condition=Q(booknote__user=self.u1)),
                usernote_text=F('usernote__text'),
                usernote_id=F('usernote'),
                ).order_by('id')
        print(q.query)
        self.assertEqual(q.count(), Book.objects.count())
        self.assertIsNone(q[0].usernote_text)
        self.assertEqual( q[1].usernote_text, self.n1.text)
        self.assertIsNone(q[2].usernote_text)

    def test_on_multiple_prefetch(self):
        @property
        def usernote(self):
            return self._usernote[0] if self._usernote else None
        Book.usernote = usernote
        q = Book.objects.all().prefetch_related(Prefetch(
            'booknote_set',
            queryset=BookNote.objects.filter(user=self.u1),
            to_attr='_usernote'
            )).order_by('id')

        self.assertEqual(q.count(), Book.objects.count())
        self.assertIsNone(q[0].usernote)
        self.assertEqual( q[1].usernote.text, self.n1.text)
        self.assertIsNone(q[2].usernote)