Неверные результаты с `annotate` +` values` + `union` в Django - PullRequest
4 голосов
/ 06 марта 2020

Перейти к редактированию, чтобы увидеть более реальный пример кода, который не работает после изменения порядка запросов

Вот мои модели:

class ModelA(models.Model):
    field_1a = models.CharField(max_length=32)
    field_2a = models.CharField(max_length=32)


class ModelB(models.Model):
    field_1b = models.CharField(max_length=32)
    field_2b = models.CharField(max_length=32)

Теперь создайте 2 экземпляра каждый:

ModelA.objects.create(field_1a="1a1", field_2a="1a2")
ModelA.objects.create(field_1a="2a1", field_2a="2a2")
ModelB.objects.create(field_1b="1b1", field_2b="1b2")
ModelB.objects.create(field_1b="2b1", field_2b="2b2")

Если я сделаю запрос только для одной модели с аннотациями, я получу что-то вроде этого:

>>> ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a")).values("field1", "field2")
[{"field1": "1a1", "field2": "1a2"}, {"field1": "2a1", "field2": "2a2"}]

Это правильное поведение. Проблема начинается, когда я хочу получить объединение этих двух моделей:

# model A first, with annotate
query = ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a"))
# now union with model B, also annotated
query = query.union(ModelB.objects.all().annotate(field1=F("field_1b"), field2=F("field_2b")))
# get only field1 and field2
query = query.values("field1", "field2")

# the results are skewed:
assert list(query) == [
    {"field1": 1, "field2": "1a1"},
    {"field1": 1, "field2": "1b1"},
    {"field1": 2, "field2": "2a1"},
    {"field1": 2, "field2": "2b1"},
]

Утверждение проходит правильно, что означает, что результаты неверны. Кажется, что values() не соответствует имени переменной, он просто перебирает объект как в кортеже. Значение field1 фактически является идентификатором объекта, а field2 равно field1.

Это довольно легко исправить в таких простых моделях, но мои реальные модели довольно сложны, и у них другое количество полей. Как правильно их объединить?

EDIT

Ниже вы можете найти расширенный пример, который не работает независимо от порядка union() и values() - модели теперь немного больше, и кажется, что подсчет различных полей как-то сбивает с толку Django:

# models

class ModelA(models.Model):
    field_1a = models.CharField(max_length=32)
    field_1aa = models.CharField(max_length=32, null=True)
    field_1aaa = models.CharField(max_length=32, null=True)
    field_2a = models.CharField(max_length=32)
    extra_a = models.CharField(max_length=32)


class ModelB(models.Model):
    extra = models.CharField(max_length=32)
    field_1b = models.CharField(max_length=32)
    field_2b = models.CharField(max_length=32)
# test

ModelA.objects.create(field_1a="1a1", field_2a="1a2", extra_a="1extra")
    ModelA.objects.create(field_1a="2a1", field_2a="2a2", extra_a="2extra")
    ModelB.objects.create(field_1b="1b1", field_2b="1b2", extra="3extra")
    ModelB.objects.create(field_1b="2b1", field_2b="2b2", extra="4extra")

    values = ("field1", "field2", "extra")

    query = (
        ModelA.objects.all()
        .annotate(
            field1=F("field_1a"), field2=F("field_2a"), extra=F("extra_a")
        )
        .values(*values)
    )
    query = query.union(
        ModelB.objects.all()
        .annotate(field1=F("field_1b"), field2=F("field_2b"))
        .values(*values)
    )
# outcome

assert list(query) == [
        {"field1": "1a1", "field2": "1a2", "extra": "1extra"},
        {"field1": "2a1", "field2": "2a2", "extra": "2extra"},
        {"field1": "3extra", "field2": "1b1", "extra": "1b2"},
        {"field1": "4extra", "field2": "2b1", "extra": "2b2"},
    ]

Ответы [ 2 ]

1 голос
/ 16 марта 2020

После некоторой отладки и изучения исходного кода у меня появилась идея, почему это происходит. Что я собираюсь сделать, так это попытаться объяснить, почему выполнение annotate + values приводит к отображению id и в чем разница между двумя приведенными выше случаями.

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

1. annotate сначала, но получите values при запросе объединения

qs1 = ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a"))

Когда вы пишете что-то вроде этого, django получит все поля + аннотированные поля, поэтому результирующий запрос sql выглядит следующим образом:

select id, field_1a, field_2a, field_1a as field1, field_2a as field2 from ModelA

Итак, если у нас есть query, который является результатом:

qs = qs1.union(qs2)

результирующий sql для django выглядит следующим образом:

(select id, field_1a, field_2a, field_1a as field1, field_2a as field2 from ModelA)
UNION
(select id, field_1b, field_2b, field_1b as field1, field_2b as field2 from ModelB)

Давайте go глубже в том, как это sql генерируется. Когда мы делаем union, qs.query и combined_queries устанавливаются на *1026*, и результирующий sql генерируется с помощью , объединяющим sql отдельных запросов. Итак, в итоге:

qs.sql == qs1.sql UNION qs2.sql # in abstract sense

Когда мы делаем qs.values('field1', 'field2'), col_count в компиляторе устанавливается в 2, что является количеством полей. Как вы можете видеть, запрос на объединение, приведенный выше, возвращает 5 столбцов, но в окончательном возвращении компилятора каждая строка в результатах разбивается на с использованием col_count. Теперь этот results, содержащий только 2 столбца, передается обратно в ValuesIterable, где он отображает каждое имя в выбранных полях с результирующими столбцами. Вот как это приводит к неверным результатам.

2. annotate + values на отдельные запросы и затем выполните union

Теперь давайте посмотрим, что произойдет, когда annotate используется с values напрямую

qs1 = ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a")).values('field1', 'field2')

Получившийся sql это:

select field_1a as field1, field_2a as field2 from ModelA

Теперь, когда мы делаем объединение:

qs = qs1.union(qs2)

sql это:

(select field_1a as field1, field_2a as field2 from ModelA)
UNION
(select field_1b as field1, field_2b as field2 from ModelB)

Теперь, когда qs.values('field1', 'field2') выполняется, число столбцов, возвращаемых из запроса объединения, имеет 2 столбца, что совпадает с col_count, равным 2, и каждое поле сопоставляется с отдельными столбцами, дающими ожидаемый результат.


3 , Разное количество аннотаций и порядок полей

В OP есть сценарий, когда даже использование .values до union не дает правильных результатов. Причина в том, что в ModelB нет аннотации для поля extra.

Итак, давайте посмотрим на запросы, сгенерированные для каждой модели:

ModelA.objects.all()
        .annotate(
            field1=F("field_1a"), field2=F("field_2a"), extra=F("extra_a")
        )
        .values(*values)

SQL становится:

select field_1a as field1, field_2a as field2, extra_a as extra from ModelA

Для модели B:

ModelB.objects.all()
        .annotate(field1=F("field_1b"), field2=F("field_2b"))
        .values(*values)

SQL:

select extra, field_1b as field1, field_2b as field2 from ModelB

и объединение:

(select field_1a as field1, field_2a as field2, extra_a as extra from ModelA)
UNION
(select extra, field_1b as field1, field_2b as field2 from ModelB)

Поскольку аннотированные поля перечислены после реальных полей БД, extra из ModelB смешивается с field1 из ModelB. Чтобы получить правильные результаты, убедитесь, что порядок полей в сгенерированном SQL всегда правильный - с аннотацией или без нее. В этом случае я предлагаю также пометить extra на ModelB.

0 голосов
/ 06 марта 2020

Я копался в документах и должен признать, что не до конца понимал, почему ваш подход не сработал (по моему мнению, так и должно быть). Я считаю, что применение union к наборам запросов с разными именами полей, по-видимому, приводит к странным эффектам.

В любом случае применение значений перед объединением, похоже, дает правильные результаты:

query = ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a")).values('field1', 'field2')
query = query.union(ModelB.objects.all().annotate(field1=F("field_1b"), field2=F("field_2b")).values('field1', 'field2'))

Что приводит к этому набору запросов

[
    {'field1': '1a1', 'field2': '1a2'}, 
    {'field1': '1b1', 'field2': '1b2'}, 
    {'field1': '2a1', 'field2': '2a2'}, 
    {'field1': '2b1', 'field2': '2b2'}
]
...