как я могу получить набор запросов из нескольких уровней наборов / внешних ключей?

Если A содержит множество B и B, содержит множество C, то я ищу способ начать с A и в итоге получить набор запросов C.

Простой пример:

class Book(models.Model): name = models.CharField(max_length=64) class Page(models.Model): number = models.IntegerField() book = models.ForeignKey(Book) class Paragraph(models.Model): number = models.IntegerField() page = models.ForeignKey(Page) def query(): books = Book.objects.all() .prefetch_related('page_set', 'page_set__paragraph_set') for book in books: pages = book.page_set # I need to do something like this paragraphs = pages.all().paragraph_set # invalid # or paragraphs = book.page_set.select_related('paragraph_set') # valid, but paragraphs is still a QuerySet of Pages # this works, but results in one query for EVERY book, # which is what I need to avoid paragraphs = Paragraph.objects.filter(page__book=book) # do stuff with the book #... # do stuff with the paragraphs in the book # ... 

Как получить набор запросов из всего экземпляра книги?

Синтаксис названных аргументов для запросов Django поддерживает бесконечный уровень вложенности отношений между наборами / внешними ключами, но я не могу найти способ использовать сопоставление ORM, чтобы на самом деле получить связанный набор запросов из нисходящего.

И получение запроса с нижнего уровня отрицает преимущества prefetch_related / select_related .

Вышеприведенный пример - упрощенная версия того, что мне нужно сделать в моем приложении. База данных содержит тысячи «Книг», и любые n + 1 запросы следует избегать.

Я нашел вопрос об использовании предварительной выборки на нескольких уровнях, но ответ не касался того, как фактически получить выбранный набор запросов для использования.

Всего 2 ответа


После того, как вы сделали предварительную выборку, кажется, что единственный доступный доступ к дочерним записям - через all() . Кажется, что любые фильтры запускают другой запрос базы данных.

Короткий ответ на ваш вопрос обо всех параграфах книги состоит в том, чтобы использовать понимание списка с двумя уровнями:

    paragraphs = [paragraph
                  for page in book.page_set.all()
                  for paragraph in page.paragraph_set.all()]

Вот пример:

# Tested with Django 1.11.13
from __future__ import print_function
import os
import sys

import django
from django.apps import apps
from django.apps.config import AppConfig
from django.conf import settings
from django.core.files.base import ContentFile, File
from django.db import connections, models, DEFAULT_DB_ALIAS
from django.db.models.base import ModelBase

from django_mock_queries.mocks import MockSet, mocked_relations

NAME = 'udjango'


def main():
    setup()

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

    class Page(models.Model):
        number = models.IntegerField()
        book = models.ForeignKey(Book)

    class Paragraph(models.Model):
        number = models.IntegerField()
        page = models.ForeignKey(Page)

    syncdb(Book)
    syncdb(Page)
    syncdb(Paragraph)

    b = Book.objects.create(name='Gone With The Wind')
    p = b.page_set.create(number=1)
    p.paragraph_set.create(number=1)
    b = Book.objects.create(name='The Three Body Problem')
    p = b.page_set.create(number=1)
    p.paragraph_set.create(number=1)
    p.paragraph_set.create(number=2)
    p = b.page_set.create(number=2)
    p.paragraph_set.create(number=1)
    p.paragraph_set.create(number=2)

    books = Book.objects.all().prefetch_related('page_set',
                                                'page_set__paragraph_set')

    for book in books:
        print(book.name)
        paragraphs = [paragraph
                      for page in book.page_set.all()
                      for paragraph in page.paragraph_set.all()]
        for paragraph in paragraphs:
            print(paragraph.page.number, paragraph.number)


def setup():
    DB_FILE = NAME + '.db'
    with open(DB_FILE, 'w'):
        pass  # wipe the database
    settings.configure(
        DEBUG=True,
        DATABASES={
            DEFAULT_DB_ALIAS: {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': DB_FILE}},
        LOGGING={'version': 1,
                 'disable_existing_loggers': False,
                 'formatters': {
                    'debug': {
                        'format': '%(asctime)s[%(levelname)s]'
                                  '%(name)s.%(funcName)s(): %(message)s',
                        'datefmt': '%Y-%m-%d %H:%M:%S'}},
                 'handlers': {
                    'console': {
                        'level': 'DEBUG',
                        'class': 'logging.StreamHandler',
                        'formatter': 'debug'}},
                 'root': {
                    'handlers': ['console'],
                    'level': 'WARN'},
                 'loggers': {
                    "django.db": {"level": "DEBUG"}}})
    app_config = AppConfig(NAME, sys.modules['__main__'])
    apps.populate([app_config])
    django.setup()
    original_new_func = ModelBase.__new__

    @staticmethod
    def patched_new(cls, name, bases, attrs):
        if 'Meta' not in attrs:
            class Meta:
                app_label = NAME
            attrs['Meta'] = Meta
        return original_new_func(cls, name, bases, attrs)
    ModelBase.__new__ = patched_new


def syncdb(model):
    """ Standard syncdb expects models to be in reliable locations.

    Based on https://github.com/django/django/blob/1.9.3
    /django/core/management/commands/migrate.py#L285
    """
    connection = connections[DEFAULT_DB_ALIAS]
    with connection.schema_editor() as editor:
        editor.create_model(model)

main()

И вот конец вывода. Вы можете видеть, что он выполняет только один запрос для каждой таблицы.

2018-10-30 15:58:25[DEBUG]django.db.backends.execute(): (0.000) SELECT "udjango_book"."id", "udjango_book"."name" FROM "udjango_book"; args=()
2018-10-30 15:58:25[DEBUG]django.db.backends.execute(): (0.000) SELECT "udjango_page"."id", "udjango_page"."number", "udjango_page"."book_id" FROM "udjango_page" WHERE "udjango_page"."book_id" IN (1, 2); args=(1, 2)
2018-10-30 15:58:25[DEBUG]django.db.backends.execute(): (0.000) SELECT "udjango_paragraph"."id", "udjango_paragraph"."number", "udjango_paragraph"."page_id" FROM "udjango_paragraph" WHERE "udjango_paragraph"."page_id" IN (1, 2, 3); args=(1, 2, 3)
Gone With The Wind
1 1
The Three Body Problem
1 1
1 2
2 1
2 2

в ответ на Дон вы можете использовать объекты Prefetch для применения любых фильтров, например:

from django.db import models, connection

def query():
    paragraph_filter = models.Prefetch(
        'page_set__paragraph_set',
        Paragraph.objects.filter(number__gt=1))

    books = Book.objects.all().prefetch_related(
        'page_set', paragraph_filter)

    for book in books:
        for page in book.page_set.all():
            for paragraph in page.paragraph_set.all():
                print(paragraph)

    print(connection.queries)

Django заботится о том, чтобы все соответствующие объекты загружались в небольшое количество запросов (по одному на таблицу, поэтому вы получите три запроса)


Есть идеи?

10000