1

我正在处理 Go 中的多对多关系。为此,我使用的是pgxPostgreSQL 驱动程序。

为了使这个问题尽可能简单,让我们假设一个简单的博客文章可以有一些标签:

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)
);

为了检索帖子及其标签,我使用了类似于以下查询的内容(为了在 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;

此查询可能会返回一些行,其中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()我总是0以标签 ID 的形式返回,这是不正确的。我在这里做错了吗?(我使用pgtype.Numeric是因为数据库中的标签 ID 是 pgsql bigint)。
4

1 回答 1

0

如果您使用的是表格视图并且不需要过滤NULL(例如WHERE col IS [NOT] NULL),那么您可能只想COALESCE在视图中使用,这样您就可以在 Go 端省去一些麻烦。如果您直接处理表,您仍然可以COALESCE在您在 Go 中构建的 SQLstatement字符串中使用,但是如果您选择这样做,您将无法将它与 一起使用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
}
于 2020-10-14T15:23:04.000 回答