如何将m:n关系映射到切片字段?

huangapple go评论91阅读模式
英文:

How to map m:n relation to slice field?

问题

我有一个结构体Person

type Person struct {
    Id     int64
    Name   string
    Colors []string
}

它应该从一个person表中获取数据:

id | name
---------
1  | Joe
2  | Moe

还有一个person_color表:

person_id | color
-----------------
1         | black
1         | blue
2         | green

通过SELECT p.id, p.name, pc.color FROM person AS p INNER JOIN person_color AS pc ON pc.person_id = p.id,我将这两个表合并为:

id | name | color
-----------------
1  | Joe  | black
1  | Joe  | blue
2  | Moe  | green

目前我能想到的唯一办法就是在迭代rows.Next()时手动映射颜色(注意:这只是虚拟代码):

ps := make([]People, 0)

rows, err := db.Query("SELECT ...")

for rows.Next() {
    var p Person

    err := rows.Scan(&p.Id, &p.Name, &p.Color[0])

    exists := false

    for _, ps := range ps {
        if ps.Id == p {
            exists = true

            ps.Color = append(ps.Color, p.Color)
        }
    }

    if !exists {
        ps = append(ps, p)
    }
}

虽然这样可以工作,但是将映射到切片字段是一种常见操作,这样做相当麻烦。

是否有办法使用sqlsqlx使上述操作适用于所有切片字段?

英文:

I have a struct Person:

type Person struct {
    Id     int64
    Name   string
    Colors []string
}

which should get its data from a person table:

id | name
---------
1  | Joe
2  | Moe

and a person_color table:

person_id | color
-----------------
1         | black
1         | blue
2         | green

Via SELECT p.id, p.name, pc.color FROM person AS p INNER JOIN person_color AS pc ON pc.person_id = p.id I merge both tables to:

id | name | color
-----------------
1  | Joe  | black
1  | Joe  | blue
2  | Moe  | green

At the moment the only thing I could think of would be to manually map colors while iterating over rows.Next() (NOTE: just dummy code):

ps := make([]People, 0)

rows, err := db.Query("SELECT ...")

for rows.Next() {
    var p Person

    err := rows.Scan(&p.Id, &p.Name, &p.Color[0])

    exists := false

    for _, ps := range ps {
        if ps.Id == p {
            exists = true

            ps.Color = append(ps.Color, p.Color)
        }
    }

    if !exists {
        ps = append(ps, p)
    }
}

Though this would work this is quite annoying as mapping to a slice field is a common operation.

Is there any way to make the above generic on all slice fields with sql or sqlx?

答案1

得分: 3

我几乎肯定会从SQL的角度来处理这个问题。在PostgreSQL中,你可以使用array_agg函数来获取数组类型的返回值,只要有适当的Scanner实现,它就能抵抗奇怪的数据值:

SELECT p.id, p.name, array_agg(person_color) AS color_array FROM 
       person AS p INNER JOIN 
       person_color AS pc 
    ON
       pc.person_id = p.id
    GROUP BY p.id;

这将返回:

 id | name |  color_array
----+------+--------------
  1 | Joe  | {black,blue}
  2 | Moe  | {green}

你需要创建一个类似于type pgarraystring []string的Go类型,并实现Scanner接口,不过我可能会在不久的将来在github.com/jmoiron/sqlx/types包中添加一些针对PostgreSQL的类型。

在MySQL或SQLite中,你将缺少数组类型,但是你可以使用GROUP_CONCAT[1]来实现类似的结果。在其他数据库中,应该有类似的连接聚合函数可以使用。

采用这种方法有几个原因。你使用SQL数据库是有原因的;它应该能够以所需的格式返回你想要的数据;除非它真的会成为一个问题并且你已经进行了测量,否则就回退到它,这是它作为数据存储的优势。它还减少了通过网络发送回来的数据量和游标执行的获取次数,因此通常情况下它的行为应该更好。

[1] 很抱歉,我不能发布GROUP_CONCAT的链接,因为我没有任何StackOverflow的声望,但你应该能够通过谷歌搜索找到相关信息。

英文:

I'd almost certainly approach this from the SQL side. In PostgreSQL you can use the array_agg to get array types back, which given the proper Scanner implementation should be resistant to weird data values:

SELECT p.id, p.name, pc.color FROM 
       person AS p INNER JOIN 
       array_agg(person_color) AS pc 
    ON
       pc.person_id = p.id
    GROUP BY p.id;

This would return:

 id | name |  array_agg
----+------+--------------
  1 | Joe  | {black,blue}
  2 | Moe  | {green}

It's up to you to create a Go type like type pgarraystring []string and implement Scanner, though it's possible I will add some of these types for PostgreSQL someday soon in the github.com/jmoiron/sqlx/types package.

In MySQL or SQLite, you're going to lack array types, but you can use GROUP_CONCAT[1] to achieve similar results. In other databases, there should be a similar concat aggregate which works with the text representation.

There are a few reasons for going this route. You're using an SQL database for a reason; it should be able to return you the data you want in the desired format; unless it's really going to be a problem and you've measured it, fall back on it, that's its strength as a datastore. It also reduces the amount of data being sent back over the wire and the number of fetches being done by the cursor, so in general it should behave better.

[1] Sorry, I can't post a link to GROUP_CONCAT because I don't have any StackOverflow reputation, but you should be able to google it.

答案2

得分: 0

我原本期望代码看起来更像这样:

// 假设上下文在一个可以返回错误的函数中。
ps := make(map[int64]Person)

rows, err := db.Query("SELECT ...")

for rows.Next() {
    var id int64
    var name string
    var color string
    err := rows.Scan(&id, &name, &color)
    if err != nil {
        return err
    }
    p, ok := ps[id]
    if !ok {
        p.Id = id
        p.Name = name
    }
    p.Colors = append(p.Colors, color)
    ps[id] = p
}

你现在的代码可能会比较耗费资源,因为原始代码是在每个连接表的行上迭代所有的人。你可以通过映射快速跳转到正确的条目,而不是手动扫描。

此外,原始代码在已存在的情况下未将修改后的人保存回切片中。请记住,你正在使用的是 "Person",而不是 "*Person"。如果你使用的是 *Person,上面的代码可以改进为:

// 假设上下文在一个可以返回错误的函数中。
ps := make(map[int64]*Person)

rows, err := db.Query("SELECT ...")

for rows.Next() {
    var id int64
    var name string
    var color string
    err := rows.Scan(&id, &name, &color)
    if err != nil {
        return err
    }
    p, ok := ps[id]
    if !ok {
        p := &Person{id, name, []string{color}}
        ps[id] = p
        continue
    }
    p.Colors = append(p.Colors, color)
}
英文:

I was expecting the code to look more like this.

// Assuming the context is in a function that can return _error_.
ps := make(map[int64]Person)

rows, err := db.Query("SELECT ...")

for rows.Next() {
    var id int64
    var name string
    var color string
    err := rows.Scan(&id, &name, &color)
    if err != nil {
        return err
    }
    p, ok := ps[id]
    if !ok {
        p.Id = id
        p.Name = name
    }
    p.Colors = append(p.Colors, color)
    ps[id] = p
}

The code you have now is potentially expensive because the original code is iterating across all the people, for each row in the joined table. Rather than do the manual scan, you can you can quickly jump to the right entry by means of the mapping.

Also, the original code fails to save the modified person back into the slice if it already exists. Remember, you're working with Person, not *Person. If you do work with *Person, the code above can be refined to:

// Assuming the context is in a function that can return _error_.
ps := make(map[int64]*Person)

rows, err := db.Query("SELECT ...")

for rows.Next() {
    var id int64
    var name string
    var color string
    err := rows.Scan(&id, &name, &color)
    if err != nil {
        return err
    }
    p, ok := ps[id]
    if !ok {
        p := &Person{id, name, []string{color}}
        ps[id] = p
        continue
    }
    p.Colors = append(p.Colors, color)
}

huangapple
  • 本文由 发表于 2014年8月30日 21:26:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/25582848.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定