correct/idiomatic 处理数据库查询中可能的空值的方法是什么?
What is the correct/idiomatic way of handling possible null values from a database query?
我正在用 Go 处理多对多关系。为此,我使用 pgx
PostgreSQL 驱动程序。
为了让这个问题尽可能简单,我们假设一个简单的博客 post 可以有一些标签:
CREATE TABLE IF NOT EXISTS tag (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tagName varchar(255) UNIQUE NOT NULL,
);
CREATE TABLE IF NOT EXISTS post (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
title varchar(255) NOT NULL,
description varchar(255),
);
CREATE TABLE IF NOT EXISTS post_tag (
post_id bigint REFERENCES post (id) ON UPDATE CASCADE ON DELETE CASCADE,
tag_id bigint REFERENCES tag (id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT post_tag_pkey PRIMARY KEY (post_id, tag_id)
);
为了检索 posts 和它们的标签,我使用了类似下面的查询(在一个视图中以便于在 Go 中查询):
SELECT p.id as post_id, p.title, p.description, t.id as tag_id, t.tagName as tag_name
FROM post p
LEFT JOIN post_tag pt
ON p.id = pt.post_id
LEFT JOIN tag t
ON t.id = pt.tag_id;
此查询可能 return 一些行,其中 tag_id
和 tag_name
为空。我目前处理此问题的方式如下(为简单起见删除了错误处理):
func ReadPosts() ([]*model.Post, error) {
var posts []*model.Post
var postWithTags = make(map[uint64]*model.Post)
statement := `SELECT *
FROM post_with_tag` // pgsql view with joins to get tags
rows, _ := db.Query(
context.Background(),
statement,
)
for rows.Next() {
var (
post model.Post
tag model.Tag
tagID pgtype.Numeric
tagName pgtype.Varchar
tagValid bool
)
_ = rows.Scan(
&post.ID,
&post.Title,
&post.Description,
&tagID,
&tagName,
)
if tagID.Status == pgtype.Present {
tag.ID = tagID.Int.Uint64()
tag.Name = tagName.String
tagValid = true
} else {
tagValid = false
}
if _, ok := postWithTags[post.ID]; ok {
if tagValid {
postWithTags[post.ID].Tags = append(postWithTags[post.ID].Tags, &tag)
}
} else {
post.Tags = []*model.Tag{}
if tagValid {
post.Tags = []*model.Tag{
&tag,
}
}
postWithTags[post.ID] = &post
}
}
for _, v := range postWithTags {
posts = append(posts, v)
}
return posts, nil
}
如您所见,我正在使用 pgtype
来处理潜在的空值。我应该提到这个解决方案有效。但是,我有两个问题:
- 这个解决方案看起来很笨拙和混乱;阅读起来很复杂(至少对我而言)。 是否有更好、更惯用的 Go 方法来做到这一点?
- 调用
tagID.Int.Uint64()
时,我总是得到 returned 0
作为标签 ID,这是不正确的。 我这里有什么地方做错了吗?(我使用 pgtype.Numeric
因为数据库中的标签 ID 是一个 pgsql bigint
)。
如果您使用的是 table 视图并且不需要按 NULL
(例如 WHERE col IS [NOT] NULL
)进行过滤,那么您可能只想使用 COALESCE
在视图中,这样你可以在 Go 结束时避免一些头痛。如果你直接处理 tables,你仍然可以在你用 Go 构建的 SQL statement
字符串中使用 COALESCE
,但是如果你选择这样做您将无法将它与 SELECT *
一起使用,而必须明确列出列。
如果您不喜欢 COALESCE
方法,您可以实现自己的 sql.Scanner
方法,而不是有一个值字段,将有一个指针字段,然后允许您设置您的模型在与您正在扫描列的行相同的行上通过间接访问字段。
_ = rows.Scan(
&post.ID,
&post.Title,
&post.Description,
MyNullNumeric{&tag.ID},
MyNullString{&tag.Name},
)
MyNullXxx
可能如下所示:
type MyNullString struct {
Ptr *string
}
func (ns MyNullString) Scan(src interface{}) error {
switch v := src.(type) {
case string:
*ns.Ptr = v
case []byte:
*ns.Ptr = string(v)
case nil:
// nothing
default:
// maybe nothing or error, up to you
}
return nil
}
我正在用 Go 处理多对多关系。为此,我使用 pgx
PostgreSQL 驱动程序。
为了让这个问题尽可能简单,我们假设一个简单的博客 post 可以有一些标签:
CREATE TABLE IF NOT EXISTS tag (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tagName varchar(255) UNIQUE NOT NULL,
);
CREATE TABLE IF NOT EXISTS post (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
title varchar(255) NOT NULL,
description varchar(255),
);
CREATE TABLE IF NOT EXISTS post_tag (
post_id bigint REFERENCES post (id) ON UPDATE CASCADE ON DELETE CASCADE,
tag_id bigint REFERENCES tag (id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT post_tag_pkey PRIMARY KEY (post_id, tag_id)
);
为了检索 posts 和它们的标签,我使用了类似下面的查询(在一个视图中以便于在 Go 中查询):
SELECT p.id as post_id, p.title, p.description, t.id as tag_id, t.tagName as tag_name
FROM post p
LEFT JOIN post_tag pt
ON p.id = pt.post_id
LEFT JOIN tag t
ON t.id = pt.tag_id;
此查询可能 return 一些行,其中 tag_id
和 tag_name
为空。我目前处理此问题的方式如下(为简单起见删除了错误处理):
func ReadPosts() ([]*model.Post, error) {
var posts []*model.Post
var postWithTags = make(map[uint64]*model.Post)
statement := `SELECT *
FROM post_with_tag` // pgsql view with joins to get tags
rows, _ := db.Query(
context.Background(),
statement,
)
for rows.Next() {
var (
post model.Post
tag model.Tag
tagID pgtype.Numeric
tagName pgtype.Varchar
tagValid bool
)
_ = rows.Scan(
&post.ID,
&post.Title,
&post.Description,
&tagID,
&tagName,
)
if tagID.Status == pgtype.Present {
tag.ID = tagID.Int.Uint64()
tag.Name = tagName.String
tagValid = true
} else {
tagValid = false
}
if _, ok := postWithTags[post.ID]; ok {
if tagValid {
postWithTags[post.ID].Tags = append(postWithTags[post.ID].Tags, &tag)
}
} else {
post.Tags = []*model.Tag{}
if tagValid {
post.Tags = []*model.Tag{
&tag,
}
}
postWithTags[post.ID] = &post
}
}
for _, v := range postWithTags {
posts = append(posts, v)
}
return posts, nil
}
如您所见,我正在使用 pgtype
来处理潜在的空值。我应该提到这个解决方案有效。但是,我有两个问题:
- 这个解决方案看起来很笨拙和混乱;阅读起来很复杂(至少对我而言)。 是否有更好、更惯用的 Go 方法来做到这一点?
- 调用
tagID.Int.Uint64()
时,我总是得到 returned0
作为标签 ID,这是不正确的。 我这里有什么地方做错了吗?(我使用pgtype.Numeric
因为数据库中的标签 ID 是一个 pgsqlbigint
)。
如果您使用的是 table 视图并且不需要按 NULL
(例如 WHERE col IS [NOT] NULL
)进行过滤,那么您可能只想使用 COALESCE
在视图中,这样你可以在 Go 结束时避免一些头痛。如果你直接处理 tables,你仍然可以在你用 Go 构建的 SQL statement
字符串中使用 COALESCE
,但是如果你选择这样做您将无法将它与 SELECT *
一起使用,而必须明确列出列。
如果您不喜欢 COALESCE
方法,您可以实现自己的 sql.Scanner
方法,而不是有一个值字段,将有一个指针字段,然后允许您设置您的模型在与您正在扫描列的行相同的行上通过间接访问字段。
_ = rows.Scan(
&post.ID,
&post.Title,
&post.Description,
MyNullNumeric{&tag.ID},
MyNullString{&tag.Name},
)
MyNullXxx
可能如下所示:
type MyNullString struct {
Ptr *string
}
func (ns MyNullString) Scan(src interface{}) error {
switch v := src.(type) {
case string:
*ns.Ptr = v
case []byte:
*ns.Ptr = string(v)
case nil:
// nothing
default:
// maybe nothing or error, up to you
}
return nil
}