英文:
How to avoid Scan pair: <nil> *json.RawMessage errors on nullable JSON columns?
问题
几周前,我开始学习Go,并在学习基础知识的同时尝试构建一个简单的博客应用程序。
目前,我正在尝试使用database/sql
和github.com/lib/pq
包来获取和保存博客文章。我不喜欢在完全理解Go的本机行为和基础知识之前使用第三方包,如sqlx
或gorm
。
我的Post
结构如下所示:
type Post struct {
Id int
Title string
Body string
Tags json.RawMessage
}
在保存文章时,我的save()
函数可以正常工作:
func (p *Post) save() (int, error) {
const query = `INSERT INTO post (title, body, tags) VALUES($1, $2, $3) RETURNING id`
db := GetDB()
var postid int
err := db.QueryRow(query, p.Title, p.Body, p.Tags).Scan(&postid)
return postid, err
}
为了读取最新的文章,我编写了一个小函数:
func getLatestPosts(page int) (*[]Post, error) {
const query = `SELECT id, title, body, tags FROM posts ORDER BY id DESC LIMIT 10`
var items []Post
db := GetDB()
rows, err := db.Query(query)
if err != nil {
return nil, err
}
for rows.Next() {
var item Post
if err := rows.Scan(&item.Id, &item.Title, &item.Body, &item.Tags); err != nil {
log.Fatal(err)
}
items = append(items, item)
}
return &items, err
}
这个函数也可以正常工作,直到遇到一个tags
列为null的文章行,此时我会遇到以下错误:
2015/04/16 21:53:04 sql: Scan error on column index 4: unsupported driver -> Scan pair:
-> *json.RawMessage
我的问题是,在扫描结果集时,如何正确处理nullable json
列?这个错误与lib/pq
有关吗?
我的数据库模式如下:
CREATE TABLE post (
"id" int4 NOT NULL DEFAULT nextval('post_id_seq'::regclass),
"title" varchar(255) NOT NULL COLLATE "default",
"body" text COLLATE "default",
"tags" json,
PRIMARY KEY ("id") NOT DEFERRABLE INITIALLY IMMEDIATE
);
以下是一个已经保存(非空)的tags
字段内容示例:
{
"black-and-white" : "Black and White",
"the-godfather" : "The Godfather"
}
有什么想法吗?
英文:
Several weeks ago I started to learn go and trying to build a simple blogging application while learning the basics.
Currently I'm trying to fetch and persist blog posts using database/sql
and github.com/lib/pq
packages. I dont prefer utilizing 3rd party packages like sqlx
or gorm
without fully understand the native behaviours and basics of the go.
My Post
struct is something like this:
type Post struct {
Id int
Title string
Body string
Tags json.RawMessage
}
When persisting posts, my save()
function works without any problem:
func (p *Post) save() (int, error) {
const query = `INSERT INTO post (title, body, tags) VALUES($1, $2, $3) RETURNING id`
db := GetDB()
var postid int
err := db.QueryRow(query, p.Title, p.Body, p.Tags).Scan(&postid)
return postid, err
}
and to read the latest posts, I wrote a small function:
func getLatestPosts(page int) (*[]Post, error) {
const query = `SELECT id, title, body, tags FROM posts ORDER BY id DESC LIMIT 10`
var items []Post
db := GetDB()
rows, err := db.Query(query)
if err != nil {
return nil, err
}
for rows.Next() {
var item Post
if err := rows.Scan(&item.Id, &item.Title, &item.Body, &item.Tags); err != nil {
log.Fatal(err)
}
items = append(items, item)
}
return &items, err
}
This also works until hitting a post row which tags column is null and I'm getting the following error at this point:
> 2015/04/16 21:53:04 sql: Scan error on column index 4: unsupported
> driver -> Scan pair: <nil> -> *json.RawMessage
My question is, what is the proper way to handle nullable json
columns while scanning the resultset? Is this error related with the lib/pq
?
My schema is:
CREATE TABLE post (
"id" int4 NOT NULL DEFAULT nextval('post_id_seq'::regclass),
"title" varchar(255) NOT NULL COLLATE "default",
"body" text COLLATE "default",
"tags" json,
PRIMARY KEY ("id") NOT DEFERRABLE INITIALLY IMMEDIATE
);
and here is an already persisted (not-null) tags
field content:
{
"black-and-white" : "Black and White",
"the-godfather" : "The Godfather"
}
Any ideas?
答案1
得分: 2
TL;DR: 将你的结构体改为 Tags *json.RawMessage
或者使用该类型的临时变量。
注意,如果你感兴趣的话,你可以在 Go 源代码中搜索该错误信息,以更好地了解背后的情况(标准包大多是良好编写的 Go 代码的良好来源)。
我使用以下表格对新鲜的 PostgreSQL-9.4.1 服务器进行了测试:
CREATE TABLE post (
"id" serial PRIMARY KEY,
"title" varchar(255) NOT NULL,
"body" text,
"tags" json
);
(顺便说一下,最好提供重新创建表格的命令,或者用于创建表格的命令,而不是你给出的无法直接使用的形式。此外,当我熟悉 PostgreSQL 时,我记得很少有 varchar
列不是一个错误,而不是直接使用 text
,可能还带有长度约束。)
使用你的结构体类型和该表格,我得到了以下错误:
converting Exec argument #2's type: unsupported type json.RawMessage, a slice
在插入时发生了错误。将其更改为 []byte(p.Tags)
可以解决这个问题(但请参阅下文),然后查询就可以按照你的方式正常工作。
只有当我将一个 NULL 值放入表格时,我才会得到与你相同的错误。解决方法是将结构体字段更改为 Tags *json.RawMessage
。然后,我可以在插入时删除我添加的转换,并且查询可以正常工作,根据需要将字段设置为 nil
或不设置。
如果你这样做,请不要忘记在使用 item.Tags
之前检查 item.Tags
是否为 nil
。或者,将数据库字段设置为 NOT NULL
。
我对 Go 的数据库支持不是特别熟悉,不知道是否合理要求使用指向切片的指针来处理 NULL 值;我希望不是这样,因为 Go 已经区分了空切片和 nil 切片。
或者,你可以保持你的类型不变,并使用如下的临时变量:
var item post
var tmp *json.RawMessage
if err := rows.Scan(&item.Id, &item.Title, &item.Body, &tmp); err != nil {
log.Fatal(err)
}
if tmp != nil {
item.Tags = *tmp
}
当你测试带有 NULL body 的情况时,你可能会遇到类似的问题。要么将数据库列设置为 NOT NULL
,要么在你的类型中使用 sql.NullString
,或者像上面那样使用临时变量(使用 NullString
的 Valid
字段来判断是否应该复制字符串)。
其他一些小注释:
golint
建议使用ID
而不是Id
。- 你没有提供
GetDB
的实现,我希望它只是获取一个共享/全局的*sql.DB
。你不希望重复调用sql.Open
。 - 在你的两个函数中,你可以使用预准备的语句(在这些函数之外创建一次)。如果你经常调用这些函数,这可能会有所帮助。
- 你的查询尝试使用 "posts" 表/视图而不是 "post";我猜这是在复制粘贴到问题时出现的错误吗?
- 你的
getLatestPosts
函数返回*[]Post
;不要这样做。只返回[]Post
。你几乎永远不会想要使用指向切片的指针,尤其不作为返回类型。
英文:
TL;DR: change your struct to have Tags *json.RawMessage
or use a temporary of that type.
Note you can search the Go source for the error message to get a better idea of what's going on behind the scenes if you're interested (the standard packages are mostly a good source of well written Go code).
I tested against a fresh PostgreSQL-9.4.1 server with the following table:
CREATE TABLE post (
"id" serial PRIMARY KEY,
"title" varchar(255) NOT NULL,
"body" text,
"tags" json
);
(by the way, it's probably better to give the commands to re-create, or that where used to create the table rather than the form use gave which can't directly be used. Also, when I was familiar with PostgreSQL I recall it was exceedingly rare that a varchar
column wasn't a mistake instead of just using text
, possibly with a length constraint.)
Using that table with your struct type I got:
converting Exec argument #2's type: unsupported type json.RawMessage, a slice
On the insert. Changing to []byte(p.Tags)
made that go away (but see below) and then querying worked as you had it.
I only got the same error you did on querying when I put a NULL value into the table. The solution to this was to change the struct field to Tags *json.RawMessage
. I could then remove the cast I added on insert and and the query worked fine, either setting the field to nil
or not as appropriate.
If you do this, don't forget to check if item.Tags
is nil
before using it. Alternatively, make the database field NOT NULL
.
I'm not overly familiar with Go's database support to know if requiring a pointer to a slice to handle NULLs is reasonable; I'd have hoped not since Go already distinguishes between an empty slice and a nil slice.
Alternatively, you can leave your type as-is and use a temporary like this:
var item post
var tmp *json.RawMessage
if err := rows.Scan(&item.Id, &item.Title, &item.Body, &tmp); err != nil {
log.Fatal(err)
}
if tmp != nil {
item.Tags = *tmp
}
You'll likely experience a similar issue when you test with a NULL body. Either make the database column NOT NULL
or use sql.NullString
, either in your type or as a temporary as above (using the Valid
field of NullString
to see if you should copy the string).
A few other minor notes:
golint
suggests usingID
instead ofId
.- You don't provide your
GetDB
implementation, I hope it's just getting a shared/global*sql.DB
. You don't want to callsql.Open
repeatedly. - In both of your functions you could use a pre-prepared statement (created once outside those functions). That might help if you call those functions often.
- Your query tries to use the "posts" table/view instead of "post"; I'm guessing a typo while cut-n-pasting to the question?
- Your
getLatestPosts
function reutrns*[]Post
; don't do that. Just return[]Post
. You almost never want to use a pointer to a slice, and certainly not as a return type.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论