golang-非orm数据库的操作与对比 database/sql、sqlx 和 sqlc
简单介绍
database/sql
database/sql 是一个标准库软件包,负责与数据库(主要是 SQL 关系数据库)的连接和交互。
它为类 SQL 交互提供泛型接口、类型和方法。database/sql 在创建时将简单易用纳入考量,配置为支持与类 SQL 数据库交互所需的最基本必要功能。
为了与数据库管理系统交互,数据库软件包需要适当的驱动程序。 目前,database/sql 支持超过 50 种数据库驱动程序,涵盖 SQLite、MySQL/MariaDB、PostgreSQL、Oracle 和 MS SQL Server 等最流行的 DBMS,能够增强适配性,增强移植性。
此软件包还支持基本的 CRUD 操作、数据库事务、命名形参、返回多个结果集、可取消查询、SQL 类型支持、连接池管理、形参化查询和预备语句等功能。
尽管它支持许多基本的现代数据库功能,例如事务和预备语句,但它也有一些局限性。 例如,它存在类型限制,无法将大的 uint64 值作为形参传递给语句。
它的主要功能和特性可以总结为以下几点:
- 统一的编程接口:database/sql包通过提供一组统一的API,如Prepare(), Exec(), Query()等,使得开发人员能够以相同的方式操作不同的数据库。这大大提高了代码的可移植性和灵活性。
- 驱动支持:database/sql包本身并不直接与数据库通信,而是依赖于第三方数据库驱动程序。这些驱动程序需要实现database/sql/driver包中定义的Driver接口,并在程序初始化阶段通过sql.Register()方法注册到database/sql中。常见的关系型数据库如MySQL、PostgreSQL、Oracle、Gbase8s等都有对应的Go语言驱动程序。比如oracle的驱动mattn/go-oci8: Oracle driver for Go using database/sql (github.com)
- 连接池管理:database/sql维护了一个数据库连接池,用于管理数据库连接。当通过sql.Open()打开一个数据库连接时,database/sql会在合适的时机调用注册的驱动来创建一个具体的连接,并将其添加到连接池中。连接池会负责连接的复用、管理和维护工作,并且这是并发安全的。
- 事务支持:database/sql包还支持事务处理,可以通过Tx类型的方法如Begin(), Commit(), Rollback()等来进行事务的管理。
- 安全性:为了防止SQL注入攻击,database/sql包推荐使用预编译语句和参数化查询。这样可以确保所有的SQL语句在执行前都会被预先分析和编译,从而避免了潜在的安全问题。
sqlx
创建 sqlx 是为了扩展标准库数据库软件包的功能。 由于它依赖 database/sql 软件包,后者提供的所有功能也可用,包括对同一组数据库技术和驱动程序的支持。
除了这些核心功能之外,sqlx 还具有以下优点:
- 带有命名形参的预备语句 – 这使您能够使用结构字段的名称和映射键绑定预备语句或查询中的变量。
- 结构扫描 – 这允许您将查询结果直接扫描到单行的结构中,不必像 database/sql 那样单独扫描每个字段或列。 它还支持扫描到嵌入式结构。
- Select 和 Get – 这些是用于处理预期将多个记录或单个记录分别返回到结构的切片或单个结构的查询的便捷方法。 不需要循环结果集!
- 对 IN 查询的支持 – 这允许您将值的切片作为单个形参绑定到 IN 查询。 与将切片作为单个值处理的 database/sql 相比,切片在预期位置上以相同数量的 bindvars 展开。
命名查询 – 这会将结构字段的名称绑定到列名称,避免在向 bindvars 赋值时对列名称的位置引用。 - 无错误结果集:结果集不返回错误,允许对返回结果进行链式操作,例如将结果直接扫描到结构中。 如以下代码段所示:
var p Place
err := db.QueryRowx("SELECT city, telcode FROM place LIMIT 1").StructScan(&p)
这些只是 sqlx 软件包众多功能中的几个例子,这些功能确保了比 database/sql 更好的工作体验。
sqlc
sqlc 是一个捆绑为可执行二进制文件的 SQL 编译器,可以为原始 SQL 架构和查询生成类型安全代码。 因此,除了实际的 SQL 语句之外,您不必编写任何样板代码。
根据文档,它可以为 PostgreSQL、MySQL/MariaDB 和 SQLite 生成代码。 然而,生成的代码也适用于标准库的 SQL 软件包;因此,它可以使用所有支持的驱动程序,但不一定使用支持的数据库。 除了支持的数据库和驱动程序之外,以下是它的一些其他功能:
- 查询注解 – 这些注解允许您为每个查询定义函数的名称以及预期的查询结果类型。 它们在代码生成期间用于确定函数的名称和签名。
- JSON 标记 – sqlc 支持为将被编组并作为 JSON 发送给客户端的结构或类型生成 JSON 标记。
- 架构修改 – sqlc 支持读取各种格式的迁移文件以修改架构并生成代码来反映这些更改。
- 结构命名 – sqlc 提供用于从表名称生成结构名称的命名方案的选项。
如果您擅长 SQL 语句,并且不喜欢使用太多代码执行数据库操作和处理数据,那么这个软件包绝对适合您。
database/sql
首先需要连接与defer关闭数据库
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" // 导入MySQL驱动
"log"
)
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err) //or panic
}
defer db.Close()
// 执行你想要的操作
}
后续可单独封装出db对象
需要关闭数据库!
Go的database/sql包在设置了连接池后,仍然需要关闭连接。尽管连接池的设计目的是为了复用数据库连接,提高性能并避免频繁地建立和断开连接,但在使用完数据库连接后,应该显式地关闭它。这是因为关闭连接并不是真正意义上的断开与数据库的TCP连接,而是将连接返回到连接池中,以便其他请求可以复用。如果不关闭连接,连接将会一直被占用,可能导致连接池中的连接被耗尽
测连接是否能ping通
err = db.Ping()
if err != nil {
panic(err)
}
连接池设置
在正常情况下,database/sql的连接池会维护一定数量的活跃和空闲连接。
SetMaxIdleConns()
用于设置连接池中空闲连接的最大数量。
SetMaxOpenConns()
用于设置到数据库的同时最大打开连接数。
SetConnMaxLifetime()
可以设置连接的最大生命周期,超过这个时间,连接将被关闭并从池中移除。
插入、删除、更新—— Exec
以下代码段演示了如何使用带有 MySQL 驱动程序的 database/sql 软件包插入记录:
func addStudent(s Student) (int64, error){
query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);"
result, err := db.Exec(query, s.Fname,s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address)
if err != nil {
return 0, fmt.Errorf("addStudent Error: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addStudent Error: %v", err)
}
return id, nil
}
正如您所看到的,插入操作与编写直接 SQL 语句非常相似。 您还将需要分别输入每个字段及其关联值。 然而,随着时间推移,在大型结构或复杂类型中维护代码会变得很麻烦,增加引入错误的机会,而这些错误可能只能在运行时被捕获。
查询 Query与QueryRow
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" // 导入MySQL驱动
"log"
)
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 执行查询
rows, err := db.Query("SELECT id, name FROM users WHERE active = ? and deleted= ?", 1,0)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// 遍历查询结果
for rows.Next() {
var this_is_id int
var this_is_name string
//Scan会按照顺序将select中的值赋值给括号中的变量
if err := rows.Scan(&this_is_id, &this_is_name); err != nil {
log.Fatal(err)
}
//打印出来
fmt.Printf("ID: %d, Name: %s\n", this_is_id, this_is_name)
}
// 检查遍历是否出现错误
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
在这个示例中,我们使用了Go的database/sql标准库和MySQL驱动。首先,我们使用sql.Open()建立了与数据库的连接。然后,我们使用db.Query()执行了一个查询,查询活跃用户的id和name。最后,我们通过rows.Next()遍历结果集,并通过rows.Scan()将结果存入变量。如果在处理过程中发生错误,我们记录日志并退出程序。
或者还可以将其保存在数组、map或者结构体数组等中(通过append或者直接赋值等方法) 比如
func fetchStudents() ([]Student, error) {
var students []Student
rows, err := db.Query("SELECT * FROM students")
if err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
defer rows.Close()
for rows.Next() {
var s Student
if err := rows.Scan(&s.ID, &s.Fname, &s.Lname, &s.DateOfBirth, &s.Email, &s.Address, &s.Gender ); err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
students = append(students, s)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
return students, nil
}
在上面的代码段中,获取记录后对其循环,使用 row.Scan() 将每个记录的每个字段单独扫描到一个结构中,然后将结构附加到一个切片。 这里需要小心,因为提供的结构中的字段数必须等于返回的记录中的字段数。
QueryRow方法则是只扫描一行
var primary string
err := c.db.QueryRow("select ha_primary from sysha_type").Scan(&primary)
您还应该注意,很难使用 IN 子句处理查询,因为它将 IN bindvar 值视为单个值,而不是多个值。
in的特殊处理
ids := []int{5,6,7}
sqlstr := `select * from student where id in (?) `
rows, err = db.Query(sqlstr, ids )
会报错 sql: converting argument $1 type: unsupported type []int, a slice of int
于是猜测是因为要问号要匹配的的缘故
于是引入一个函数去处理
func placeholders(n int) string {
var b strings.Builder
for i := 0; i < n - 1; i++ {
b.WriteString("?,")
}
if n > 0 {
b.WriteString("?")
}
return b.String()
}
func (c *GetMovieInfoController) Get() {
ids := []int{5,6,7}
db := models.ConnectDb();
defer db.Close()
query := fmt.Sprintf("select id,movie_name ,movie_director from movie_info where id in (%s)", placeholders(len(ids)))
rows, err := db.Query(query,ids)
还是不行 所以是不支持直接数组
解决方法1
ids := []int{}{5,6,7}
query := fmt.Sprintf("select id,movie_name,movie_director from movie_info where id in (%s)", placeholders(len(ids)))
rows, err := db.Query(query,ids...) //加... 这里看golang基础可变参数那里
如果是Exec (包括sqlx的),由于函数定义就是
func (db *DB) Exec(query string, args ...interface{})
所以
ids := []int{}{5,6,7}
要改为ids := []intereface{}{5,6,7}
或者
// 需要查询的ID列表
ids := []int{1, 2, 3}
// 准备查询语句
query := `SELECT * FROM your_table WHERE your_column IN (?)`
// 准备一个args切片来构造查询参数
args := []interface{}{ids}
解决方法2
func formatids(ids []int) ([]interface{},string) {
inIds := ""
params:=make([]interface{},0)
for i:=0;i<len(ids);i++{
if i==0{
inIds+="?"
}else{
inIds+=",?"
}
params=append(params , ids[i])
}
return params,inIds
}
func (c *GetMovieInfoController) Get() {
ids := []int{8,9,10}
db := models.ConnectDb();
defer db.Close()
sql := "select id,movie_name,movie_director from movie_info where id in (%s)"
params,inIds := formatids(ids)
sql = fmt.Sprintf(sql ,inIds )
fmt.Fprint(c.Ctx.ResponseWriter,sql)
rows,err := db.Query(sql , params...)
解决方法3
int好像没这个功能
func (c *GetMovieInfoController) Get() {
ids := []string{"8", "9", "10"}
db := models.ConnectDb();
defer db.Close()
idStr := strings.Join(ids, "','")
fmt.Println(idStr)
sqlText := "select id,movie_name,movie_director from movie_info where id in ('%s')"
sqlText = fmt.Sprintf(sqlText, idStr)
rows,err := db.Query(sqlText)
}
解决方法4 py.Array
将切片参数转换为 pq.Array 类型:
ids := []int{1, 2, 3}
query := `SELECT * FROM your_table WHERE id = ANY($1)`
// 将切片转换为 pq.Array 类型
pqIds := pq.Array(ids)
// 使用 stdlib's database/sql
result, err := db.Query(query, pqIds)
// 或者使用 sqlx
var results []YourStruct
err := db.Select(&results, query, pqIds)
解决方法5
如果你使用的数据库和语言不支持这种操作,你可能需要手动构造SQL查询,将切片元素拼接成逗号分隔的字符串,并确保处理好 SQL 注入的风险。
ids := []int{1, 2, 3}
idStrings := []string{}
for _, id := range ids {
idStrings = append(idStrings, strconv.Itoa(id))
}
query := `SELECT * FROM your_table WHERE id IN (` + strings.Join(idStrings, ",") + `)`
// 使用 database/sql 或 sqlx 执行查询
注意:直接拼接字符串可能会有 SQL 注入的风险,如果你的参数来源不可信,请确保适当地清理或预处理参数。
?与$
在Go语言中,使用database/sql包与数据库交互时,可以使用问号(?)或者占位符($1, $2 等)来预处理SQL语句并防止SQL注入。
问号(?)通常用于占位符,它是一种参数化查询的形式,可以在执行时替换为实际的值。使用问号的优点是代码简洁,缺点是不能重复使用一个占位符。
占位符($1, $2 等)是PostgreSQL的语法,它允许你在SQL语句中多次使用相同的占位符,并在执行时分别替换为不同的值。
以下是使用问号和PostgreSQL占位符的例子:
使用问号:
import (
"database/sql"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "your_connection_string")
if err != nil {
panic(err)
}
defer db.Close()
// 使用问号作为占位符
stmt, err := db.Prepare("SELECT * FROM users WHERE username = $1")
if err != nil {
panic(err)
}
defer stmt.Close()
// 执行查询
rows, err := stmt.Query("alice")
if err != nil {
panic(err)
}
defer rows.Close()
// ... 处理rows
}
使用PostgreSQL的占位符:
import (
"database/sql"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "your_connection_string")
if err != nil {
panic(err)
}
defer db.Close()
// 使用PostgreSQL的占位符
stmt, err := db.Prepare("SELECT * FROM users WHERE username = $1 AND password = $2")
if err != nil {
panic(err)
}
defer stmt.Close()
// 执行查询
rows, err := stmt.Query("alice", "password123")
if err != nil {
panic(err)
}
defer rows.Close()
// ... 处理rows
}
在Go中,通常使用$1, 2 这样的占位符来防止 S Q L 注入,尤其是在处理动态查询时。而问号 ( ? ) 通常用于简单的参数化查询,尤其是在使用 M y S Q L 这类数据库时。如果你使用的数据库不支持 2 这样的占位符来防止SQL注入,尤其是在处理动态查询时。而问号(?)通常用于简单的参数化查询,尤其是在使用MySQL这类数据库时。如果你使用的数据库不支持 2这样的占位符来防止SQL注入,尤其是在处理动态查询时。而问号(?)通常用于简单的参数化查询,尤其是在使用MySQL这类数据库时。如果你使用的数据库不支持占位符,你只能使用问号。
sql注入与预处理 Prepare
为什么占位符能够避免sql注入?
占位符 ? 实际生成的结果,变量会被加引号处理,不会包含有害语句,故可以保证sql注入问题 也就是说 你的sql语句和参数是分开的
select id,namem from mtable1 where name=""kate";drop table table2;"
/sql预处理也可以也可以防止注入问题,
预处理 只是会缓存你执行sql的一些通用的步骤 这样减少重复计算消耗
1、预处理可以提高性能,一次编译多次执行
2、语句与查询条件是分开的,可以保证sql注入问题
什么是预处理?
普通SQL语句的执行过程
- 客户端对SQL语句进行占位符的替换得到了完整的SQL语句
- 客户端发送完整SQL语句到MySOL服务端
- MySQL服务端执行完整的SQL语句并将结果返回终端
预处理的执行过程
- 先把SQL语句拆分成两部分,SQL语句部分和参数部分
- 先把SOL语句部分发送给MySQL服务端进行SQL预处理
- 然后参数部分发送给MySQL服务端,MySOL对SQL语句进行拼接
- MySQL服务端执行完整的SQL语句返回结果
为什么要预处理
- 优化MYSQL服务器重复执行SQL的方法。可以执行服务器的性能,提前让服务器编译,一次编译多次执行,节省后续重复编译的成本
- 避免SQL注入(普通占位符也可以避免)
我们使用 PREPARE 准备这个SQL语句,并通过 EXECUTE 来执行它。最后,记得使用 DEALLOCATE 释放预处理语句占用的资源。
总结来说,PREPARE 和 EXECUTE 提供了一种安全且高效的方式来动态执行SQL语句,特别是在需要多次执行相似SQL但参数不同的场景中。
事务
略
sqlx
sqlx 主要是 Go database/sql 软件包的扩展
在 GitHub 上,sqlx 获得了超过 12,800 颗星,拥有大量关注者和非常活跃的问题列表。
如果您有 database/sql 背景,您将顺利过渡,因为查询在语法上相似且兼容。 从以下代码段中,您会注意到与先前的 database/sql 代码的很多相似之处,尤其是 insert 语句:
func addStudent(s Student) (int64, error){
query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);"
result := db.MustExec(query, s.Fname,s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address)
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addStudent Error: %v", err)
}
return id, nil
}
由于使用 db.MustExec()
方法减少了错误检查的需要,代码行数比 database/sql
少。
以下代码展示了如何使用 sqlx 便捷方法检索多个记录:
func fetchStudents() ([]Student, error) {
var students []Student
err := db.Select(&students,"SELECT * FROM students LIMIT 10")
if err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
return students, nil
}
正如您所看到的,相对于使用 db.Query()
编写与 database/sql
兼容的语法并循环遍历结果,sqlx
提供了更简单、更清晰的代码,帮助您使用 db.Select()
实现相同的目标。
它甚至提供了一种更好的方式来处理带有 IN 子句的查询,如以上“功能”部分所述。
sqlc
前述 sqlc 生成类型安全代码的能力是易用性的另一个好处,因为它减少了为数据库操作编写的 Go 代码量,节省您的时间和精力。
sqlc 拥有一个超过 6,800 颗星的活跃且不断壮大的社区,还拥有强力的参与度和社区支持。
文档提供了从安装到代码生成的分步演示,如果您想要更具互动性的方式,可以观看简明的视频教程。 sqlc 非常容易上手,如下所示。
安装二进制文件后,在 sqlc.yaml 中编写配置文件,它应该类似于下面的代码段:
version: 1
packages:
- path: "./"
name: "main"
engine: "mysql"
schema: "schema.sql"
queries: "query.sql"
现在,您需要做的就是在 2 个文件中编写普通的 SQL,如下图所示,然后在工作目录中运行命令 sqlc generate:
以下列表解释了上图的元素:
- 红框显示了 3 个新生成的文件:models.go 对应于结构、query.sql.go 和 db.go 对应于其他数据库相关代码。
- 黄框高亮显示了包含架构定义的 schema.sql 文件。
- 蓝框高亮显示了 query.sql 文件,其中包含应用程序数据库操作的所有 SQL 语句,以及一些用于生成函数的元数据。
- 绿框高亮显示了 sqlc 在终端的工作目录中生成的运行。
现在,您只需使用 database/sql 软件包提供数据库连接,然后调用所需的方法而不必修改生成的代码,如以下代码段所示:
func main() {
// Create a new database connection
conn, err := sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true")
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected!")
db := New(conn)
// Initialize record to be inserted
newSt := addStudentParams{
Fname: "Leon",
Lname: "Ashling",
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
Email: "lashling5@senate.gov",
Gender: "Male",
Address: "39 Kipling Pass",
}
// Insert the record
sID, err := db.addStudent(context.Background(), newSt)
if err != nil {
log.Fatal(err)
}
fmt.Printf("addStudent id: %v n", sID)
// Fetch the records
students, err := db.fetchStudents(context.Background())
if err != nil {
log.Println(err)
}
fmt.Printf("fetchStudents count: %v n", len(students))
}
如果您擅长编写 SQL 语句,那么这个软件包生成的类型安全代码是更好的选择,特别是在必须与许多不同类型的架构交互的情况下。
GORM
GORM 有非常全面而简单的入门文档和指南。 不过,GORM 更多地采用基于代码的方法与数据库交互,一开始有一个陡峭的学习曲线。
使用 GORM 语法,您可能需要花费大量时间来最初构造类似于原始 SQL 查询的代码。 然而,当您熟悉了语法,您将拥有一个干净的代码库,很少与原始 SQL 交互。
GORM 是 GitHub 上第二大的 Go 数据库软件包(仅次于 database/sql),拥有超过 30,400 颗星,不会缺少社区支持。
以下示例演示了如何使用 GORM 实现与之前相同的 insert 语句和多个记录查询:
func main() {
// Open a database connection
db, err := gorm.Open(mysql.Open("theuser:thepass@tcp(127.0.0.1:3306)/thedb?charset=utf8mb4&parseTime=True&loc=Local"))
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected!")
// Initialize record to be inserted
s := Student{
Fname: "Leon",
Lname: "Ashling",
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
Email: "lashling5@senate.gov",
Address: "39 Kipling Pass",
Gender: "Male",
}
// Add student record and return the ID into the ID field
db.Create(&s)
fmt.Printf("addStudent id: %v n", s.ID)
// Select multiple records
var students []Student
db.Limit(10).Find(&students)
fmt.Printf("fetchStudents count: %v n", len(students))
}
如以上代码段所示,代码相对简单,只有很少几行。 通过了陡峭的初始学习曲线后,GORM 实际上在编写查询方面比其他 3 个选项更有效。
更多请看另一篇文章
性能和速度
数据库操作的性能和速度对于以数据为中心的应用程序的整体性能至关重要。 最合适的数据库软件包高度依赖于性能,特别是在开发低延迟应用程序时。
本部分比较所有 4 个数据库包的性能。 为进行此基准化分析,已经建立了一个包含 15,000 个学生记录的 MySQL/MariaDB 数据库。 由于这些软件包的大多数用例都是获取记录的查询,基准化分析捕获了所有 4 个软件包获取 1、10、100、1000、10,000 和 15,000 个记录并将其扫描到结构中的性能:
================================== BENCHMARKING 1 RECORDS ======================================
goos: linux
goarch: amd64
pkg: github.com/rexfordnyrk/go-db-comparison/benchmarks
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
Benchmark/Database/sql_limit:1_-8 9054 124134 ns/op
Benchmark/Sqlx_limit:1_-8 8914 138792 ns/op
Benchmark/Sqlc_limit:1_-8 7954 147056 ns/op
Benchmark/GORM_limit:1_-8 13388 89251 ns/op
=================================================================================================
================================== BENCHMARKING 10 RECORDS ======================================
Benchmark/Database/sql_limit:10_-8 7576 157780 ns/op
Benchmark/Sqlx_limit:10_-8 4384 260402 ns/op
Benchmark/Sqlc_limit:10_-8 4183 256384 ns/op
Benchmark/GORM_limit:10_-8 9466 136556 ns/op
=================================================================================================
================================== BENCHMARKING 100 RECORDS ======================================
Benchmark/Database/sql_limit:100_-8 2521 427603 ns/op
Benchmark/Sqlx_limit:100_-8 2139 497755 ns/op
Benchmark/Sqlc_limit:100_-8 2838 456938 ns/op
Benchmark/GORM_limit:100_-8 1896 563539 ns/op
=================================================================================================
================================== BENCHMARKING 1000 RECORDS ======================================
Benchmark/Database/sql_limit:1000_-8 516 2201303 ns/op
Benchmark/Sqlx_limit:1000_-8 445 2786983 ns/op
Benchmark/Sqlc_limit:1000_-8 535 2313674 ns/op
Benchmark/GORM_limit:1000_-8 315 4186201 ns/op
=================================================================================================
================================== BENCHMARKING 10000 RECORDS ======================================
Benchmark/Database/sql_limit:10000_-8 51 21690323 ns/op
Benchmark/Sqlx_limit:10000_-8 38 28458473 ns/op
Benchmark/Sqlc_limit:10000_-8 55 21558300 ns/op
Benchmark/GORM_limit:10000_-8 28 40463924 ns/op
=================================================================================================
================================== BENCHMARKING 15000 RECORDS ======================================
Benchmark/Database/sql_limit:15000_-8 36 32048808 ns/op
Benchmark/Sqlx_limit:15000_-8 28 41484578 ns/op
Benchmark/Sqlc_limit:15000_-8 34 31680017 ns/op
Benchmark/GORM_limit:15000_-8 20 59348697 ns/op
=================================================================================================
PASS
ok github.com/rexfordnyrk/go-db-comparison/benchmarks 77.835s
为了一致性和公平性,基准化分析在相同的硬件上运行。 测试还将每个操作放在单独的函数中并分别测量其性能来确保类似的代码结构。
有许多因素会影响生产服务器的性能,因此我们通常使用简单的基准化分析来尽可能消除外部因素。 虽然此基准化分析使用单条 select 语句,但我们鼓励您修改源代码并尝试更复杂的测试,例如使用连接查询和嵌套结构并获取更高的记录集,以便更好地模拟您自己的生产环境。 您可以使用 GitHub 上此仓库的 benchmarks 代码目录复制基准化分析。
每个结果集分为三列:
运行的基准化分析方法的名称。
在生成可靠时间之前基准化分析运行的次数。
每次执行基准化分析所花费的时间(纳秒)。
对于 1 个记录和 10 个记录的前两个测试,GORM 优于其他库。 但是,随着记录数的增加,它开始明显落后。 就性能而言,sqlx 一直排在第三位,比 GORM 更好,但当数据量增加时,它通常会落后于 sqlc 和 database/sql。
database/sql 和 sqlc 软件包在基准化分析的所有六种情况下都表现出色。 随着获取的记录数量增加(增加到 10,000 和 15,000 个记录),sqlc 比 database/sql 稍快。
结论
虽然 database/sql 是默认的 Golang 软件包,但您也应该视开发需求使用。 本文介绍了每个软件包的优点。
如果您需要高级查询、来自底层数据库技术的完整支持功能以及干净的代码库,那么 GORM 是最适合您的软件包 – 只要您愿意牺牲一些性能。 如果您只需要基本的查询并且愿意编写自己的 SQL,那么 database/sql 或 sqlx 软件包就足够了。
最后,sqlc 最适合大量使用数据库并需要在紧迫的期限内编写大量查询的后端开发者。 您可以编写原始 SQL 查询并生成代码,不必担心类型、扫描或其他影响工作效率的障碍。 sqlc 还提供了巨大的性能提升,尤其是在处理更大量的数据或记录集时。
请注意,由于基准化分析中存在错误,本文已更新。 非常感谢 Lukáš Zapletal 对原始文章发表的评论和提供的 bug 修正。 也感谢 JetBrains 社区提供这样的空间让大家共同学习和做出贡献。