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_idtag_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 来处理潜在的空值。我应该提到这个解决方案有效。但是,我有两个问题:

  1. 这个解决方案看起来很笨拙和混乱;阅读起来很复杂(至少对我而言)。 是否有更好、更惯用的 Go 方法来做到这一点?
  2. 调用 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
}