Аннотировать набор запросов Django с левым внешним соединением? - PullRequest
26 голосов
/ 28 июня 2011

Скажите, у меня есть модель:

class Foo(models.Model):
    ...

и другая модель, которая в основном дает информацию для каждого пользователя о Foo:

class UserFoo(models.Model):
    user = models.ForeignKey(User)
    foo = models.ForeignKey(Foo)
    ...

    class Meta:
        unique_together = ("user", "foo")

Я хотел бы сгенерировать набор запросов Foo с, но аннотированный (необязательно) связанным UserFoo на основе user=request.user.

То есть, по сути, это LEFT OUTER JOIN on (foo.id = userfoo.foo_id AND userfoo.user_id = ...)

Ответы [ 9 ]

16 голосов
/ 28 июня 2011

Решение с raw может выглядеть как

foos = Foo.objects.raw("SELECT foo.* FROM foo LEFT OUTER JOIN userfoo ON (foo.id = userfoo.foo_id AND foo.user_id = %s)", [request.user.id])

Вам нужно будет изменить SELECT, чтобы включить дополнительные поля из userfoo, которые будут аннотированы для результирующих экземпляров Fooв наборе запросов.

9 голосов
/ 21 декабря 2015

Этот ответ может быть не совсем тем, что вы ищете, но так как он является первым результатом в Google при поиске "django annotate external join", поэтому я опубликую его здесь.

Примечание: проверено на Djang 1.7

Предположим, у вас есть следующие модели

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

class EarnedPoints(models.Model):
    points = models.PositiveIntegerField()
    user = models.ForgeinKey(User)

Чтобы получить общее количество пользовательских баллов, вы можете сделать что-то подобное

 User.objects.annotate(points=Sum("earned_points__points"))

это будет работатьно он будет не возвращать пользователей, у которых нет баллов, здесь нам нужно внешнее объединение без каких-либо прямых взломов или необработанного sql

Этого можно добиться, выполнив это

 users_with_points = User.objects.annotate(points=Sum("earned_points__points"))
 result = users_with_points | User.objects.exclude(pk__in=users_with_points)

Это будет переведено в OUTER LEFT JOIN, и все пользователи будут возвращены.пользователи, у которых нет баллов, будут иметь значение None в атрибуте своих баллов.

Надеюсь, это поможет

9 голосов
/ 18 октября 2012

Примечание: Этот метод не работает в Django 1.6+.Как объяснено в комментарии tcarobruce ниже , аргумент promote был удален как часть тикета # 19849: Очистка ORM .


Джангоне предоставляет полностью встроенный способ сделать это, но нет необходимости создавать полностью необработанный запрос.(Этот метод не работает для выбора * из UserFoo, поэтому я использую .comment в качестве примера поля для включения из UserFoo.)

QuerySet.extra() method позволяет нам добавлять условия к предложениям SELECT и WHERE нашего запроса.Мы используем это для включения полей из таблицы UserFoo в наши результаты и ограничиваем наши совпадения UserFoo для текущего пользователя.

results = Foo.objects.extra(
    select={"user_comment": "UserFoo.comment"},
    where=["(UserFoo.user_id IS NULL OR UserFoo.user_id = %s)"],
    params=[request.user.id]
)

Для этого запроса все еще нужна таблица UserFoo.Можно было бы использовать .extras(tables=...) для получения неявного INNER JOIN, но для OUTER JOIN нам нужно изменить внутренний объект запроса самостоятельно.

connection = (
    UserFoo._meta.db_table, User._meta.db_table,  # JOIN these tables
    "user_id",              "id",                 # on these fields
)

results.query.join(  # modify the query
    connection,      # with this table connection
    promote=True,    # as LEFT OUTER JOIN
)

Теперь мы можем оценить результаты.Каждый экземпляр будет иметь свойство .user_comment, содержащее значение из UserFoo или None, если оно не существует.

print results[0].user_comment

(Кредит этого сообщения в блоге отКолина Коупленда за то, что он показал мне, как делать НАРУЖНЫЕ СОЕДИНЕНИЯ.)

3 голосов
/ 01 мая 2013

Я наткнулся на эту проблему, которую не смог решить, не прибегая к необработанному SQL, но я не хотел переписывать весь запрос.

Ниже приведено описание того, как можно дополнить набор запросов с помощью внешнегоraw sql, без необходимости заботиться о реальном запросе, который генерирует набор запросов.

Вот типичный сценарий: у вас есть сайт, похожий на reddit, с моделью LinkPost и режимом UserPostVote, например:

class LinkPost(models.Model):
some fields....

class UserPostVote(models.Model):
    user = models.ForeignKey(User,related_name="post_votes")
    post = models.ForeignKey(LinkPost,related_name="user_votes")
    value = models.IntegerField(null=False, default=0)

где таблица userpostvote собирает голоса пользователей по сообщениям.Теперь вы пытаетесь отобразить главную страницу для пользователя с приложением нумерации страниц, но вы хотите, чтобы стрелки были красными для сообщений, за которые проголосовал пользователь.

Сначала вы получаете сообщения для страницы:

post_list = LinkPost.objects.all()
paginator = Paginator(post_list,25)
posts_page = paginator.page(request.GET.get('page'))

, так что теперь у вас есть QuerySet posts_page, сгенерированный пагинатором django, который выбирает записи для отображения.Как нам теперь добавить аннотацию голоса пользователя к каждому сообщению перед его отображением в шаблоне?

Вот где это сложно, и мне не удалось найти чистое решение ORM.select_related не позволит вам получать только голоса, соответствующие вошедшему в систему пользователю, а циклический просмотр постов будет выполнять групповые запросы вместо одного и делать все это в прямом смысле: мы не можем использовать набор запросов из приложения разбивки на страницы.

Итак, вот как я это делаю:

q1 = posts_page.object_list.query  # The query object of the queryset
q1_alias = q1.get_initial_alias()  # This forces the query object to generate it's sql
(q1str, q1param) = q1.sql_with_params() #This gets the sql for the query along with 
                                        #parameters, which are none in this example

теперь у нас есть запрос для набора запросов, и просто оберните его, добавив псевдоним и оставив внешнее соединение с ним:

q2_augment = "SELECT B.value as uservote, A.* 
from ("+q1str+") A LEFT OUTER JOIN reddit_userpostvote B 
ON A.id = B.post_id AND B.user_id = %s"
q2param = (request.user.id,)
posts_augmented = LinkPost.objects.raw(q2_augment,q1param+q2param)

вуаля!Теперь мы можем получить доступ к post.uservote для сообщения в расширенном наборе запросов.И мы просто попали в базу данных одним запросом.

3 голосов
/ 28 июня 2011

Два предложенных вами запроса достаточно хороши (без использования raw ()), этот тип запроса в настоящее время не представлен в ORM.

2 голосов
/ 28 июня 2011

Вы можете сделать это, используя django-queryset-transform для simonw, чтобы избежать жесткого кодирования необработанного SQL-запроса - код будет выглядеть примерно так:

def userfoo_retriever(qs):
    userfoos = dict((i.pk, i) for i in UserFoo.objects.filter(foo__in=qs))
    for i in qs:
        i.userfoo = userfoos.get(i.pk, None)

for foo in Foo.objects.filter(…).tranform(userfoo_retriever):
    print foo.userfoo

Этот подход имеетбыл достаточно успешным для этой необходимости и для эффективного извлечения значений M2M;количество ваших запросов будет не таким низким, но в некоторых базах данных ( кашель MySQL кашель ) выполнение двух более простых запросов часто может быть быстрее, чем в случае со сложными JOIN, и во многих случаяхЯ больше всего нуждался в том, чтобы он имел дополнительную сложность, которую было бы еще труднее взломать в выражении ORM.

1 голос
/ 16 февраля 2018

Для этого не нужно прибегать к extra или raw.

Следующее должно работать.

Foo.objects.filter(
    Q(userfoo_set__user=request.user) |
    Q(userfoo_set=None)  # This forces the use of LOUTER JOIN.
).annotate(
    comment=F('userfoo_set__comment'),
    # ... annotate all the fields you'd like to see added here.
)
1 голос
/ 25 августа 2016

Что касается внешних объединений: Как только у вас есть набор запросов qs из foo, который включает ссылку на столбцы из userfoo, вы можете повысить внутреннее соединение до внешнего объединения с помощью qs.query.promote_joins(["userfoo"])

0 голосов
/ 17 июня 2019

комментарий maparent поставил меня на правильный путь:

from django.db.models.sql.datastructures import Join

for alias in qs.query.alias_map.values():
  if isinstance(alias, Join):
    alias.nullable = True

qs.query.promote_joins(qs.query.tables)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...