Джанго ОРМ. Присоединение подзапроса

У меня есть таблица, которая содержит список некоторых веб-сайтов и таблицу со статистикой по ним.

class Site(models.Model):
    domain_name = models.CharField(
        max_length=256,
        unique=True,
    )


class Stats(models.Model):
    date = models.DateField()
    site = models.ForeignKey('Site')
    google_pr = models.PositiveIntegerField()

    class Meta:
        unique_together = ('site', 'date')

Я хочу видеть все сайты и статистику на конкретную дату. Если записи статистики на дату не существует, то выбор должен содержать только сайт.

Если я использую:

Site.objects.filter(stats__date=my_date)

Я не получу сайты, у которых нет записей для my_date в таблице stats. Потому что в этом случае SQL-запрос будет выглядеть следующим образом:

SELECT *
FROM site
LEFT OUTER JOIN stats ON site.id = stats.site_id
WHERE stats.date = 'my_date'

Условие запроса исключит записи с NULL-датами, а сайты без статистики не попадут в выборку.

В моем случае мне нужна таблица статистики присоединения, которая уже отфильтрована по дате:

SELECT *
FROM site
LEFT OUTER JOIN
  (SELECT *
   FROM stats
   WHERE stats.date = 'my-date') AS stats
ON site.id = stats.site_id

Как я могу перевести этот запрос в Django ORM?

Спасибо.


person psln    schedule 07.04.2014    source источник


Ответы (3)


В Django v2.0 используйте FilteredRelation.

Site.objects.annotate(
    t=FilteredRelation(
        'stats', condition=Q(stats__date='my-date')
).filter(t__google_pr__in=[...])
person Charmy    schedule 10.01.2018
comment
К сожалению, это не работает для вложенных отношений. Скажем, если бы у stats было само поле m2m, по которому вы хотели бы отфильтровать. Кто-нибудь знает, как с этим бороться? - person getup8; 16.11.2019
comment
Я пытался присоединиться к подзапросу, потому что делать группу по отдельности было намного быстрее. На самом деле, это было то, что у меня было несколько COUNT () FILTER (WHERE) с частью общих условий, что было медленным. Таким образом, FilteredRelation с общими критериями улучшил выполнение примерно в 60 раз. ???? - person Eric Darchis; 17.09.2020

У меня была аналогичная проблема, и я написал следующую служебную функцию для добавления левого внешнего соединения к набору подзапросов с использованием Django ORM.

Утилита получена из решения, данного для добавления пользовательского левого внешнего соединения к другой таблице (не подзапросу) с использованием Django ORM. Вот это решение: https://stackoverflow.com/a/37688104/2367394

Ниже приведена утилита и весь связанный с ней код:

from django.db.models.fields.related import ForeignObject
from django.db.models.options import Options
from django.db.models.sql.where import ExtraWhere
from django.db.models.sql.datastructures import Join


class CustomJoin(Join):
    def __init__(self, subquery, subquery_params, parent_alias, table_alias, join_type, join_field, nullable):
        self.subquery_params = subquery_params
        super(CustomJoin, self).__init__(subquery, parent_alias, table_alias, join_type, join_field, nullable)

    def as_sql(self, compiler, connection):
        """
        Generates the full
        LEFT OUTER JOIN (somequery) alias ON alias.somecol = othertable.othercol, params
        clause for this join.
        """
        params = []
        sql = []
        alias_str = '' if self.table_alias == self.table_name else (' %s' % self.table_alias)
        params.extend(self.subquery_params)
        qn = compiler.quote_name_unless_alias
        qn2 = connection.ops.quote_name
        sql.append('%s (%s)%s ON (' % (self.join_type, self.table_name, alias_str))
        for index, (lhs_col, rhs_col) in enumerate(self.join_cols):
            if index != 0:
                sql.append(' AND ')
            sql.append('%s.%s = %s.%s' % (
                qn(self.parent_alias),
                qn2(lhs_col),
                qn(self.table_alias),
                qn2(rhs_col),
            ))
        extra_cond = self.join_field.get_extra_restriction(
            compiler.query.where_class, self.table_alias, self.parent_alias)
        if extra_cond:
            extra_sql, extra_params = compiler.compile(extra_cond)
            extra_sql = 'AND (%s)' % extra_sql
            params.extend(extra_params)
            sql.append('%s' % extra_sql)
        sql.append(')')
        return ' '.join(sql), params

def join_to(table, subquery, table_field, subquery_field, queryset, alias):
    """
    Add a join on `subquery` to `queryset` (having table `table`).
    """
    # here you can set complex clause for join
    def extra_join_cond(where_class, alias, related_alias):
        if (alias, related_alias) == ('[sys].[columns]',
                                    '[sys].[database_permissions]'):
            where = '[sys].[columns].[column_id] = ' \
                    '[sys].[database_permissions].[minor_id]'
            children = [ExtraWhere([where], ())]
            return where_class(children)
        return None
    foreign_object = ForeignObject(to=subquery, from_fields=[None], to_fields=[None], rel=None)
    foreign_object.opts = Options(table._meta)
    foreign_object.opts.model = table
    foreign_object.get_joining_columns = lambda: ((table_field, subquery_field),)
    foreign_object.get_extra_restriction = extra_join_cond
    subquery_sql, subquery_params = subquery.query.sql_with_params()
    join = CustomJoin(
        subquery_sql, subquery_params, table._meta.db_table,
        alias, "LEFT JOIN", foreign_object, True)

    queryset.query.join(join)

    # hook for set alias
    join.table_alias = alias
    queryset.query.external_aliases.add(alias)

    return queryset

join_to — это служебная функция, которую вы хотите использовать. Для вашего запроса вы можете использовать его следующим образом:

sq = Stats.objects.filter(date=my_date)
q = Site.objects.filter()
q = join_to(Site, sq, 'id', 'site_id', q, 'stats')

И следующий оператор напечатает запрос, аналогичный вашему примерному запросу (с подзапросом).

print q.query
person Debanshu Kundu    schedule 15.03.2017
comment
У кого-нибудь это работает в Django 3? - person Christopher Broderick; 08.02.2021

Посмотрите на это так: вы хотите увидеть статистику с сопутствующими данными сайта на определенную дату, что означает:

Stats.objects.filter(date=my_date).select_related('site')
person Suor    schedule 07.04.2014
comment
Спасибо, Суор. Но в этом случае Django ORM использует INNER JOIN для присоединения статистики к сайтам, а затем применяет фильтрацию по дате. Если в таблице статистики нет записи для определенного сайта и даты, то сайт не будет включен в результат. Думаю, есть только один способ решить проблему - присоединиться к отфильтрованной таблице статистики. - person psln; 07.04.2014
comment
Я не совсем понимаю тебя. Если таблица статистики не содержит записи, у вас нет даты, и строка не должна возвращаться. Если вы имеете в виду, что статистика может не иметь сайта и поэтому будет исключена при присоединении, то нет, это не так, как это работает в Django. В этом случае вы устанавливаете site = models.ForeignKey('Site', null=True), и Django выполняет для вас левое соединение для того же кода. - person Suor; 07.04.2014
comment
На сайте может не быть статистики на определенную дату. Мне нужно получить все сайты и статистику, которые существуют на эту дату. Ф.э. если таблица site содержит записи site_1 и site_2, таблица stats содержит только stats_1(site=site_1, date=my_date), я хочу получить [{site_1, stats_1}, {site_2, NULL}]. Но ваш код вернет только [{site_1, stats_1}]. - person psln; 07.04.2014
comment
О, понял. Вам, вероятно, потребуется 2 запроса для этого с использованием Django ORM, а затем соединить все вручную. Просто примечание: в грядущей версии Django 1.7 вы можете использовать для этого одно выражение: Site.objects.prefetch_related(Prefetch('stats_set', queryset=Stats.objects.filter(date=my_date))). - person Suor; 07.04.2014