登录
首页 >  Golang >  Go问答

Postgres 数组在 Golang 结构中的应用

来源:stackoverflow

时间:2024-03-02 10:54:23 415浏览 收藏

怎么入门Golang编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《Postgres 数组在 Golang 结构中的应用》,涉及到,有需要的可以收藏一下

问题内容

我有以下 go 结构:

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
)

我想在表 bar 上进行 left join 并将其聚合为一个数组以存储在结构 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(charactervaring,charactervarying)不存在)。有没有办法在不单独查询 bar 的情况下执行此操作?


解决方案


正如您所知,array_agg 接受一个单个参数并返回该参数类型的数组。因此,如果您希望行的所有列都包含在数组的元素中,您可以直接传递行引用,例如:

select array_agg(b) from b

但是,如果您只想在数组元素中包含特定列,则可以使用 ROW 构造函数,例如:

select array_agg(row(b.stuff, b.other)) from b

go 的标准库为仅扫描标量值提供开箱即用的支持。要扫描更复杂的值(例如任意对象和数组),必须寻找第 3 方解决方案,或实现他们自己的 sql.Scanner

为了能够实现您自己的 sql.scanner 并正确解析 postgres 行数组,您首先需要知道 postgres 使用什么格式输出值,您可以直接使用 psql 和一些查询来找到这一点: p>

-- 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 试试。

有了类似的东西,你就可以为你的 go 条形切片实现 sql.scanner

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
}

现在,如果您将 foo 类型中的 bars 字段的类型从 []*bar 更改为 barlist,您将能够直接将该字段的指针传递给 (*sql.row|*sql) .rows).scan 调用:

rows.scan(&f.bars)

如果您不想更改字段的类型,您仍然可以通过在将指针传递给 scan 方法时转换指针来使其工作:

rows.scan((*barlist)(&f.bars))

json

henry woody 建议的 json 解决方案的 sql.scanner 实现如下所示:

type barlist []*bar

func (ls *barlist) scan(src interface{}) error {
    if b, ok := src.([]byte); ok {
        return json.unmarshal(b, ls)
    }
    return nil
}

看起来你想要的是 bars 成为一个 bar 对象数组来匹配你的 go 类型。为此,您应该使用 json_agg 而不是 array_agg,因为 array_agg 仅适用于单列,并且在这种情况下会生成文本类型的数组 (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

然后你必须在 go 中处理对 json 的解组,但除此之外你应该可以开始了。

另请注意,在将 json 解组到结构体时,go 会忽略未使用的键,因此如果需要,您可以通过选择 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 ,然后只需使用 zqbc zqbcoalesce 转换为空 json 数组。

文中关于的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《Postgres 数组在 Golang 结构中的应用》文章吧,也可关注golang学习网公众号了解相关技术文章。

声明:本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>