亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

用 Go 處理 SQL 數據庫的方法

標簽:
MySQL Go 數據庫

点击这里阅读原文(在packagemain.tech)

不同的编程语言在处理关系型数据库和SQL时有自己的方法。Ruby on Rails 有自己的 Active Record,Python 有 SQLAlchemy,TypeScript — Drizzle,等等。Go 作为一种拥有多样化标准库的语言,其中就有著名的 database/sql 包,也有适合不同需求和团队的 SQL 库和解决方案。

在这篇文章中,我们将探索和比较最常用的 Go 包,并通过实际示例来展示它们的优点和缺点。我们还将简要讨论数据库迁移以及如何在 Go 中进行管理。如果你已经有一些 Go、SQL 和关系型数据库的经验,无论使用哪种数据库,你将从这篇文章中获得最大的收获。

演示模板

在这篇文章中,我们将使用一个简单的结构,包含三个表:userspostsblogs。为了简单,我们将使用 SQLite 作为我们的数据库引擎,如果选择其他数据库引擎也不是问题,因为我们即将探讨的所有库都支持多种 SQL 方言。

这是我们数据库的SQL结构。

    创建一个表 users (  
        id INTEGER 主键 自增,  
        name TEXT NOT NULL,  
        email TEXT NOT NULL 唯一  
    );  

    创建一个表 blogs (  
        id INTEGER 主键 自增,  
        name TEXT NOT NULL,  
        url TEXT NOT NULL 唯一  
    );  

    创建一个表 posts (  
        id INTEGER 主键 自增,  
        title TEXT NOT NULL,  
        content TEXT NOT NULL,  
        user_id INTEGER NOT NULL,  
        blog_id INTEGER NOT NULL,  
        外键 (user_id) 关联到 users (id) ON DELETE CASCADE,  
        外键 (blog_id) 关联到 blogs (id) ON DELETE CASCADE  
    );

这里就是它的实体关系图(ERD,即实体关系图)。(点击链接了解更多信息):

纯 SQL 和 database/sql

假设你的App需要做以下事情:

找到至少发表了两篇文章的用户,以及他们发布文章的总数量。

在纯SQL中,可以写成如下查询:

    SELECT u.名字, COUNT(p.id) AS 帖子数  
    FROM 用户 AS u  
    JOIN 帖子 AS p ON u.id = p.用户ID  
    GROUP BY u.id  
    HAVING 帖子数 >= 2;

这个查询的简短解释:我们将用户表和帖子表进行联接,然后按 user_id 进行分组,HAVING 子句用来筛选结果,只包括至少发布了两条帖子的用户,COUNT 函数用于统计帖子的数量。

如上所述,Go 提供了一个内置包 database/sql,其中包含操作 SQL 数据库所需的所有工具。它设计简洁,但支持所有必要的功能,例如事务、参数查询、连接池管理等。

只要你能够自如地编写查询并处理错误和分析结果,这便是一个很好的选择,如果你觉得。有些人认为这是最好的选择,因为没有隐藏的逻辑,你可以随时复制查询并用 EXPLAIN 命令进行分析。

这里是用 Go 代码通过 database/sql 包获取上面提到的查询结果的方法(这里省略了某些部分,比如连接部分):

type 用户统计 struct {  
  用户名  sql.NullString  
  发帖数 sql.NullInt64  
}  

func 获取用户统计(conn *sql.DB, minPosts int) ([]用户统计, error) {  
  查询 := `SELECT u.名称, COUNT(p.id) AS 发帖数  
FROM 用户 AS u  
JOIN 发帖 AS p ON u.id = p.user_id  
GROUP BY u.id  
HAVING 发帖数 >= ?;`  

  rows, err := conn.Query(查询, minPosts)  
  if err != nil {  
    return nil, err  
  }  
  defer rows.Close()  

  用户 := []用户统计{}  
  for rows.Next() {  
    var user 用户统计  
    if err := rows.Scan(&user.用户名, &user.发帖数); err != nil {  
      return nil, err  
    }  
    用户 = append(用户, user)  
  }  

  if err := rows.Err(); err != nil {  
    return nil, err  
  }  

  return 用户, nil  
}

我们在这段代码中:

  • 使用具有未命名参数的原始SQL查询,并将该参数的值传递给conn.Query()
  • 遍历返回的每一行,并将每一行手动扫描到上面定义的userStats结构体中。注意,该结构体使用**sql.Null***类型来正确处理空值。
  • 我们还需要手动检查可能发生的错误,并关闭行以释放资源。

优点:

  • 不增加额外的抽象或复杂性。便于调试原始SQL语句。
  • 性能表现非常出色。
  • 提供了足够好的跨不同数据库后端的抽象。

不足 :

  • 代码稍微显得啰嗦,因为需要扫描每一行,定义合适的数据类型,同时还要处理可能出现的错误。
  • 缺乏编译时的类型安全性检查。

您可以在我们的Github仓库找到这里的完整代码。

原始 SQL 和 sqlx

我们现在来看看在 Go 社区里很受欢迎的一些第三方包。

如果你已经熟悉 database/sql 并且觉得它简单,你可能会喜欢使用 sqlx 这个库,它是基于标准库构建的,并且只是在原有功能的基础上增加了更多特性。因为它没有改变底层的接口(比如 sql.DBsql.Tx 等),所以很容易把现有的 database/sql 代码库与 sqlx 结合。

sqlx的一些关键特点包括:

  • 命名参数。
  • 更方便地将带嵌套结构支持的行扫描为结构体。
  • 通过使用 Get()Select() 方法可以更好地区分单行和多行数据。
  • 可以将值切片作为 IN 查询中的单参数进行绑定。

下面是如何通过sqlx获取上述查询结果如下所示:

    type userStats struct {  
      // 用户统计结构体,包含用户名和发帖数量  
      UserName  string `db:"name"`  
      PostCount string `db:"post_count"`  
    }  

    func getUsersStats(conn *sqlx.DB, minPosts int) ([]userStats, error) {  
      // 获取用户统计信息,返回满足发帖数条件的用户列表  
      users := []userStats{}  

      // SQL 查询语句,用于获取用户统计信息  
      query := `SELECT u.name, COUNT(p.id) AS post_count  
    FROM users AS u  
    JOIN posts AS p ON u.id = p.user_id  
    GROUP BY u.id  
    HAVING post_count >= ?;`  

      // 处理 SQL 查询返回的错误,如果查询失败则返回错误信息  
      if err := conn.Select(&users, query, minPosts); err != nil {  
        // 返回错误信息  
        return nil, err  
      }  

      // 返回用户统计列表和 nil 错误  
      return users, nil  
    }

在这个代码中,我们使用了 Select() 方法来扫描行。它还会自动关闭行,因此我们无需担心这些细节。代码比 database/sql 版本要短很多,但可能会隐藏一些实现细节,比如 Select 会一次性把整个数据集加载到内存里。

优点:

  • 和 database/sql 差不多,没有太大的区别。调试原始 SQL 查询仍然非常方便。
  • 有许多功能可以减少代码冗余,让编写代码更加简洁。

不足:

  • 同 database/sql 一样
对象关系映射 (ORM)

对象关系映射(ORM)是一种技术,有时也被认为是一种设计模式,通过操作对象来访问数据库,无需编写复杂的SQL语句。它在面向对象的语言中非常流行,特别是在 Ruby on Rails 中有其 Active Record,Python 中有 SQLAlchemy,TypeScript 和 Drizzle 等等之类的。

简单来说,Go 有 GORM。它允许你用 Go 代码编写查询,通过调用对象上的各种方法,这些方法最终会被转换成 SQL 查询。不仅如此,它还有更多功能,比如数据库迁移、数据库解决工具等。

你可能需要花一点时间初始化配置你的 GORM 模型定义,但之后可以大大减少重复代码。

我们的简单模式和查询示例可能不太适合展示 GORM 的强弱点,但应该足够用来展示我们如何运行类似查询并扫描结果。

    type User struct {  
      gorm.Model  
      ID    int  
      Name  string  
      Posts []Post  
    }  

    type Post struct {  
      gorm.Model  
      ID     int  
      UserID int  
    }  

    type userStats struct {  
      Name  string  
      Count int `gorm:"column:post_count"`  
    }  

    func getUsersStats(conn *gorm.DB, minPosts int) ([]userStats, error) {  
      var users []userStats  

      err := conn.Model(&User{}).  
        Select("name", "COUNT(p.id) AS post_count").  
        Joins("JOIN posts AS p ON users.id = p.user_id").  
        Group("users.id").  
        Having("post_count >= ?", minPosts).  
        Find(&users).Error  

      return users, err  
    }

gorm 生成的 SQL 查询大致相同于我们在使用 database/sql 的情况下手动编写的查询。

简单总结一下上面这段代码

  • 我们声明了 User 和 Post 模型,并扩展了默认的 gorm.Model 结构。之后,我们可以使用这些模型并通过 gorm 方法构建我们所需的任何查询。
  • 我们还定义了自己的小结果类型 userStats
  • 我们使用了 Select()、Joins()、Group()、Having() 等方法来创建我们想要的查询。

这么简单的例子很难发现潜在的问题,一切都看起来挺好的。不过,等你的项目变得复杂起来时,你肯定会遇到不少麻烦。看看标记为go-gorm的问题。

在需要高性能的系统或需要直接控制数据库交互的地方谨慎使用ORM是明智的,因为golang中的gorm框架会大量使用反射机制,并可能会增加额外的负担,有时会使底层数据库操作变得不透明。任何将功能封装在另一个庞大层次中的项目都会增加整体复杂度的风险。

好处:

  • 从不同的数据库后端进行抽象。
  • 功能丰富:如迁移、钩子、数据库解析器等。
  • 大大减少了繁琐的编码工作。

不足:

  • 增加了复杂性和开销。调试原始SQL查询很困难。
  • 性能上的劣势。对于某些关键应用可能不够高效,甚至会成为瓶颈。
  • 设置初期可能需要花时间配置所有模型。

使用sqlc从SQL生成的Go代码

这很自然地将我们引向另一个独特的从SQL查询生成Go代码的方法。使用sqlc,你可以编写你的模式(schema)和SQL查询,然后使用命令行工具从这些模式和查询生成Go代码,并使用生成的代码与数据库进行交互。

它确保查询语句既正确又安全,非常适合喜欢写SQL但又希望高效地将其集成到Go应用中的人。

sqlc 需要知道你的数据库模式和查询,才能生成代码,因此它需要一些初始设置,例如配置文件。我们的模式和查询可以分别添加到 schema.sqlquery.sql 文件中,并使用以下配置生成 Go 代码,如下所示:

    版本号: "2"  
    sql:  
      - 数据库引擎: "sqlite"  
        SQL查询文件: "query.sql"  
        SQL架构文件: "schema.sql"  
        gen:  
          go:  
            Go包: "main"  
            输出目录: "."

我们还需要在 query.sql 中给查询命名,并标出参数。

    -- name: 获取用户帖子统计 :many  
    SELECT u.name, COUNT(p.id) AS 发帖数  
    FROM 用户表 AS u  
    JOIN 帖子表 p ON u.id = p.user_id  
    按用户ID分组  
    其中发帖数大于等于?;

运行 sqlc generate 之后,我们可以利用以下生成的类型和函数,这使得我们的代码既安全又简洁。

func 获取用户统计数据(conn *sql.DB, minPosts int) ([]用户统计数据行, error) {  
  queries := New(conn)  
  ctx := context  
  ctx = context.Background()  
  return queries.获取用户统计数据(ctx, minPosts)  
}

sqlc特别的地方在于它能理解你的数据库模式,并据此验证你编写的SQL。你的SQL查询会根据实际的数据库表被验证,如果出错,sqlc会在编译时给出错误提示。

好的地方:

  • 具有类型安全特性的生成Go代码。
  • 调试SQL代码依然简单。
  • 省去了很多枯燥的编码工作。
  • 卓越的性能。

不足

  • 数据库模式和查询的初始配置。
  • 静态分析不是完美的。有时你需要明确指定参数类型等参数。

如果你熟悉SQL语句,并且更喜欢避免编写大量代码来处理数据库交互,这个包就很适合你。

数据库迁移

既然我们在这里谈到了SQL数据库,让我们简短地回顾一下Go中数据库迁移的相关内容。数据库的结构几乎总是随着时间的推移而变化,没有人愿意手动完成这些改动,因此开发了相应的工具来帮助完成这些工作。

数据库迁移软件的主要目标是确保所有环境的数据库模式相同,并使开发人员能够轻松地应用或回滚变更。

正如我们之前所说,如果项目使用 GORM 作为 ORM,它也可以执行迁移。如果你使用的是 database/sql、sqlx 或 sqlc,你就得用其他工具来管理这些。

这些最受欢迎的项目有:

  • golang-migrate — 处理数据库迁移的著名工具之一。它支持许多数据库驱动和迁移来源,并且采用简单直接的方式处理数据库迁移。
  • goose — 在选择迁移工具时的另一个可靠选项。它也支持主要数据库驱动,其主要特性之一是支持用 Go 语言编写的迁移以及对迁移应用过程的更多掌控。

你可以将这些工具直接集成到你的应用程序或CI/CD流程中。然而,在CI/CD中正确运行它们则需要一些配置(例如在部署到Kubernetes集群时),我们将在即将发布的一系列文章中进一步探讨这一话题。

结论

有许多优质且经过测试支持的数据库包可供 Go 语言使用,这些包可以帮助加速开发并保持代码整洁。标准库中还提供了一个非常强大的 database/sql 模块,可以处理你日常工作的大部分任务。

不过,是否使用它取决于你的需求、喜好以及项目情况。在这篇文章里,我们试图强调它们的强项和弱项。

你可以在这个文章的完整源代码中找到我们的Github仓库

我将用这个流行的梗作为结尾。

资源
點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消