Запрос с массивом JSON в WHERE в Postgresql - PullRequest
0 голосов
/ 14 мая 2018

У меня есть пользовательская модель со столбцом JSONB payment_info, который содержит следующий пример json:

{
    "customer_id": "cst_K5gCsCkKAU",
    "subscriptions": [
        {
            "status": "active",
            "external_id": "sub_3Q9Q4bP2zW"
        }
    ]
}

Я новичок с JSON-запросами, но создал следующее для db Postgres (PG), которые, похоже, работают: я ищу всех пользователей, у которых есть определенное значение external_id:

SELECT payment_info->'subscriptions' as Subscriptions
    FROM public."user"
    , jsonb_array_elements(payment_info->'subscriptions') as subs
    where (subs->>'external_id')::text = 'sub_3Q9Q4bP2zW'

Как мне сделать то же самое в SQLAlchemy? Я попробовал несколько думает, что я нашел в Интернете (SO), но это не работает. Я попробовал:

  1. JSONB Comparator

    query = misc.setup_query(db_session, User).filter(
        User.payment_info.comparator.contains(
            ('subscriptions', 'external_id') == payment_subscription_id))
    

    Это приводит к следующей ошибке:

    sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) operator does not exist: jsonb @> boolean
    LINE 3: WHERE "user".payment_info @> false
                                  ^
    HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
    
  2. json_contains Функция:

    from sqlalchemy import func
    query = misc.setup_query(db_session, User).filter(
        func.json_contains(User.payment_info,
                           payment_subscription_id,
                           ['subscriptions', 'external_id']))
    

    В результате:

    LINE 3: WHERE json_contains("user".payment_info, 'sub_QxyMEmU', ARRA...
               ^
    HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
    
  3. Путь к ключу:

    query = misc.setup_query(db_session, User).filter(
        User.payment_info['subscriptions', 'external_id'].astext == payment_subscription_id)
    

    Это приводит к пустому результату со следующим запросом:

    SELECT *
    FROM "user" 
    WHERE ("user".payment_info #>> %(payment_info_1)s) = %(param_1)s
    

Что я делаю не так, и как я могу заставить его работать? Кстати: мне нужно поставить индекс на external_id? (пока нет)

1 Ответ

0 голосов
/ 15 мая 2018

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

misc.setup_query(db_session, User).\
    select_from(
        User,
        func.jsonb_array_elements(User.payment_info['subscriptions']).
            alias('subs')).\
    filter(column('subs', type_=JSONB)['external_id'].astext == 'sub_3Q9Q4bP2zW')

, которое компилируется в

SELECT "user".id AS user_id, "user".payment_info AS user_payment_info 
FROM "user", jsonb_array_elements("user".payment_info -> %(payment_info_1)s) AS subs 
WHERE (subs ->> %(subs_1)s) = %(param_1)s

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

misc.setup_query(db_session, User).\
    filter(User.payment_info['subscriptions'].contains(
        [{'external_id': 'sub_3Q9Q4bP2zW'}]))

Обратите внимание, что требуется внешний список, так как он является частью «пути» для проверки.Используя ту же логику, вы можете опустить извлечение массива:

misc.setup_query(db_session, User).\
    filter(User.payment_info.contains(
        {'subscriptions': [{'external_id': 'sub_3Q9Q4bP2zW'}]}))

Приведенные выше @> с использованием подходов индексируются с использованием GIN index .1-й требует функционального индекса, потому что сначала он извлекает массив:

CREATE INDEX user_payment_info_subscriptions_idx ON "user"
USING GIN ((payment_info -> 'subscriptions'));

2-й требует индексации всего столбца payment_info jsonb.Создание индексов GIN можно выполнить в определениях модели SQLAlchemy с помощью специфичных для Postgresql параметров индекса :

class User(Base):
    ...

Index('user_payment_info_subscriptions_idx',
      User.payment_info['subscriptions'],
      postgresql_using='gin')

Почему различные попытки оказались безуспешными:

  1. Вы не должны напрямую обращаться к компаратору.Предоставляет операторы для типа.Кроме того, вы передаете contains() результат выражения

    ('subscriptions', 'external_id') == payment_subscription_id
    

    , который, скорее всего, ложен (зависит от того, что payment_subscription_id).То есть он оценивается в Python .

  2. В Postgresql нет функции json_contains() (в отличие от MySQL).Используйте оператор @>.

  3. У вас неверный путь.User.payment_info['subscriptions', 'external_id'].astext будет соответствовать что-то вроде {"subscriptions": {"external_id": "foo"}}, но в ваших данных subscriptions ссылается на массив.

...