【问题标题】:Postgres array of Golang structsGolang 结构的 Postgres 数组
【发布时间】:2020-05-01 22:49:48
【问题描述】:

我有以下 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
)

我想将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 的情况下做到这一点?

【问题讨论】:

  • 在结构中添加如何绑定数据的代码。

标签: postgresql go sqlx


【解决方案1】:

看起来你想要 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,然后只需使用@987654340 @ 转换为空的 json 数组。

【讨论】:

    【解决方案2】:

    您已经知道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 和一些直接查询来找到它:

    -- 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
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-12
      • 1970-01-01
      • 2015-03-23
      • 1970-01-01
      相关资源
      最近更新 更多