Postgres массив Golang структур - PullRequest
1 голос
/ 01 мая 2020

У меня есть следующая Go struct:

type Bar struct {
    Stuff string `db:"stuff"`
    Other string `db:"other"`
}

type Foo struct {
    ID    int    `db:"id"`
    Bars  []*Bar `db:"bars"`
}

Так что Foo содержит фрагмент Bar указателей. У меня также есть следующие таблицы в Postgres:

CREATE TABLE foo (
    id  INT
)

CREATE TABLE bar (
    id      INT,
    stuff   VARCHAR,
    other   VARCHAR,
    trash   VARCHAR
)

Я хочу LEFT JOIN в таблице bar и объединить ее в массив для хранения в структуре Foo. Я пробовал:

SELECT f.*,
ARRAY_AGG(b.stuff, b.other) AS bars
FROM foo f
LEFT JOIN bar b
ON f.id = b.id
WHERE f.id = $1
GROUP BY f.id

Но похоже, что подпись функции ARRAY_AGG неверна (function array_agg(character varying, character varying) does not exist). Есть ли способ сделать это без отдельного запроса к bar?

Ответы [ 2 ]

2 голосов
/ 01 мая 2020

Как вы уже знаете, array_agg принимает аргумент single и возвращает массив типа аргумента. Итак, если вы хотите, чтобы все столбцы строки были включены в элементы массива, вы можете просто передать ссылку на строку напрямую, например:

SELECT array_agg(b) FROM b

Если, однако, вы хотите включить только указание c столбцы в элементах массива, вы можете использовать конструктор ROW, например:

SELECT array_agg(ROW(b.stuff, b.other)) FROM b

Go Стандартная библиотека предоставляет готовые решения поддержка сканирования только скалярных значений. Для сканирования более сложных значений, таких как произвольные объекты и массивы, нужно либо искать сторонние решения, либо реализовывать свои собственные sql.Scanner.

Чтобы иметь возможность реализовать свои собственные sql.Scanner и правильно проанализировать массив postgres строк, для начала вам нужно знать, какой формат postgres использует для вывода значения, вы можете узнать это с помощью psql и некоторых запросов напрямую:

-- simple values
SELECT ARRAY[ROW(123,'foo'),ROW(456,'bar')];
-- output: {"(123,foo)","(456,bar)"}

-- not so simple values 
SELECT ARRAY[ROW(1,'a b'),ROW(2,'a,b'),ROW(3,'a",b'),ROW(4,'(a,b)'),ROW(5,'"','""')];
-- output: {"(1,\"a b\")","(2,\"a,b\")","(3,\"a\"\",b\")","(4,\"(a,b)\")","(5,\"\"\"\",\"\"\"\"\"\")"}

Как вы можете видеть, это может показаться довольно странным, но, тем не менее, его можно разобрать, синтаксис выглядит примерно так:

{"(column_value[, ...])"[, ...]}

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

ПРИМЕЧАНИЕ: могут существовать другие синтаксические правила, о которых я не знаю, которые необходимо учитывать при синтаксическом анализе. В дополнение к этому код ниже не обрабатывает NULL должным образом.

func parseRowArray(a []byte) (out [][]string) {
    a = a[1 : len(a)-1] // drop surrounding curlies

    for i := 0; i < len(a); i++ {
        if a[i] == '"' { // start of row element
            row := []string{}

            i += 2 // skip over current '"' and the following '('
            for j := i; j < len(a); j++ {
                if a[j] == '\\' && a[j+1] == '"' { // start of quoted column value
                    var col string // column value

                    j += 2 // skip over current '\' and following '"'
                    for k := j; k < len(a); k++ {
                        if a[k] == '\\' && a[k+1] == '"' { // end of quoted column, maybe
                            if a[k+2] == '\\' && a[k+3] == '"' { // nope, just escaped quote
                                col += string(a[j:k]) + `"`
                                k += 3    // skip over `\"\` (the k++ in the for statement will skip over the `"`)
                                j = k + 1 // skip over `\"\"`
                                continue  // go to k loop
                            } else { // yes, end of quoted column
                                col += string(a[j:k])
                                row = append(row, col)
                                j = k + 2 // skip over `\"`
                                break     // go back to j loop
                            }
                        }

                    }

                    if a[j] == ')' { // row end
                        out = append(out, row)
                        i = j + 1 // advance i to j's position and skip the potential ','
                        break     // go to back i loop
                    }
                } else { // assume non quoted column value
                    for k := j; k < len(a); k++ {
                        if a[k] == ',' || a[k] == ')' { // column value end
                            col := string(a[j:k])
                            row = append(row, col)
                            j = k // advance j to k's position
                            break // go back to j loop
                        }
                    }

                    if a[j] == ')' { // row end
                        out = append(out, row)
                        i = j + 1 // advance i to j's position and skip the potential ','
                        break     // go to back i loop
                    }
                }
            }
        }
    }
    return out
}

Попробуйте его на playground.

С чем-то подобным вам затем можно реализовать sql.Scanner для вашего Go среза баров.

type BarList []*Bar

func (ls *BarList) Scan(src interface{}) error {
    switch data := src.(type) {
    case []byte:
        a := praseRowArray(data)
        res := make(BarList, len(a))
        for i := 0; i < len(a); i++ {
            bar := new(Bar)
            // Here i'm assuming the parser produced a slice of at least two
            // strings, if there are cases where this may not be the true you
            // should add proper length checks to avoid unnecessary panics.
            bar.Stuff = a[i][0]
            bar.Other = a[i][1]
            res[i] = bar
        }
        *ls = res
    }
    return nil
}

Теперь, если вы измените тип поля Bars в типе Foo с []*Bar на BarList вы сможете напрямую передать указатель поля на (*sql.Row|*sql.Rows).Scan вызов:

rows.Scan(&f.Bars)

Если вы не хотите изменять тип поля, вы все равно можете заставить его работать, преобразовав указатель, когда он передается методу Scan:

rows.Scan((*BarList)(&f.Bars))

JSON

Реализация sql.Scanner для решения json, предложенного Генри Вуди будет выглядеть примерно так:

type BarList []*Bar

func (ls *BarList) Scan(src interface{}) error {
    if b, ok := src.([]byte); ok {
        return json.Unmarshal(b, ls)
    }
    return nil
}
1 голос
/ 01 мая 2020

Похоже, что вы хотите, чтобы bars был массивом объектов-баров, соответствующих вашим Go типам. Чтобы сделать это, вы должны использовать JSON_AGG вместо ARRAY_AGG, поскольку ARRAY_AGG работает только для отдельных столбцов и в этом случае выдает массив типа text (TEXT[]). JSON_AGG, с другой стороны, создает массив объектов json. Вы можете комбинировать это с JSON_BUILD_OBJECT, чтобы выбрать только те столбцы, которые вам нужны.

Вот пример:

SELECT f.*,
JSON_AGG(JSON_BUILD_OBJECT('stuff', b.stuff, 'other', b.other)) AS bars
FROM foo f
LEFT JOIN bar b
ON f.id = b.id
WHERE f.id = $1
GROUP BY f.id

Тогда вам придется справиться с демаршалированием json в Go, но кроме этого вы должны быть хороши в go.

Также обратите внимание, что Go будет игнорировать неиспользованные ключи для вас при демаршалировании json в структуре, так что вы можете упростить запрос, просто выбрав все поля в таблице bar, если хотите. Например:

SELECT f.*,
JSON_AGG(TO_JSON(b.*)) AS bars -- or JSON_AGG(b.*)
FROM foo f
LEFT JOIN bar b
ON f.id = b.id
WHERE f.id = $1
GROUP BY f.id

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

SELECT f.*,
COALESCE(
    JSON_AGG(TO_JSON(b.*)) FILTER (WHERE b.id IS NOT NULL),
    '[]'::JSON
) AS bars
FROM foo f
LEFT JOIN bar b
ON f.id = b.id
WHERE f.id = $1
GROUP BY f.id

Без FILTER, вы получите [NULL] для строк в foo, у которых нет соответствующих строк в bar, а FILTER дает вам NULL вместо этого, а затем просто используйте COALESCE для преобразования в пустой массив json.

...