英文:
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)
}
}
虽然这样可以工作,但是将映射到切片字段是一种常见操作,这样做相当麻烦。
是否有办法使用sql或sqlx使上述操作适用于所有切片字段?
英文:
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)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论