英文:
Scan a PostgreSQL field (of ARRAY type) into a slice of Go structs
问题
假设我有以下代码:
type User struct {
ID int64 `json:"id"`
Posts []Post `json:"posts"`
}
type Post struct {
ID int64 `json:"id"`
Text string `json:"text"`
}
SQL查询如下:
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)
SELECT user_id, ARRAY_AGG(ARRAY[post_id::text, post_text])
FROM temp
GROUP BY user_id
我想要的是将上述查询的结果扫描到一个User对象的切片中:
import (
"context"
"fmt"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/lib/pq"
)
var out []User
rows, _ := client.Query(context.Background(), query) // 为简洁起见,省略了错误处理
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, pq.Array(&u.Posts)); err != nil {
return
}
out = append(out, u)
}
可以预料到,上述代码会出现以下错误:
pq: cannot convert ARRAY[4][2] to StringArray
这是有道理的,但有没有办法将SQL的输出读取到我的用户切片中呢?
英文:
Let's say I have:
type User struct {
ID int64 `json:"id"
Posts []Post `json:"posts"
}
type Post struct {
ID int64 `json:"id"
Text string `json:"text"
}
The SQL query:
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)
SELECT user_id, ARRAY_AGG(ARRAY[post_id::text, post_text])
FROM temp
GROUP BY user_id
)
What I want is to scan rows from the query above into a slice of User objects:
import (
"context"
"fmt"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/lib/pq"
)
var out []User
rows, _ := client.Query(context.Background(), query) // No error handling for brevity
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, pq.Array(&u.Posts)); err != nil {
return
}
out = append(out, u)
}
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
实现中自己解析和解码它。
例如:
type PostList []Post
func (ls *PostList) Scan(src any) error {
var data []byte
switch v := src.(type) {
case string:
data = []byte(v)
case []byte:
data = v
}
// data变量保存了多维数组的值,类似于:{{"1","foo"}, {"2","bar"}, ...}
// 上面的示例很容易解析,但过于简单,
// 数组可能更复杂,因此解析起来可能更困难,
// 但如果你愿意,这并不是不可能的。
return nil
}
如果你想了解更多关于PostgreSQL数组表示语法的信息,请参阅:
一种不需要你实现一个解析器来处理PostgreSQL数组的方法是,使用JSON对象而不是PostgreSQL数组来构建并传递给array_agg
。这样做的结果将是一个以jsonb
为元素类型的一维数组。
SELECT user_id, array_agg(jsonb_build_object('id', post_id, 'text', post_text))
FROM temp
GROUP BY user_id
然后,自定义的sql.Scanner
实现只需要委托给lib/pq.GenericArray
,而另一个特定于元素的sql.Scanner
则委托给encoding/json
。
type PostList []Post
func (ls *PostList) Scan(src any) error {
return pq.GenericArray{ls}.Scan(src)
}
func (p *Post) Scan(src any) error {
var data []byte
switch v := src.(type) {
case string:
data = []byte(v)
case []byte:
data = v
}
return json.Unmarshal(data, p)
}
type User struct {
ID int64 `json:"id"`
Posts PostList `json:"posts"`
}
英文:
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:
type PostList []Post
func (ls *PostList) Scan(src any) error {
var data []byte
switch v := src.(type) {
case string:
data = []byte(v)
case []byte:
data = v
}
// The data var holds the multi-dimensional array value,
// something like: {{"1","foo"}, {"2","bar"}, ...}
// The above example is easy to parse but too simplistic,
// the array is likely to be more complex and therefore
// harder to parse, but not at all impossible if that's
// what you want.
return nil
}
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.
SELECT user_id, array_agg(jsonb_build_object('id', post_id, 'text', post_text))
FROM temp
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
.
type PostList []Post
func (ls *PostList) Scan(src any) error {
return pq.GenericArray{ls}.Scan(src)
}
func (p *Post) Scan(src any) error {
var data []byte
switch v := src.(type) {
case string:
data = []byte(v)
case []byte:
data = v
}
return json.Unmarshal(data, p)
}
type User struct {
ID int64 `json:"id"`
Posts PostList `json:"posts"`
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论