在Golang中处理一对多或多对多SQL关系时,将行映射到结构的最佳方式(高效,推荐的"类似Go")是什么?

以下面的示例设置为例,我尝试详细介绍每种方法的优缺点,但是想知道社区的建议。

要求

  • 适用于PostgreSQL(可以是通用的,但不包括MySQL / Oracle特定的功能)
  • 效率-不会强求每种组合
  • 没有ORM-理想情况下仅使用database/sqljmoiron/sqlx

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

楷模

1
2
3
4
5
6
7
8
9
TYPE Tag struct {
  ID INT
  Name string
}

TYPE Item struct {
  ID INT
  Tags []Tag
}

数据库

1
2
3
4
5
6
7
8
9
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-选择所有项目,然后为每个项目选择标签

1
2
3
4
5
6
7
8
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连接并手动遍历行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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结构扫描

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

1
2
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

虽然我确定这将行不通,但我已经包含了这个未经测试的选项,以查看是否可以对其进行改进,使其可行。

1
2
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")

有时间时,我将尝试在此处进行一些实验。

  • 关于您的sqlx尝试:如何定义Item结构类型?
  • @LeGEC-在上面我的问题的Models部分中进行了详细说明-在此简单示例中,只是一个ID int Tags []Tag数组。 实际上,这要复杂得多。

postgres中的sql:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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:

1
2
3
4
SELECT  row_to_json(ROW)
FROM (
    SELECT * FROM item_tags
) ROW;

并解组结构:

优点:

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

  • golang管理业务模型和逻辑。

  • 这是简单的方法。

    • 谢谢你! 我喜欢这个主意,对责任有明确的了解是很不错的。 同样,这是另一个需要维护的映射,虽然比较冗长但得到了好评。

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

    在这种情况下,您可以在查询中为标签制作一个json,然后将其返回。

    优点:您对数据库有1个调用,该调用将聚合数据,而您要做的就是将json解析为一个数组。

    缺点:有点难看。 随时为我ash。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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
    }
    • 感谢您的替代方法。 Ive授予了upvote,因为我认为这绝对是一个选择,尽管您是对的-这不是很漂亮-通过将其添加到具有20多个字段的结构中,可以提高兼容性,并保留另一个映射。 但是,我不会接受,因为我希望有一个更"优雅"的解决方案。
    • 这真的很聪明! 丑陋可以解决,但这可以与常规group by和自定义json解组一起使用