I'm handling a many-to-many relationship in Go. For this, I'm using the pgx
PostgreSQL driver.
For the sake of keeping this question as simple as possible, let's assume a simple blog post that can have some tags:
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)
);
To retrieve posts and their tags, I use something like the following query (in a view for easy querying in 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;
This query will likely return some rows where tag_id
and tag_name
are null. The way I'm currently handling this, is the following (error handling removed for sake of simplicity):
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
}
As you can see, I'm using pgtype
to handle the potential null values. I should mention that this solution works. However, I have two issues:
- This solution seems quite clunky and messy; it's complex to read (at least to me). Is there a better, more idiomatic Go way of doing this?
- When calling
tagID.Int.Uint64()
I always get returned0
as the tag ID, which is not correct. Is there something I'm doing wrong here? (I'm usingpgtype.Numeric
because the tag ID in the database is a pgsqlbigint
).