31

问题

在 Golang 中处理一对多或多对多 SQL 关系时,将行映射到结构的最佳(高效、推荐、“Go-like”)方法是什么?

以下面的示例设置为例,我尝试详细说明每种方法的优缺点,但想知道社区推荐什么。

要求

  • 适用于 PostgreSQL(可以是通用的,但不包括 MySQL/Oracle 特定功能)
  • 效率 - 没有暴力破解每个组合
  • 没有 ORM - 理想情况下只使用database/sqljmoiron/sqlx

例子

为了清楚起见,我删除了错误处理

楷模

type Tag struct {
  ID int
  Name string
}

type Item struct {
  ID int
  Tags []Tag
}

数据库

CREATE TABLE item (
  id                      INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
);

CREATE TABLE tag (
  id                      INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name                    VARCHAR(160),
  item_id                 INT REFERENCES item(id)
);

方法 1 - 选择所有项目,然后为每个项目选择标签

var items []Item
sqlxdb.Select(&items, "SELECT * FROM item")

for i, item := range items {
  var tags []Tag
  sqlxdb.Select(&tags, "SELECT * FROM tag WHERE item_id = $1", item.ID)
  items[i].Tags = tags
}

优点

  • 简单的
  • 容易明白

缺点

  • 随着数据库查询的数量与项目数量成比例地增加,效率低下

方法 2 - 手动构建 SQL 连接和循环遍历行

var itemTags = make(map[int][]Tag)

var items = []Item{}
rows, _ := sqlxdb.Queryx("SELECT i.id, t.id, t.name FROM item AS i JOIN tag AS t ON t.item_id = i.id")
for rows.Next() {
  var (
    itemID  int
    tagID   int
    tagName string
  )
  rows.Scan(&itemID, &tagID, &tagName)
  if tags, ok := itemTags[itemID]; ok {
    itemTags[itemID] = append(tags, Tag{ID: tagID, Name: tagName,})
  } else {
    itemTags[itemID] = []Tag{Tag{ID: tagID, Name: tagName,}}
  }
}
for itemID, tags := range itemTags {
  items = append(Item{
    ID: itemID,
    Tags: tags,
  })
}

优点

  • 单个数据库调用和游标,可以在不消耗太多内存的情况下循环

缺点

  • 结构上的多个连接和许多属性使开发变得复杂且困难
  • 不太高效;更多的内存使用和处理时间与更多的网络调用

失败的方法 3 - sqlx 结构扫描

尽管失败了,但我想包含这种方法,因为我发现它是我目前的目标,即效率与开发简单性相结合。我希望通过db在每个结构字段上显式设置标签sqlx可以进行一些高级结构扫描

var items []Item
sqlxdb.Select(&items, "SELECT i.id AS item_id, t.id AS tag_id, t.name AS tag_name FROM item AS i JOIN tag AS t ON t.item_id = i.id")

不幸missing destination name tag_id in *[]Item的是,这个错误导致我相信StructScan它不够先进,无法递归循环遍历行(没有批评 - 这是一个复杂的场景)

可能的方法 4 - PostgreSQL 数组聚合器和GROUP BY

虽然我确信这行不通,但我已经包含了这个未经测试的选项,看看它是否可以改进,以便它可以工作。

var items = []Item{}
sqlxdb.Select(&items, "SELECT i.id as item_id, array_agg(t.*) as tags FROM item AS i JOIN tag AS t ON t.item_id = i.id GROUP BY i.id")

当我有时间时,我会尝试在这里进行一些实验。

4

3 回答 3

12

postgres中的sql:

create schema temp;
set search_path = temp;
create table item
(
  id INT generated by default as identity primary key
);

create table tag
(
  id      INT generated by default as identity primary key,
  name    VARCHAR(160),
  item_id INT references item (id)
);

create view item_tags as
select id,
  (
          select
            array_to_json(array_agg(row_to_json(taglist.*))) as array_to_json
          from (
                select tag.name, tag.id
                 from tag
                         where item_id = item.id
               ) taglist ) as tags
from item ;


-- golang query this maybe 
select  row_to_json(row)
from (
    select * from item_tags
) row;

然后golang查询这个sql:

select  row_to_json(row)
from (
    select * from item_tags
) row;

并解组去结构:

亲:

  1. postgres 管理数据的关系。使用 sql 函数添加/更新数据。

  2. golang 管理业务模型和逻辑。

这是简单的方法。

.

于 2019-02-10T09:26:36.650 回答
6

我可以建议我以前使用过的另一种方法。

在这种情况下,您在查询中创建标签的 json 并将其返回。

优点:您有 1 次调用 db 来聚合数据,您所要做的就是将 json 解析为一个数组。

缺点:有点丑。随意抨击我。

type jointItem struct {
  Item 
  ParsedTags string
  Tags []Tag `gorm:"-"`
}

var jointItems []*jointItem
db.Raw(`SELECT 
  items.*, 
  (SELECT CONCAT(
            '[', 
             GROUP_CONCAT(
                  JSON_OBJECT('id', id,
                             'name', name 
                  )
             ), 
            ']'
         )) as parsed_tags 
   FROM items`).Scan(&jointItems)

for _, o := range jointItems {
var tempTags []Tag
   if err := json.Unmarshall(o.ParsedTags, &tempTags) ; err != nil {
      // do something
   }
  o.Tags = tempTags
}


编辑:代码可能表现得很奇怪,所以我发现在移动时使用临时标签数组而不是使用相同的结构更好。

于 2019-02-09T21:31:23.977 回答
0

您可以使用https://github.com/jackskj/carta中的 carta.Map() 它自动跟踪 has-many 关系。

于 2020-06-08T21:54:22.177 回答