Написание поиска кортежей с помощью Django ORM

Я пытаюсь написать поиск на основе кортежей с использованием синтаксиса Django ORM.

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

SELECT * FROM mytable WHERE (field_a,field_b) IN ((1,2),(3,4));

Я знаю, что могу достичь этого в django, используя дополнительное ключевое слово:

MyModel.objects.extra(
    where=["(field_a, field_b) IN %s"],
    params=[((1,2),(3,4))]
)

но ключевое слово "extra" в какой-то момент будет устаревшим в django, поэтому я бы хотел чистое ORM/django решение.

Исследуя Интернет, я нашел https://code.djangoproject.com/ticket/33015 и комментарий Саймона Шаретта, что-то вроде приведенного ниже фрагмента может быть в порядке, но я не могу заставить его работать.

from django.db.models import Func, lookups

class ExpressionTuple(Func):
    template = '(%(expressions)s)'
    arg_joiner = ","


MyModel.objects.filter(lookups.In(
    ExpressionTuple('field_a', 'field_b'),
    ((1,2),(3,4)),
))

Я использую Django 3.2, но я не ожидаю, что Django 4.x сделает здесь большую разницу. Мой бэкенд базы данных - posgresql, если это имеет значение.

Я могу предложить одно решение, которое будет строить запрос заранее с помощью Q и затем передавать его через функцию фильтрации:

q = Q()
for (item1, item2) in [(1,2),(3,4)]:
    q |= Q(field_one=item1, field_two=item2)

Mymodel.objects.filter(q)

Другим более надежным решением было бы следующее:

q = Q()

fields = ['field_one', 'field_two']

for item in [(1,2),(3,4)]:
   q |= Q(**dict(zip(fields, item)))

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

from django.db.models import Func, lookups

class Tuple(Func):
    function = '(%s)'

    def as_sql(self, compiler, connection):
        sql, params = super().as_sql(compiler, connection)
        if sql.endswith(',)'):
            sql = sql[:-2] + ')'
        return sql, params

MyModel.objects.filter((Func('field_a', function='(%s)'), Func('field_b', function='(%s)'))__in=[(1,2),(3,4)])

С этими изменениями результирующий SQL-запрос должен выглядеть примерно так:

SELECT * FROM mytable WHERE (field_a, field_b) IN ((1, 2), (3, 4))

Для справки и под впечатлением от предложения Акшая-Джайна, мне удалось написать кое-что, что работает:

from django.db.models import Func,Value

def ValueTuple(items):
    return tuple(Value(i) for i in items)

class Tuple(Func):
    function = ''

qs = (
    MyModel.objects
    .alias(a=Tuple('field_a', 'field_b'))
    .filter(a__in=ValueTuple([(1, 2), (3, 4)])
)

В результате получается sql-запрос вида

SELECT * FROM table WHERE (field_a,field_b) IN ((1,2),(3,4));

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

Я не делал никаких сравнительных тестов, чтобы сравнить его с фильтрацией Q-объектов.

Вернуться на верх