Бесконечное самосоединение Postgres

Итак, у меня есть статья и «комментарии» к статье.

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

Быстрый макет того, как выглядят столы

Comments(id, news_id, user_id, body, likes)

Replies(id, parent_id) --> id here is = Comments.id

User(id, username, password)

News(id, title, body, image)

Есть ли способ запросить базу данных Postgres, чтобы получить результат чего-то вроде

Таким образом, все внутри таблицы Replies с нулевым parent_id является «основным» комментарием (он же не является ответом). Мне бы хотелось, если это возможно, если поля children заполнялись внутри себя (т.е. ответ ответа)

Возможно ли это даже с Postgres? Или я должен получать все Replies, соединяя их с Comments, а затем перебирать каждый, пытаясь найти правильное назначение?

Кстати, я использую GoLang для своего бэкэнда и пакет Gorm для доступа к моей базе данных postgres.

EDIT: я использую этот запрос

with recursive commentss as (
  select r.id, r.parent, array[r.id] as all_parents, 
         c.body, u.username 
    from replies r 
          inner join comments c 
                  on c.id = r.id 
                join users u 
                  on u.id = c.user_refer 
   where (parent <> '') IS NOT TRUE 
   union all 
  select r.id, r.parent, c.all_parents || r.id, 
         co.body, u.username 
    from replies r 
          join comments co 
            on co.id = r.id 
          join users u 
            on u.id = co.user_refer 
          join commentss c 
            on r.parent = c.id 
               and r.id <> ALL (c.all_parents)
  ) 
   select * from commentss order by all_parents;

Что приводит к:

введите здесь описание изображения

Что на шаг ближе... однако мне нужно, чтобы объект JSON возвращался в виде

comments: [
  {
    comment_id: ...,
    username: ...,
    comment_body: ....,
    comment_likes: ....,
    children: [...]
  },
  {
    .....
  }
]

Где первыми элементами внутри объекта comments будут комментарии, которые НЕ являются ответом, а поле children должно быть заполнено отвеченными комментариями.. и комментарии внутри children также должны иметь свои children ответы на этот ответ.


person Mado Baker    schedule 04.10.2018    source источник
comment
Примеры данных и желаемые результаты действительно помогут.   -  person Gordon Linoff    schedule 04.10.2018
comment
Это будет зависеть от вашей версии. Исследуйте CTE, так как этот тип рекурсивного запроса — это именно то, как вы бы сделали этот запрос, если ваша версия поддерживает их.   -  person TomC    schedule 04.10.2018
comment
@Tomc Это максимум, что я мог придумать, используя CTE .. есть ли лучший способ подойти к этому или JSON с sql - плохая идея?   -  person Mado Baker    schedule 04.10.2018
comment
@GordonLinoff PING; более ясно с этим редактированием?   -  person Mado Baker    schedule 04.10.2018
comment
@MadoBaker Да, но, пожалуйста, предоставьте некоторые данные о самоле, которые мы можем скопировать. Итак, мы можем написать запрос. В противном случае мы должны написать это без возможности проверить это   -  person S-Man    schedule 04.10.2018
comment
По крайней мере, вы получаете данные. превращая его во вложенный json, я не знаю. Недостаточно знаком с Postgressql, чтобы помочь. Извините, но надеюсь, что кто-то еще может помочь с этим.   -  person TomC    schedule 04.10.2018
comment
Я немного смущен. На скриншоте вы выбираете родителей, в JSON вы хотите детей. Что вы ищете? Если вам нужны дети, вам нужны прямые дети или все более глубокие дети в списке?   -  person S-Man    schedule 04.10.2018
comment
Если мы считаем, что ответы являются комментариями к комментариям, и объединим эти таблицы с двумя дополнительными полями parent_id и node_path, этого можно добиться с помощью ltree в postgres. Взгляните на это Используйте ltreee + plv8 для получения иерархических записей в формате json. Если вы столкнулись с трудностями, прокомментируйте.   -  person Arun    schedule 04.10.2018


Ответы (1)


Надеясь, что это ваш ожидаемый результат. (здесь я сделал нечто подобное: https://stackoverflow.com/a/52076212/3984221)

демонстрация: db‹>скрипка

Таблица comments:

id  body          user_id  likes  
--  ------------  -------  -----  
a   foo           1        1      
b   foofoo        1        232    
c   foofoofoo     1        23232  
d   fooFOO        1        53     
e   cookies       1        864    
f   bar           1        44     
g   barbar        1        54     
h   barBAR        1        222    
i   more cookies  1        1      

Таблица replies

id  parent_id  
--  ---------  
a   (null)     
b   a          
c   b          
d   a          
e   (null)     
f   (null)     
g   f          
h   f          
i   (null)     

Результат:

{
    "comments": [{
        "children": [],
        "username": "Mike Tyson",
        "comment_id": "i",
        "comment_body": "more cookies",
        "comment_likes": 1
    },
    {
        "children": [{
            "children": [],
            "username": "Mike Tyson",
            "comment_id": "b",
            "comment_body": "foofoo",
            "comment_likes": 232
        },
        {
            "children": [{
                "children": [],
                "username": "Mike Tyson",
                "comment_id": "c",
                "comment_body": "foofoofoo",
                "comment_likes": 23232
            }],
            "username": "Mike Tyson",
            "comment_id": "d",
            "comment_body": "fooFOO",
            "comment_likes": 53
        }],
        "username": "Mike Tyson",
        "comment_id": "a",
        "comment_body": "foo",
        "comment_likes": 1
    },
    {
        "children": [],
        "username": "Mike Tyson",
        "comment_id": "e",
        "comment_body": "cookies",
        "comment_likes": 864
    },
    {
        "children": [{
            "children": [],
            "username": "Mike Tyson",
            "comment_id": "g",
            "comment_body": "barbar",
            "comment_likes": 54
        },
        {
            "children": [],
            "username": "Mike Tyson",
            "comment_id": "h",
            "comment_body": "barBAR",
            "comment_likes": 222
        }],
        "username": "Mike Tyson",
        "comment_id": "f",
        "comment_body": "bar",
        "comment_likes": 44
    }]
}

Запрос:

Рекурсия:

WITH RECURSIVE parent_tree AS (
    SELECT 
        id, 
        NULL::text[] as parent_id,
        array_append('{comments}'::text[], (row_number() OVER ())::text) as path, 
        rc.children  
    FROM replies r
    LEFT JOIN LATERAL (SELECT parent_id, ARRAY_AGG(id) as children FROM replies WHERE parent_id = r.id GROUP BY parent_id) rc ON rc.parent_id = r.id
    WHERE r.parent_id IS NULL 

    UNION

    SELECT 
        r.id, 
        array_append(pt.parent_id, r.parent_id), 
        array_append(array_append(pt.path, 'children'), (row_number() OVER (PARTITION BY pt.parent_id))::text),
        rc.children      
    FROM parent_tree pt
    JOIN replies r ON r.id = ANY(pt.children)
    LEFT JOIN LATERAL (SELECT parent_id, ARRAY_AGG(id) as children FROM replies WHERE parent_id = r.id GROUP BY parent_id) rc ON rc.parent_id = r.id
), json_objects AS (
   SELECT c.id, jsonb_build_object('children', '[]'::jsonb, 'comment_id', c.id, 'username', u.name, 'comment_body', c.body, 'comment_likes', c.likes) as jsondata
   FROM comments c
   LEFT JOIN users u ON u.id = c.user_id
)
SELECT 
    parent_id, 
    path,
    jsondata
FROM parent_tree pt 
LEFT JOIN json_objects jo ON pt.id = jo.id
ORDER BY parent_id NULLS FIRST

Единственная часть рекурсии находится внутри CTE parent_tree. Здесь я ищу родителей и строю путь. Этот путь необходим для вставки данных json позже в нужное место.

Второй CTE (json_objects) создает объект json для каждого комментария с пустым массивом дочерних элементов, куда дочерние элементы можно вставить позже.

Соединение LATERAL ищет в таблице ответов дочерние элементы с текущим идентификатором и предоставляет массив с их идентификаторами.

Предложение ORDER BY в конце важно. При этом гарантируется, что все верхние узлы предшествуют нижним узлам (их дочерним элементам). В противном случае ввод в глобальный json-объект может впоследствии завершиться неудачно, потому что нужный родитель может не существовать в нужный момент.

Построение окончательного объекта JSON:

CREATE OR REPLACE FUNCTION json_tree() RETURNS jsonb AS $$
DECLARE
    _json_output jsonb;
    _temprow record;
BEGIN

    SELECT 
        jsonb_build_object('comments', '[]'::jsonb) 
    INTO _json_output;

    FOR _temprow IN
        -- <query above>
    LOOP
        SELECT jsonb_insert(_json_output, _temprow.path, _temprow.jsondata) INTO _json_output;
    END LOOP;

    RETURN _json_output;
END;
$$ LANGUAGE plpgsql;

Невозможно построить объект json в рекурсии, потому что в запросе объект jsondata не является глобальной переменной. Поэтому, если бы я добавил b в качестве дочернего в a в одной ветке рекурсии, его не было бы в другой ветке, где я бы добавил c в качестве дочернего.

Поэтому необходимо создать глобальную переменную. Это можно сделать в функции. С вычисленным путем и дочерними объектами очень просто собрать окончательный json вместе: пройтись по результирующему набору и добавить объект json в путь глобального объекта.

person S-Man    schedule 04.10.2018
comment
Извините, что возвращаюсь, но я пытался расшифровать его и не мог понять, но как правильно фильтровать результаты по comment.news_id? например, верните мне только те комментарии, которые имеют определенный comment.news_id - person Mado Baker; 05.10.2018
comment
Есть несколько способов добиться этого. Вы можете сделать фильтр в json_objects CTE и сделать INNER JOIN вместо LEFT JOIN (что можно было бы использовать и в этой версии). Это отфильтровало бы все ожидаемые результаты. Вы также можете присоединиться к comments в конце. Но наибольший прирост производительности будет заключаться в том, чтобы сделать это в обеих частях рекурсии: присоединиться к таблице comments и отфильтровать (но тогда вам придется сделать это дважды - в обеих частях рекурсии) - person S-Man; 05.10.2018
comment
Извините, @S-man, но я не могу понять это.. Я могу отфильтровать все детские комментарии и получить только детские комментарии к новостям, которые я хочу, выполнив это LEFT JOIN LATERAL (SELECT parent, ARRAY_AGG(replies.id) as children FROM replies INNER JOIN comments c ON c.id = r.id WHERE parent = r.id AND c.news_id='8gl98XAig' GROUP BY parent) в первый LEFT JOIN LATERAL однако, я m все еще получаю все комментарии, у которых parent установлено на NULL - person Mado Baker; 15.10.2018
comment
Пожалуйста, помогите мне с этим: возьмите мою скрипку (или новую) и добавьте колонку новостей в набор данных. Затем добавьте свой запрос. Запустите и опубликуйте новую ссылку на скрипку с объяснением желаемого вывода как «Редактировать» в свой вопрос. Тогда я могу проверить это правильно - person S-Man; 15.10.2018
comment
Итак, я получил запрос для фильтрации результатов, которые мне нужны, однако вывод цикла for дает мне только один результат? взгляните на эту скрипту: dbfiddle.uk/ - person Mado Baker; 16.10.2018
comment
Хммм, только что узнал, что если вы измените news_id на =1, вы получите желаемые результаты. Похоже, если существует только 1 исходный комментарий (комментарий без родителя), он не будет заполнен - person Mado Baker; 16.10.2018
comment
Разберись с проблемой! Объекты JSON начинают свой индекс с 0, а не с 1.. поэтому, когда вы строили поле path с их индексом, он всегда был на 1 выше, чем должен быть... минус row_number() OVER на 1 все исправляет!! Спасибо за код - person Mado Baker; 16.10.2018