Tutorials

比较 database/sql、GORM、sqlx 和 sqlc

Read this post in other languages:

本文由外部贡献者撰写。

Rexford A. Nyarko

Rexford A. Nyarko

Rexford Nyarko 是一名解决方案架构师,具有软件与网络系统、云环境、服务器管理和多种数据库技术背景。 Rexford 特别喜欢向非技术人员解释技术概念。

LinkedIn Twitter

Go 以其简单易用、性能和完整的标准库而闻名。 Go 的库开箱即支持大量常见操作,许多其他语言需要第三方库来执行或根本不支持。 这些操作位于处理并发、网络、I/O 和文本处理等功能的各种软件包中。

database/sql 是一个标准库软件包,负责与数据库(主要是 SQL 关系数据库)的连接和交互。 它为类 SQL 交互提供泛型接口、类型和方法。 尽管它支持许多基本的现代数据库功能,例如事务和预备语句,但它也有一些局限性。 例如,它存在类型限制,无法将大的 uint64 值作为形参传递给语句。

本文将 database/sql 软件包与其他 3 个 Go 软件包 sqlxsqlcGORM 进行比较。 这 4 种工具的比较集中在 3 个主要方面:

  • 功能 – 探索核心功能,包括工具的特点和不足。
  • 易用性 – 通过对每个工具执行相同操作的代码示例演示相对易用性。
  • 性能/速度 – 通过使用标准库测试软件包中的工具对一些数据库操作进行基准化分析来衡量性能。

功能

本部分介绍每个软件包提供的功能。 这种比较使用 database/sql 软件包的基本功能作为功能方面的最低要求,因为所有软件包都是为了扩展默认软件包提供的功能而创建的。

database/sql

database/sql 在创建时将简单易用纳入考量,配置为支持与类 SQL 数据库交互所需的最基本必要功能。

为了与数据库管理系统交互,数据库软件包需要适当的驱动程序。 目前,database/sql 支持超过 50 种数据库驱动程序,涵盖 SQLite、MySQL/MariaDB、PostgreSQL、Oracle 和 MS SQL Server 等最流行的 DBMS。

此软件包还支持基本的 CRUD 操作、数据库事务、命名形参、返回多个结果集、可取消查询、SQL 类型支持、连接池管理、形参化查询和预备语句等功能。

sqlx

创建 sqlx 是为了扩展标准库数据库软件包的功能。 由于它依赖 database/sql 软件包,后者提供的所有功能也可用,包括对同一组数据库技术和驱动程序的支持。

除了这些核心功能之外,sqlx 还具有以下优点:

  • 带有命名形参的预备语句 – 这使您能够使用结构字段的名称和映射键绑定预备语句或查询中的变量。
  • 结构扫描 – 这允许您将查询结果直接扫描到单行的结构中,不必像 database/sql 那样单独扫描每个字段或列。 它还支持扫描到嵌入式结构。
  • SelectGet – 这些是用于处理预期将多个记录或单个记录分别返回到结构的切片或单个结构的查询的便捷方法。 不需要循环结果集!
  • 对 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 语句,并且不喜欢使用太多代码执行数据库操作和处理数据,那么这个软件包绝对适合您。

GORM

GORM 是面向 Go 语言的全功能对象关系映射器 (ORM) 库。 它允许您通过在对象上调用各种方法将查询编写为 Go 代码,与数据库交互时,GORM 将其转换为 SQL 查询。 它为数据库交互提供了一组丰富的功能,其中一些如下所示:

  • 数据库和驱动程序 – 根据官方文档,GORM 支持 MySQL/MariaDB、PostgreSQL、SQLite、SQL Server 和 ClickHouse。 它还为这些数据库系统提供驱动程序。 同时,您可以将现有 database/sql 连接传递给它,使其可能与 database/sql 软件包支持的所有其他数据库和驱动程序兼容。
  • 迁移 – GORM 支持数据库迁移管理,用于修改架构或还原更改。 它为此做出了各种预配,例如能够从结构创建表。
  • 关系 – 您可以在标记和嵌套结构中使用模型或带注解的结构来定义数据库关系。 可以实现一对一、多对多、一对多的关系。
  • 事务 – GORM 提供对事务、嵌套事务、提交和回滚事务等的支持。
  • 数据库解析器 – 这允许您在一个应用程序中访问多个数据库和架构源。 这还允许根据您正在访问的结构或数据库可用性和负载平衡进行连接切换(自动和手动)。
  • SQL 构建器 – 提供的 SQL 构建器支持各种形式的 SQL 和 SQL 子句。 包括但不限于 CRUD 操作、子查询、预备语句、命名实参、upsert、SQL 表达式、join 等。
  • 挂钩 – 这些接口可以在执行 DML 语句之前或之后执行某些活动。

这是此软件包功能的非详尽列表;它具有大量其他功能,在几乎所有用例中都表现良好。

易用性

为了最大限度地提高工作效率,库对于经验丰富的开发者和新手来说都应易于使用。 本部分着眼于开始使用每个库的难易程度,考量可用的文档、教程(文本和视频)和社区支持。 本部分还会介绍一些常见操作的代码示例和相关的学习曲线。

本部分的代码示例位于 GitHub 上此仓库examples 目录中。 要运行代码示例,您可以连接到 GoLand 中的数据库。 您可以为此使用 MySQLMariaDB。 确保已安装其中任何一个。

要连接到数据库,请打开 Database(数据库)工具窗口,点击加号图标,然后选择 Add data source(添加数据源)。

为设置的数据库连接提供一个名称,并为设置提供所有必要凭据。 此示例使用 localhost 作为主机、默认 MySQL/MariaDB 端口 3306、名为 theuser 的用户、值为 thepass 的隐藏密码,以及名为 thedb 的数据库。 如果先前没有下载过必要的驱动程序,GoLand 将建议下载。 最后,点击屏幕左下角的 Test Connection(测试连接)链接。 这将使用您提供的用户和密码凭据尝试建立与指定数据库的连接来验证凭据。

您应该已经连接到数据库了。 接下来,可以运行 setup.sql 脚本 – 只需打开它并按左上角的绿色箭头或按 ⌘⏎ /Ctrl+Enter 执行语句。

如果一切顺利,您将在 tables 列表中看到您的表。 您可以双击 students 表来查看行。

database/sql

对于有经验的开发者,database/sql 有技术先进的详细文档。 此外,也有一个面向初学者的出色教程。 语法非常简单,但您需要经常检查错误,编写更多代码。 作为标准库中的首选软件包,它还拥有整个 Go 开发社区的大量追随者和强大支持。

如果您习惯编写自己的 SQL 查询,这个软件包也是一个不错的选择,特别适合基本查询和基本类型或具有少量字段的结构。

以下代码段演示了如何使用带有 MySQL 驱动程序的 database/sql 软件包插入记录:

// Function to add a record and return the record ID if successful, if not then an error
func addStudent(s Student) (int64, error){
    // Insert statement listing all the fields and providing their respective values 
    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)
    }
    // Check to get the ID of the inserted record
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("addStudent Error: %v", err)
    }
    return id, nil
}

正如您所看到的,插入操作与编写直接 SQL 语句非常相似。 您还将需要分别输入每个字段及其关联值。 然而,随着时间推移,在大型结构或复杂类型中维护代码会变得很麻烦,增加引入错误的机会,而这些错误可能只能在运行时被捕获。

以下代码段演示了如何使用 database/sql 软件包检索多个记录:

// Function to fetch multiple records 
func fetchStudents() ([]Student, error) {
    // A slice of Students to hold data from returned rows
    var students []Student
    rows, err := db.Query("SELECT * FROM students")
    if err != nil {
        return nil, fmt.Errorf("fetchStudents %v", err)
    }
    defer rows.Close()
    // Loop through rows, using Scan to assign column data to struct fields
    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() 将每个记录的每个字段单独扫描到一个结构中,然后将结构附加到一个切片。 这里需要小心,因为提供的结构中的字段数必须等于返回的记录中的字段数。 您还应该注意,很难使用 IN 子句处理查询,因为它将 IN bindvar 值视为单个值,而不是多个值。

sqlx

sqlx 还提供了详细的技术文档。 由于它主要是 Go database/sql 软件包的扩展,还有一个关于将此软件包与 database/sql 共同使用的综合指南

在 GitHub 上,sqlx 获得了超过 12,800 颗星,拥有大量关注者和非常活跃的问题列表。

如果您有 database/sql 背景,您将顺利过渡,因为查询在语法上相似且兼容。 从以下代码段中,您会注意到与先前的 database/sql 代码的很多相似之处,尤其是 insert 语句:

// Add inserting student record using the sqlx package
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 便捷方法检索多个记录:

// Function to fetch multiple records 
func fetchStudents() ([]Student, error) {
    // A slice of Students to hold data from returned rows
    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.godb.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. 运行的基准化分析方法的名称。
  2. 在生成可靠时间之前基准化分析运行的次数。
  3. 每次执行基准化分析所花费的时间(纳秒)。

对于 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 社区提供这样的空间让大家共同学习和做出贡献。

image description

Discover more