将一个 PostgreSQL 字段(ARRAY 类型)扫描到一个 Go 结构体切片中。

huangapple go评论116阅读模式
英文:

Scan a PostgreSQL field (of ARRAY type) into a slice of Go structs

问题

假设我有以下代码:

  1. type User struct {
  2. ID int64 `json:"id"`
  3. Posts []Post `json:"posts"`
  4. }
  5. type Post struct {
  6. ID int64 `json:"id"`
  7. Text string `json:"text"`
  8. }

SQL查询如下:

  1. WITH temp AS (SELECT u.id AS user_id, p.id AS post_id, p.text AS post_text FROM users u JOIN posts p ON u.id=p.user_id)
  2. SELECT user_id, ARRAY_AGG(ARRAY[post_id::text, post_text])
  3. FROM temp
  4. GROUP BY user_id

我想要的是将上述查询的结果扫描到一个User对象的切片中:

  1. import (
  2. "context"
  3. "fmt"
  4. "github.com/jackc/pgx/v4/pgxpool"
  5. "github.com/lib/pq"
  6. )
  7. var out []User
  8. rows, _ := client.Query(context.Background(), query) // 为简洁起见,省略了错误处理
  9. for rows.Next() {
  10. var u User
  11. if err := rows.Scan(&u.ID, pq.Array(&u.Posts)); err != nil {
  12. return
  13. }
  14. out = append(out, u)
  15. }

可以预料到,上述代码会出现以下错误:

pq: cannot convert ARRAY[4][2] to StringArray

这是有道理的,但有没有办法将SQL的输出读取到我的用户切片中呢?

英文:

Let's say I have:

  1. type User struct {
  2. ID int64 `json:"id"
  3. Posts []Post `json:"posts"
  4. }
  5. type Post struct {
  6. ID int64 `json:"id"
  7. Text string `json:"text"
  8. }

The SQL query:

  1. WITH temp AS (SELECT u.id AS user_id, p.id AS post_id, p.text AS post_text FROM users u JOIN posts p ON u.id=p.user_id)
  2. SELECT user_id, ARRAY_AGG(ARRAY[post_id::text, post_text])
  3. FROM temp
  4. GROUP BY user_id
  5. )

What I want is to scan rows from the query above into a slice of User objects:

  1. import (
  2. "context"
  3. "fmt"
  4. "github.com/jackc/pgx/v4/pgxpool"
  5. "github.com/lib/pq"
  6. )
  7. var out []User
  8. rows, _ := client.Query(context.Background(), query) // No error handling for brevity
  9. for rows.Next() {
  10. var u User
  11. if err := rows.Scan(&u.ID, pq.Array(&u.Posts)); err != nil {
  12. return
  13. }
  14. out = append(out, u)
  15. }

Pretty much expectedly, the code above fails with:

pq: cannot convert ARRAY[4][2] to StringArray

This makes sense, but is there a way to read the SQL output into my slice of users?

答案1

得分: 3

lib/pq不支持扫描任意类型的多维数组,例如结构体。如果你想要扫描这样的数组,你需要在自定义的sql.Scanner实现中自己解析和解码它。

例如:

  1. type PostList []Post
  2. func (ls *PostList) Scan(src any) error {
  3. var data []byte
  4. switch v := src.(type) {
  5. case string:
  6. data = []byte(v)
  7. case []byte:
  8. data = v
  9. }
  10. // data变量保存了多维数组的值,类似于:{{"1","foo"}, {"2","bar"}, ...}
  11. // 上面的示例很容易解析,但过于简单,
  12. // 数组可能更复杂,因此解析起来可能更困难,
  13. // 但如果你愿意,这并不是不可能的。
  14. return nil
  15. }

如果你想了解更多关于PostgreSQL数组表示语法的信息,请参阅:


一种不需要你实现一个解析器来处理PostgreSQL数组的方法是,使用JSON对象而不是PostgreSQL数组来构建并传递给array_agg。这样做的结果将是一个以jsonb为元素类型的一维数组。

  1. SELECT user_id, array_agg(jsonb_build_object('id', post_id, 'text', post_text))
  2. FROM temp
  3. GROUP BY user_id

然后,自定义的sql.Scanner实现只需要委托给lib/pq.GenericArray,而另一个特定于元素的sql.Scanner则委托给encoding/json

  1. type PostList []Post
  2. func (ls *PostList) Scan(src any) error {
  3. return pq.GenericArray{ls}.Scan(src)
  4. }
  5. func (p *Post) Scan(src any) error {
  6. var data []byte
  7. switch v := src.(type) {
  8. case string:
  9. data = []byte(v)
  10. case []byte:
  11. data = v
  12. }
  13. return json.Unmarshal(data, p)
  14. }
  15. type User struct {
  16. ID int64 `json:"id"`
  17. Posts PostList `json:"posts"`
  18. }
英文:

Scanning of multi-dimensional arrays of arbitrary types, like structs, is not supported by lib/pq. If you want to scan such an array you'll have to parse and decode it yourself in a custom sql.Scanner implementation.

For example:

  1. type PostList []Post
  2. func (ls *PostList) Scan(src any) error {
  3. var data []byte
  4. switch v := src.(type) {
  5. case string:
  6. data = []byte(v)
  7. case []byte:
  8. data = v
  9. }
  10. // The data var holds the multi-dimensional array value,
  11. // something like: {{"1","foo"}, {"2","bar"}, ...}
  12. // The above example is easy to parse but too simplistic,
  13. // the array is likely to be more complex and therefore
  14. // harder to parse, but not at all impossible if that's
  15. // what you want.
  16. return nil
  17. }

If you want to learn more about the PostgreSQL array representation syntax, see:


An approach that does not require you to implement a parser for PostgreSQL arrays would be to build and pass JSON objects, instead of PostgreSQL arrays, to array_agg. The result of that would be a one-dimensional array with jsonb as the element type.

  1. SELECT user_id, array_agg(jsonb_build_object('id', post_id, 'text', post_text))
  2. FROM temp
  3. GROUP BY user_id

Then the implementation of the custom sql.Scanner just needs to delegate to lib/pq.GenericArray and another, element-specific sql.Scanner, would delegate to encoding/json.

  1. type PostList []Post
  2. func (ls *PostList) Scan(src any) error {
  3. return pq.GenericArray{ls}.Scan(src)
  4. }
  5. func (p *Post) Scan(src any) error {
  6. var data []byte
  7. switch v := src.(type) {
  8. case string:
  9. data = []byte(v)
  10. case []byte:
  11. data = v
  12. }
  13. return json.Unmarshal(data, p)
  14. }
  15. type User struct {
  16. ID int64 `json:"id"`
  17. Posts PostList `json:"posts"`
  18. }

huangapple
  • 本文由 发表于 2022年9月5日 20:07:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/73609123.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定