简体   繁体   English

Django ORM-左外连接有两列?

[英]Django ORM - LEFT OUTER JOIN with two columns?

This is the relevant code: 这是相关代码:

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'), ]

Now, for a specific user in the website: 现在,对于网站中的特定用户:
I want to query all the books (all the table). 我想查询所有书籍(所有桌子)。 And, 和,
For each book object, If the user has a BookNote for the book - get it, otherwise booknote should be null. 对于每个书对象,如果用户具有该书的BookNote,请获取它,否则Booknote应该为null。

This is how I would do that with SQL (works): 这就是我使用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})

This is what I've tried, does not work: 这是我试过, 工作:

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

I examine qs.query and I see why - Django uses WHERE clause to filter by user_id, so I don't get all the books. 我检查了qs.query然后了解了原因qs.query使用WHERE子句按user_id进行过滤,因此我收不到所有的书。
How can I do the same query with django ORM? 如何使用django ORM执行相同的查询? Without raw sql? 没有原始SQL?

The reason your query doesn't work is you're expicitly asking for either Books with your user's notes or no notes at all: this excludes books where only other users have notes. 您的查询不起作用的原因是,您明确要求提供带有用户注释的书或根本没有注释的书:这不包括只有其他用户有注释的书。

I think what you're looking for is best performed as an annotation. 我认为您要寻找的内容最好作为注释执行。 Under django 2.0+, you can use the new FilteredRelation to perform a LEFT OUTER JOIN ON (... AND ...) , but I had trouble doing it and maintaining the ForeignKey in the ORM; 在django 2.0+下,您可以使用新的FilteredRelation来执行LEFT OUTER JOIN ON (... AND ...) ,但是我很难做到并在ORM中维护ForeignKey。 you'll have to re-export the fields you need with additional annotations. 您必须重新导出需要的字段以及其他注释。

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

Resulting query: 结果查询:

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>))

If you're using 1.11 still, you can get the same result (but less performance and different queries) with Prefetch objects or a case-when annotation . 如果仍在使用1.11,则通过Prefetch对象 或case-when注释 可以获得相同的结果(但性能较低,查询不同)。

In models.py: 在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'
         )

By your query: 根据您的查询:

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

Full tests.py : 完整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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM