golang-sqlx-clickhouse-tuple

Go查询CH 数据 tuple 转数组

Go 查询 ClickHouse

业务场景:需要使用 GolangCilckHouse 集群中查询数据,查询的 SQL 以模版的形式存放在 Postgresql 数据库中.
使用的 driversqlx

  • 使用CH 在私有集群中,报漏的端口只有 http8123 所以需要更改拨号协议
  • 另外如果 ClickHouse 是必要的依赖服务,每次获取查询对象 需要先检查一下 Ping()
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
// ClickHouse配置参数
type ClickHouse struct {
Host string
Port int
User string
Password string
DBName string
ReadTimeout uint
WriteTimeout uint
Parameters string // debug=true
}
// DSN 数据库连接串
func (a ClickHouse) DSN() string {
// dsn := fmt.Sprintf("http://%s:%s@%s:%d/%s?read_timeout=%d&write_timeout=%d", a.User, a.Password, a.Host, a.Port, a.DBName, a.ReadTimeout, a.WriteTimeout)
return fmt.Sprintf("http://%s:%s@%s:%d/%s", a.User, a.Password, a.Host, a.Port, a.DBName)
}

connect, err := sqlx.Open("clickhouse", "tcp://127.0.0.1:9000?compress=true&debug=true")
//connect, err := sqlx.Open("clickhouse", "http://127.0.0.1:8123?compress=true&debug=true")

checkErr(err)
if err := connect.Ping(); err != nil {
if exception, ok := err.(*clickhouse.Exception); ok {
fmt.Printf("[%d] %s \n%s\n", exception.Code, exception.Message, exception.StackTrace)
} else {
fmt.Println(err)
}
return
}

Row 转 Map

  • 因为查询的 SQL 模版较多, 不想定义对应的结构体 :smaile:
    所以对查询数据做了一个 Row2Map 的处理,这样获取到的数据 直接转换为 JSON 输出到前端
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

type QueryResult map[string]interface{}

func RowsToMap(rows *sqlx.Rows) []QueryResult {
var list = []QueryResult{} //返回的切片

if rows == nil {
return list
}
ts, _ := rows.ColumnTypes()
columns, _ := rows.Columns()
columnLength := len(columns)
cache := make([]interface{}, columnLength) //临时存储每行数据
for index, _ := range cache { //为每一列初始化一个指针
var a interface{}
cache[index] = &a
}
for rows.Next() {
_ = rows.Scan(cache...)
item := make(QueryResult)
for idex, data := range cache {
val := *data.(*interface{}) //取实际类型
item[columns[idex]] = GenarateTuplesToArray(val, ts[idex])
}
list = append(list, item)
}
return list
}

Tuple 转 数组

  • 在返回的数据中出现 前端 Javascrip 取值异常的情况,因为先前的数据接口已经有一版本 nodejs 完成的.
    所以前端的 数据处理部分已经是完成了的.现在出现了出局结构不一致的情况,具体数据不同体现在:
1
2
3
[{"11111","asdasd,aaaa,aaa"}] => nodejs => [["11111","asdasd,aaaa,aaa"]] 

[{"11111","asdasd,aaaa,aaa"}] => golang => [{field0:"11111",field1:"asdasd,aaaa,aaa"}]

最初以为是 SQL 变更了: 排查后发现并没有修改 SQL.但是发现了查询有使用 Tuple
具体对比数据数据结构后 确认是 由于 node/go 转换元组 Tuple 的操作不同产生的

1
2
3
4
5
6
7
8
9
// anwsers 字段的查询Tuple
arrayFilter(
(i,type) -> (type IN (1, 28)),
arrayMap(rdata -> (
rdata, concat(dictGet('ipip4', 'country', tuple(IPv4StringToNum(rdata))),
If(province = '','',','),dictGet('ipip4','province',tuple(IPv4StringToNum(rdata))) AS province,
If(city = '','',','),dictGet('ipip4','city',tuple(IPv4StringToNum(rdata))) AS city)),
res.`responseAnswerRrs.rdata`),
res.`responseAnswerRrs.type`) AS answers

打印详细的查询结果 及 元组字段 ‘anwsers’ 格式:

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

ts, _ := rows.ColumnTypes() // 所有列对象

columnTypes := ts[idex] // idex 是查询结果中 元组字段的下标,具体可以在 row2map 中获取使用

// 字段类型
columnTypes.DatabaseTypeName() = "Array(Tuple(String, String))"
columnTypes.ScanType() = []struct {
Field0 string
Field1 string
}

// 查看 database/sql 包中的 ColumnType 类定义
// ColumnType contains the name and type of a column.
type ColumnType struct {
name string

hasNullable bool
hasLength bool
hasPrecisionScale bool

nullable bool
length int64
databaseType string
precision int64
scale int64
scanType reflect.Type
}

// 最后查询出来的数据格式
[{"11111","asdasd,aaaa,aaa"}]

最后解决方案是: 修改 row2map 函数,将 Array(Tuple(String, String)) 手动转换为 [][]string,
这样 json.Marshal(d) 处理的时候就会保持原来的数组格式

[{"11111","asdasd,aaaa,aaa"}] => golang => [["11111","asdasd,aaaa,aaa"]]

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 转换查询结果中的 Tuple 数据 (aaa,vvv,aaa)
func GenarateTuplesToArray(val interface{}, columnTypes *sql.ColumnType) interface{} {
if columnTypes.DatabaseTypeName() == "Array(Tuple(String, String))" {
d := val.([]struct {
Field0 string
Field1 string
}) // 按照具体情况来
t := make([][]string, len(d))
for k, v := range d {
t[k] = []string{v.Field0, v.Field1}
}
return t
}
return val
}