Go语言入门:GORM框架的简单使用 | 青训营

148 阅读12分钟

引言

在软件开发中,而为了更加高效地处理数据库操作,通常会选择使用ORM(对象关系映射)框架,GO语言的开发也不例外。

ORM框架的作用与优势

ORM框架的主要目标是将数据库表和应用程序中的对象模型之间建立映射关系,从而可以通过操作对象来进行数据库操作,而不必直接编写SQL语句。这带来了诸多优势:

  • 抽象化数据库层:ORM框架屏蔽了底层数据库的复杂性,使开发者能够以面向对象的方式进行操作,提升了代码的可读性和可维护性。
  • 提高开发效率:通过ORM,开发者无需手动编写大量重复的SQL语句,从而节省了时间和精力。
  • 跨数据库兼容性:ORM框架通常提供了对多种数据库的支持,这意味着您可以轻松地切换数据库引擎,而无需更改大部分代码。
  • 安全性和预防SQL注入:ORM框架通常会处理输入参数的转义和验证,从而降低了SQL注入攻击的风险。
  • 可扩展性:ORM框架提供了灵活的扩展机制,允许您添加自定义逻辑,以满足特定需求。

GORM作为Go语言中最受欢迎的ORM框架之一,为开发者提供了简化和抽象化数据库操作的能力。而在接下来的内容中,我们将探讨如何使用GORM框架来进行数据库操作。

安装与连接

在开始使用GORM框架进行数据库操作之前,我们需要进行初始的设置,包括安装GORM以及与数据库建立连接。这一部分将引导您完成GORM的安装和数据库连接过程。

GORM安装

首先,确保项目已经启用了Go Modules,这将帮助管理项目的依赖关系。在项目的根目录下,使用以下命令初始化Go Modules:

 go mod init <your_module_name>

接下来,需要安装GORM。可以使用go get命令来获取并安装GORM库:

 go get -u gorm.io/gorm

PS:参数 -u 表示更新已安装的包到最新版本。Go工具将会检查已经安装的包,并尝试更新到包的最新版本(如果有新版本可用)。如果包之前没有被安装,-u 参数将会被忽略,而直接进行包的安装。

数据库连接

GORM支持多种数据库,可以根据项目需求选择合适的数据库类型。下面将分别介绍如何连接MySQL和SQLite数据库,其余的数据库可以根据GORM官网教程进行连接。

MySQL数据库连接

对于MySQL数据库,需要首先安装相应的数据库驱动,通过以下命令来下载MySQL驱动:

 go get -u gorm.io/driver/mysql

接下来,可以使用以下示例代码来连接MySQL数据库(这里只介绍简单的连接方式,其余方式可参考GORM官网教程):

 package main
 ​
 import (
     "gorm.io/gorm"
     "gorm.io/driver/mysql"
 )
 ​
 func main() {
     user := "your_mysql_user"  // mysql用户名
     password := "your_mysql_password"  // mysql用户对应的密码
     database := "your_mysql_database"  // 需要使用的mysql数据库
     dsn := user + ":" + password + "@tcp(127.0.0.1:3306)/" + database + "?charset=utf8mb4&parseTime=True&loc=Local"
     // 连接MySQL数据库
     db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
     if err != nil {
         panic("连接数据库失败:" + err.Error())
     }
     defer db.Close()
 }

SQLite数据库连接

同样,对于SQLite数据库,需要下载相应的数据库驱动。通过以下命令来获取SQLite驱动:

 go get -u gorm.io/driver/sqlite

然后,可以使用以下示例代码来连接SQLite数据库:

 package main
 ​
 import (
     "gorm.io/gorm"
     "gorm.io/driver/sqlite"
 )
 ​
 func main() {
     // 连接SQLite数据库
     db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
     if err != nil {
         panic("连接数据库失败:" + err.Error())
     }
     defer db.Close()  // 函数执行结束后关闭连接
 }

上述代码将连接到SQLite数据库,并连接名为 "test.db" 的数据库文件,如果没有那么将创建这个文件。

编写ORM实体

ORM实体的概念与作用

ORM实体是连接对象和数据库表之间的桥梁。通过创建ORM实体,就可以通过Go代码直观地操作数据库,而无需编写冗长的SQL语句。GORM会根据定义的结构体自动生成适当的SQL查询和更新操作,大大减少了手动编写数据库操作代码的工作量。

定义ORM实体

让我们以一个名为 "User" 的ORM实体为例,来演示如何定义ORM实体以及如何映射表中的字段和关系。

 type User struct {
     ID          int64     `gorm:"primaryKey"`
     Username    string    `gorm:"uniqueIndex"`
     Password    string    `gorm:"size:255"`
     PhoneNumber string    `gorm:"size:19;default:''"`
     Email       string    `gorm:"size:64;default:''"`
     Status      int8      `gorm:"type:tinyint;default:0"`
     Deleted     bool      `gorm:"default:false"`
     CreatedAt   time.Time `gorm:"default:CURRENT_TIMESTAMP"`
     UpdatedAt   time.Time `gorm:"default:CURRENT_TIMESTAMP"`
 }
 ​
 func (User) TableName() string {
     return "user"
 }

上述代码中有一个名为 User 的ORM模型,它表示数据库中的一个用户表。以下是这段代码的解释:

  1. type User struct { ... }:定义了一个名为 User 的结构体,它将映射到数据库中的用户表。
  2. ID int64 gorm:"primaryKey":这是 User 结构体中的字段,它表示用户的唯一标识符。primaryKey 标签指定了这个字段是主键,这意味着它将被用作表中记录的唯一标识。
  3. Username string gorm:"uniqueIndex":这是用户的用户名字段,用来存储用户名。uniqueIndex 标签指定了这个字段应该有唯一索引,确保用户名在表中是唯一的。
  4. Password string gorm:"size:255":这是用户的密码字段,用来存储用户的密码。size 标签设置了该字段的最大长度为 255 个字符。
  5. PhoneNumber string gorm:"size:19;default:''":这是用户的电话号码字段,用来存储电话号码。size 标签设置了该字段的最大长度为 19 个字符,default 标签设置了字段的默认值为空字符串 ''
  6. Email string gorm:"size:64;default:''":这是用户的电子邮件字段,用来存储电子邮件地址。size 标签设置了该字段的最大长度为 64 个字符,default 标签设置了字段的默认值为空字符串 ''
  7. Status int8 gorm:"type:tinyint;default:0":这是用户的状态字段,用来存储用户的状态。type 标签设置了数据库字段类型为 tinyintdefault 标签设置了字段的默认值为 0
  8. Deleted bool gorm:"default:false":这是用户的删除标志字段,用来表示用户是否已被逻辑删除。default 标签设置了字段的默认值为 false
  9. CreatedAt time.Time gorm:"default:CURRENT_TIMESTAMP":这是用户记录的创建时间字段。default 标签设置了字段的默认值为当前时间。
  10. UpdatedAt time.Time gorm:"default:CURRENT_TIMESTAMP":这是用户记录的更新时间字段。default 标签设置了字段的默认值为当前时间。
  11. func (User) TableName() string:这个方法用来指定 User 结构体对应的数据库表名。在这里,它返回字符串 "user",表示该模型映射到名为 "user" 的数据库表。

PS:使用 GORM 时,通常会自动将驼峰命名的字段转换为小写下划线命名,比如PhoneNumber在数据库中为phone_number

下面是一些常用的gorm标签和对应的SQL语句以及作用:

标签SQL 语句作用
primaryKeyINT AUTO_INCREMENT PRIMARY KEY将字段标记为主键,通常用于自增主键的字段。
uniqueIndexUNIQUE INDEX为字段创建唯一索引,确保值在整个表中是唯一的。
indexINDEX为字段创建普通索引,优化字段的查询性能。
column自定义列名自定义字段在数据库表中的列名,覆盖默认的列名。
size限制字段长度定义字段的长度限制,用于限制字符串字段的大小。
defaultDEFAULT设置字段的默认值,当插入数据时,如果未提供值,将使用默认值。
not nullNOT NULL指示字段不能为空,通常与其他标签一起使用,如 primaryKey
autoCreateTime自动设置创建时间字段,如 created_at自动设置字段的创建时间,通常与 gorm.Model 结构体一起使用。
autoUpdateTime自动设置更新时间字段,如 updated_at自动设置字段的更新时间,通常与 gorm.Model 结构体一起使用。
uniqueUNIQUE创建字段级的唯一约束,确保该字段的值在表中是唯一的。
foreignKeyFOREIGN KEY为字段创建外键关系,将其与另一个表的主键关联起来。
indexINDEX为字段创建普通索引,提升查询性能。
uniqueIndexUNIQUE INDEX为字段创建唯一索引,确保值在整个表中是唯一的。
.........

使用这些标签可以使用gorm的自动生成数据表的功能建立对应的数据表,使得数据表的建立更加方便也保障了数据的对应。

此外再创建Post实体和PostUser实体方便后面的例子代码使用

 type Post struct {
     ID        int64     `gorm:"primaryKey"`
     Title     string    `gorm:"size:255"`
     Content   string    `gorm:"type:text"`
     Status    int8      `gorm:"type:tinyint;default:0"`
     Deleted   bool      `gorm:"default:false"`
     CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"`
     UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"`
 }
 ​
 func (Post) TableName() string {
     return "post"
 }
 ​
 type PostUser struct {
     ID     int64 `gorm:"primaryKey"`
     UserID int64 `gorm:"index:user_post_idx"`
     PostID int64 `gorm:"index:user_post_idx"`
 }
 ​
 func (PostUser) TableName() string {
     return "post_user"
 }

创建好ORM实体后可以使用gorm的创建数据表函数根据定义的实体和里面的标签创建数据表,例如:

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
    panic("连接数据库失败, error=" + err.Error())
}

// 根据定义的orm实体自动创建数据表
err = db.AutoMigrate(&User{}, &Post{}, &PostUser{})
if err != nil {
    return
}

数据库操作:增删查改

在本部分,将介绍如何使用 GORM 框架进行简单的数据 CRUD 操作。

CREATE

单条插入

首先,让我们看看如何向数据库中插入一条数据。我们将使用 User 结构体作为示例:

user := User{  // 定义待插入的user结构体
    Username:    "Mike",
    Password:    "password0",
    PhoneNumber: "123456789",
    Email:       "alice@example.com",
}
result := db.Create(&user)  // 插入数据并获取返回
if result.Error != nil {
    panic("插入数据失败:" + result.Error.Error())
}

多条插入

如果我们需要一次性插入多条数据,可以通过如下方式实现:

users := []User{
    {Username: "Trev", Password: "password1"},
    {Username: "Franklin", Password: "password2"},
}
result = db.Create(&users)
if result.Error != nil {
    panic("插入数据失败:" + result.Error.Error())
}

原子性插入

有时,我们需要保证一组插入操作要么全部成功,要么全部失败。这称为原子性操作。以下是如何实现这样的操作:

// 开始事务
tx := db.Begin()

// 1. 插入一篇文章
newPost := Post{
    Title:   "新文章标题",
    Content: "新文章内容",
    Status:  0,
}
if err := tx.Create(&newPost).Error; err != nil {
    tx.Rollback()  // 插入失败回滚事务
    panic("插入文章失败:" + err.Error())
}

// 2. 获取刚刚插入的文章的ID
newPostID := newPost.ID

// 3. 在PostUser关联表中插入数据
postUser := PostUser{
    UserID: userID,
    PostID: newPostID,
}
if err := tx.Create(&postUser).Error; err != nil {
    tx.Rollback()  // 插入失败回滚事务
    panic("插入关联数据失败:" + err.Error())
}

// 提交事务
tx.Commit()

数据库事务的特性,包括一致性(Consistency)、原子性(Atomicity)、隔离性(Isolation)和持久性(Durability),通常被称为 ACID 特性。在上述示例中,我们使用 db.Begin() 开始了一个事务,然后通过 tx.Create()user 表和 post 表分别插入数据。如果其中一个插入操作失败,我们会回滚事务并输出错误信息。如果两个插入操作都成功,我们会通过 tx.Commit() 提交事务。

READ

基础查询

现在来学习如何从数据库中进行基本的查询操作:

// single
var retrievedUser User
result := db.First(&retrievedUser, "username = ?", "Mike")
if result.Error != nil {
    panic("查询数据失败:" + result.Error.Error())
}
fmt.Println("查询到的用户:", retrievedUser)

// multi
var users []User
result = db.Where("status = ?", 0).Find(&users)
if result.Error != nil {
    panic("查询数据失败:" + result.Error.Error())
}
fmt.Println("查询到的用户列表:", users)

分页和连表查询

如果我们需要实现分页查询或进行连表查询,可以采用以下方法:

// 分页查询
var users []User
offset := (page - 1) * pageSize
result := db.Offset(offset).Limit(pageSize).Find(&users)  // 通过offset和limit实现分页操作
if result.Error != nil {
    panic("查询数据失败:" + result.Error.Error())
}
fmt.Println("分页查询用户列表:", users)

// 连表查询
var posts []Post
result := db.Joins("JOIN post_user ON post.id = post_user.post_id").
Where("post_user.user_id = ?", userID).Find(&posts)
if result.Error != nil {
    panic("查询数据失败:" + result.Error.Error())
}
fmt.Println("用户ID为", userID, "的帖子列表:", posts)

UPDATE

在实际应用中,我们经常需要更新数据库中的数据。以下是如何更新用户密码的示例:

result := db.Model(&User{}).Where("username = ?", "Mike").Update("password", "new_password")
if result.Error != nil {
    panic("更新数据失败:" + result.Error.Error())
}
fmt.Println("更新密码成功")

上面的代码演示了如何通过用户名来更新用户的密码。我们使用Updata函数对数据库中username为Mike的记录密码更新为新的密码。

DELETE

当需要从数据库中删除记录时,我们通常还需要注意关联的数据,确保数据的完整性。以下示例演示了如何删除一条 post 记录,并同时删除关联的 PostUser 记录:

tx := db.Begin()  // 开始事务

// 1. 删除关联的 PostUser 记录
if err := tx.Where("post_id = ?", postID).Delete(&PostUser{}).Error; err != nil {
    tx.Rollback()
    panic("删除关联 PostUser 失败:" + err.Error())
}

// 2. 删除 Post 记录
if err := tx.Delete(&Post{}, postID).Error; err != nil {
    tx.Rollback()
    panic("删除 Post 失败:" + err.Error())
}

tx.Commit()  // 提交事务

在上述代码中,使用事务来确保删除操作的原子性。首先删除与之关联的 PostUser 记录,然后删除指定的 post 记录。如果任何一步操作失败,事务会回滚,确保数据的完整性。

PS:一般情况下不会删除真正的数据,而是逻辑删除,也就是定义一个删除字段来表示数据被删除。

简单的性能优化

GORM 提供了一些方法来提升数据库操作的效率。以下是一些性能优化的建议和实践:

  • 使用预加载:通过 Preload 函数可以在查询数据的同时预加载关联数据,减少 N+1 查询问题。
  • 批量操作:使用 CreateInBatchesUpdateColumn 等函数可以批量插入、更新数据,减少单条操作的开销。
  • 使用索引:合理地为表字段创建索引可以加速查询操作。
  • 避免不必要的查询:仅查询所需的字段,避免拉取大量无用数据。

总结

本文介绍了 GORM 框架在数据库操作方面的基本使用方法和技巧。通过内容可以更加轻松地进行数据库的增删查改操作。如果还想深入了解更多关于 GORM 的高级用法和特性,可以查阅官方文档以及相关教程。

鉴于本人水平有限,如果文章中出现错误希望指出,感谢您的阅读。