GOLANG:使用SQLX库执行简单的SQL语句

目录

虽然 GOLANG 自带 SQL 库 database/sql,但为了简化代码提高编程效率,笔者推荐使用第三方库来处理数据库相关操作。

本文介绍如何使用 SQLX 库连接 MySQL 数据库并执行简单的 SELECT 查询语句。

示例代码来自 nwpc-oper/nmc-typhoon-db-client 项目,从 NMC 台风数据库中检索台风数据,计划用于 CMA-PI 上的业务系统。

安装

连接 MySQL 数据库需要下载数据库驱动 Go-MySQL-Driver

go get -u github.com/go-sql-driver/mysql

下载 SQLX 库

go get github.com/jmoiron/sqlx

在源码中载入上述两个库

import (
  	// ...skip...
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
)

创建数据库连接

SQLX 使用的数据库连接地址字符串与 SQLAlchemy 不同,格式如下所示

user:password@tcp(host:port)/database?param1=value1

为了解析时间字段,增加 parseTime=true 参数

var db *sqlx.DB

conn := fmt.Sprintf(
		"%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true&loc=Local",
		config.Auth.User,
		config.Auth.Password,
		config.Host,
		config.DatabaseName,
)

db, err = sqlx.Open("mysql", conn)

if err != nil {
		log.Fatal("open db connection has error:", err)
		return nil, err
}

defer db.Close()

执行查询

使用 sqlx.DB 对象的 Queryx() 函数执行 SQL 语句,返回值中的 rows 是一个迭代器。

querySQL := fmt.Sprintf("SELECT %s FROM %s WHERE %s AND %s",
		queryColumnsString,
		tableName,
		datetimeQuery,
		forecastHourQuery,
)

rows, err := db.Queryx(querySQL)

if err != nil {
		log.Fatal("query db has error:", err)
		return nil, err
}

defer rows.Close()

处理结果

在标准库 database/sql 中,对每行对象使用 Scan() 方法获取具体的字段值。

该方法需要为每个字段设置一个变量。例如

var (
  name string,
  number int,
  speed float,
)
row.Scan(&name, &number, &speed)

如果检索结果中字段很多,就可能会导致代码不够简洁。

SQLX 库提供 StructScan() 方法,可以将所有字段放入一个结构体中。

下面首先定义结构体 Record 代表每行数据,再将检索结果全部转为 Record

定义记录结构体

为每个字段都创建对应的成员,可以使用名称为 db 的 struct tag,为字段指定对应的列名。

type Record struct {
	Xuhao     int          `csv:"xuhao"`
	Center    string       `csv:"center"`
	Bwtype    DataString   `csv:"bwtype"`
	FcstType  string       `db:"FCSTType" csv:"FCSTType"`
	Zone      DataString   `csv:"zone"`
	Tfbh      DataString   `csv:"tfbh"`
	Tfbhbabj  DataString   `csv:"tfbhbabj"`
	Engname   DataString   `csv:"engname"`
	Datetime  NullDateTime `csv:"datetime"`
	Fcsthour  int          `csv:"fcsthour"`
	Lat       DataFloat64  `csv:"lat"`
	Lon       DataFloat64  `csv:"lon"`
	Pressure  DataInt32    `csv:"pressure"`
	Windv     DataFloat64  `csv:"windv"`
	Gusts     DataFloat64  `csv:"gusts"`
	Strength  DataString   `csv:"strength"`
	Windclass DataInt32    `csv:"windclass"`
	Movedir   DataString   `csv:"movedir"`
	Movespeed DataFloat64  `csv:"movespeed"`
	Wind7v1   DataFloat64  `csv:"wind7v1"`
	Wind7v2   DataFloat64  `csv:"wind7v2"`
	Wind7v3   DataFloat64  `csv:"wind7v3"`
	Wind7v4   DataFloat64  `csv:"wind7v4"`
	Wind10v1  DataFloat64  `csv:"wind10v1"`
	Wind10v2  DataFloat64  `csv:"wind10v2"`
	Wind10v3  DataFloat64  `csv:"wind10v3"`
	Wind10v4  DataFloat64  `csv:"wind10v4"`
	Wind12v1  DataFloat64  `csv:"wind12v1"`
	Wind12v2  DataFloat64  `csv:"wind12v2"`
	Wind12v3  DataFloat64  `csv:"wind12v3"`
	Wind12v4  DataFloat64  `csv:"wind12v4"`
}

注:上面的代码中使用了自定义的数据类型 DataStringDataFloat64DataInt32NullDateTime,并定义名为 csv 的 struct tag,这都是为了将结果输出到 CSV 格式文件,后面会介绍。

解析检索数据

使用 StructScan 方法根据每行结果填充 Record 对象,将所有结果放到 records 数组中。

records := []Record{}
for rows.Next() {
		var record Record
		err = rows.StructScan(&record)

		if err != nil {
			log.Fatal("scan row has error:", err)
			return nil, err
		}

		records = append(records, record)
}

err = rows.Err()
if err != nil {
		log.Fatal("query rows has error:", err)
		return nil, err
}

输出CSV文件

使用 csvutil 库将 Records 对象输出到 CSV 文件中

自定义类型

因为数据库中的部分字段可以为空,所以不能使用内置的格式,而是使用类似 mysql.NullTimesql.NullString 等特殊类型。

csvutil 库在输出 CSV 文件时会调用对象的 MarshalCSV() 方法,上述这些特殊类型都没有定义该方法,所以无法直接使用。

nmc-typhoon-db-client 项目使用自定义类型封装上述类型,并为这些自定义类型创建 MarshalCSV() 方法。 这就是上面提到的 DataStringDataFloat64DataInt32NullDateTime 几种类型。

DataInt32 为例说明。MarshalCSV() 方法会检测是否为空,只有不为空的情况下才会返回字节数组。

type DataInt32 struct {
	sql.NullInt32
}

func (d DataInt32) MarshalCSV() ([]byte, error) {
	if d.Valid {
		return []byte(fmt.Sprintf("%d", d.Int32)), nil
	} else {
		return nil, nil
	}
}

另外,这种方式也可以用于自定义日期和时间的序列化。

DateTime 是对 time.Time 的封装,序列化时返回 YYYYMMDDHH 格式的字符串

type DateTime struct {
	time.Time
}

func (date DateTime) MarshalCSV() ([]byte, error) {
	return []byte(date.Format("200601021504")), nil
}

输出

csvutil.Marshal() 函数返回序列化后的字节数组,可以直接写入到文件中。

默认设置下,输出的 CSV 文件使用逗号分隔,并包含表头。

b, err := csvutil.Marshal(records)
if err != nil {
		fmt.Println("error:", err)
		return err
}

f, err := os.Create(filePath)
if err != nil {
		fmt.Println("crate file error:", err)
		return err
}
f.Write(b)
f.Close()

讨论

当前代码还有改进空间,queryColumnsString 字符串由列名字符串数组拼接而成,与 Record 类型的成员变量名称有重复。 后续可以考虑使用反射机制,直接从 Record 类型生成 queryColumnsString 字符串。

参考

nwpc-oper/nmc-typhoon-db-client

https://github.com/nwpc-oper/nmc-typhoon-db-client

SQLAlchemy:执行简单的SQL语句》介绍使用 Python 的 SQLAlchemy 库实现同样的功能。