Правильное извлечение массивов JSON из поля JSONB - PullRequest
1 голос
/ 25 июня 2019

Из таблицы в PostgreSQL 10 я пытаюсь соединить все элементы массива в нескольких дочерних элементах одного и того же поля jsonb с их родительской строкой, что-то вроде этого вопроса или этого . Но я делаю ошибку в JOIN, так что вместо того, чтобы получать отдельные элементы массива, я заключаю отдельные элементы массива в один элемент массива.

Вот сокращенное определение таблицы:

CREATE TABLE public.worker_customformstore (
    id integer NOT NULL DEFAULT nextval('worker_customformstore_id_seq'::regclass),
    created_on timestamp with time zone NOT NULL,
    store jsonb,
    schema_id integer NOT NULL,
    polymorphic_ctype_id integer,
    pdf_key character varying(100) COLLATE pg_catalog."default" NOT NULL,
    last_updated timestamp with time zone
)

и пример значения для поля store:

'{"Subcontractor Use": {
        "labor": [
            {
                "note": null,
                "hours": {
                    "dt": null,
                    "ot": null,
                    "st": 1,
                    "pdt": null,
                    "pot": null
                },
                "employee": {
                    "id": 456,
                    "trade": "XXX",
                    "is_active": true,
                    "last_name": "Uknow",
                    "first_name": "Noone",
                    "company_supplied_id": "456"
                },
                "external subcontractor": false
            },
            {
                "note": null,
                "hours": {
                    "dt": null,
                    "ot": null,
                    "st": 8,
                    "pdt": null,
                    "pot": null
                },
                "employee": {
                    "id": 123,
                    "trade": "",
                    "member": null,
                    "is_active": true,
                    "last_name": "Guy",
                    "user_role": "WORKER",
                    "first_name": "Some",
                    "company_supplied_id": "123"
                },
                "external subcontractor": false
            }
        ],
        "Equipment": [
            {
                "note": null,
                "hours": {
                    "idle": null,
                    "over": null,
                    "running": 8
                },
                "quantity": 1,
                "equipment": {
                    "id": 6243,
                    "status": "Rented",
                    "project": "8399",
                    "category": "XXXXX",
                    "caltrans_id": "00-20",
                    "description": "19",
                    "equipment_id": "Scissor",
                    "idle_time_price": 0,
                    "over_time_price": 0,
                    "running_time_price": 0
                }
            }
        ]
    }
}'

Мой упрощенный запрос выглядит так:

SELECT 
cufstore.id, 
CASE
    WHEN labor IS NOT DISTINCT FROM NULL THEN
    0
    WHEN (jsonb_array_elements(labor) -> 'hours' ->> 'st') = '' THEN
    0
    ELSE
    COALESCE((jsonb_array_elements(labor) -> 'hours' ->> 'st')::numeric, 0)
END
-- more stuff here ...
as total_hours,

CASE
    WHEN labor IS NOT DISTINCT FROM NULL THEN
    0
    ELSE
    COALESCE(jsonb_array_length(cufstore.store -> 'Subcontractor Use' -> 'labor'), 0)
END as total_workers,

labor, equipment

FROM public.worker_customformstore AS cufstore
...

LEFT OUTER JOIN LATERAL 
    (SELECT
        jsonb_array_elements(jsonb_strip_nulls(cufstore.store -> 'Subcontractor Use' -> 'labor'))
        WHERE cufstore.store -> 'Subcontractor Use' ->> 'labor' IS NOT NULL
    ) labor on true

LEFT OUTER JOIN LATERAL 
    (SELECT
        jsonb_array_elements(jsonb_strip_nulls(cufstore.store -> 'Subcontractor Use' -> 'Equipment'))
        WHERE cufstore.store -> 'Subcontractor Use' ->> 'Equipment' IS NOT NULL
    ) equipment on true

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

Я думаю, что я хочу что-то вроде:

LEFT OUTER JOIN LATERAL 
    jsonb_array_elements(jsonb_strip_nulls(cufstore.store -> 'Subcontractor Use' -> 'labor')) labor
    ON jsonb_typeof(labor) = 'array'

Но при попытке получить это cannot extract elements from a scalar, когда данные NULL или выглядят неправильно.

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

("{""hours"": {""running"": 8}, ""quantity"": 1, . . .}")

и я бы хотел иметь возможность спросить о equipment -> 'hours' ->> 'running' без необходимости оборачивать его в jsonb_array_elements(equipment). Нужно ли это делать или я случайно добавляю скобки в начале и конце значения столбца?

1 Ответ

1 голос
/ 28 июня 2019

Неясно, как связаны элементы двух вложенных массивов JSON "labor" и "Equipment". Из вашего примера кажется, что "Equipment" имеет только один элемент, а оболочка массива - просто шум ...

К сожалению, есть также вложенный ключ "equipment", который легко спутать с другим.

Я тоже в неведении, какова точная цель.

Как бы то ни было, после устранения большого количества шума и ненужных осложнений, это может быть близко к тому, что вы ищете:

SELECT s.id
     , COALESCE((NULLIF(labor->'hours'->>'st', ''))::numeric, 0) AS total_hours
     , CASE WHEN labor IS NULL THEN 0
            ELSE COALESCE(jsonb_array_length(s.store->'Subcontractor Use'->'labor'), 0)
       END AS total_workers
     , s.store #>> '{Subcontractor Use, Equipment, 0, hours, running}' AS equipment_hours
     , labor
FROM   worker_customformstore s
LEFT   JOIN jsonb_array_elements(s.store->'Subcontractor Use'->'labor') labor ON true;

дБ <> скрипка здесь

Примечания

Это длинное выражение:

CASE
    WHEN labor IS NOT DISTINCT FROM NULL THEN
    0
    WHEN (jsonb_array_elements(labor) -> 'hours' ->> 'st') = '' THEN
    0
    ELSE
    COALESCE((jsonb_array_elements(labor) -> 'hours' ->> 'st')::numeric, 0)
END

сводится к:

COALESCE((NULLIF(labor -> 'hours' ->> 'st', ''))::numeric, 0)
  • Не применять jsonb_array_elements() в другой раз, это уже сделано в боковом подзапросе.

  • labor IS NOT DISTINCT FROM NULL - это то же самое, что и labor IS NULL, но нам и не нужно, поскольку более поздние COALESCE все равно это делают.

  • При использовании NULLIF нам не нужно CASE с другой ветвью вообще.

Предполагая, что есть только один элемент во вложенном массиве JSON "Equipment", мы можем получить доступ к equipment_hours напрямую s.store #>> '{Subcontractor Use, Equipment, 0, hours, running}'. Если предположение не выполняется, вам придется сделать больше (и объяснить больше).


Обращаясь к Ваш комментарий

Если store -> 'Subcontractor Use' -> 'labor' не является вложенным массивом JSON, а, например, вместо этого скаляр, вы получите сообщение об ошибке, как вы прокомментировали:

ERROR: cannot extract elements from a scalar

дБ <> скрипка здесь

Вы можете избежать исключения с помощью вложенного CASE, например:

...
LEFT   JOIN jsonb_array_elements(
          <b>CASE WHEN jsonb_typeof(s.store -> 'Subcontractor Use' -> 'labor') = 'array'
               THEN              s.store -> 'Subcontractor Use' -> 'labor'
          END</b>) labor ON true;

дБ <> скрипка здесь

Возможно, вы захотите сделать больше, чтобы вернуть альтернативные значения для регистра ...

...