如何避免在可为空的 JSON 列上出现 “Scan pair: *json.RawMessage” 错误?

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

How to avoid Scan pair: <nil> *json.RawMessage errors on nullable JSON columns?

问题

几周前,我开始学习Go,并在学习基础知识的同时尝试构建一个简单的博客应用程序。

目前,我正在尝试使用database/sqlgithub.com/lib/pq包来获取和保存博客文章。我不喜欢在完全理解Go的本机行为和基础知识之前使用第三方包,如sqlxgorm

我的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(&amp;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(&amp;item.Id, &amp;item.Title, &amp;item.Body, &amp;item.Tags); err != nil {        
            log.Fatal(err)
        }
        items = append(items, item)
    }
    return &amp;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 (
    &quot;id&quot; int4 NOT NULL DEFAULT nextval(&#39;post_id_seq&#39;::regclass),
    &quot;title&quot; varchar(255) NOT NULL COLLATE &quot;default&quot;,
    &quot;body&quot; text COLLATE &quot;default&quot;,
    &quot;tags&quot; json,
    PRIMARY KEY (&quot;id&quot;) NOT DEFERRABLE INITIALLY IMMEDIATE
);

and here is an already persisted (not-null) tags field content:

 {
     &quot;black-and-white&quot; : &quot;Black and White&quot;,
     &quot;the-godfather&quot; : &quot;The Godfather&quot;
 }

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,或者像上面那样使用临时变量(使用 NullStringValid 字段来判断是否应该复制字符串)。


其他一些小注释:

  • 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 (
        &quot;id&quot;    serial          PRIMARY KEY,
        &quot;title&quot; varchar(255)    NOT NULL,
        &quot;body&quot;  text,
        &quot;tags&quot;  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&#39;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(&amp;item.Id, &amp;item.Title, &amp;item.Body, &amp;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 using ID instead of Id.
  • You don't provide your GetDB implementation, I hope it's just getting a shared/global *sql.DB. You don't want to call sql.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.

huangapple
  • 本文由 发表于 2015年4月18日 02:23:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/29706727.html
匿名

发表评论

匿名网友

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

确定