GORM简易实践 | 青训营

35 阅读5分钟

本文会使用GORM连接本地SQLite数据库,并简单实现对数据库的增删改查操作。

运行环境

  • MacOS
  • SQLite3数据库
  • Go 1.21.0 & GoLand

实现过程

首先,导入GORM依赖。

import "gorm.io/gorm"

定义模型

假设我们需要管理每个分公司下的所有员工信息,就会产生一张Staff表,其中包含员工号ID、分公司号BranchNo、员工姓名Name、员工性别Sex、员工职位Position、员工出生日期DOB、员工工资Salary这些字段,而员工号ID作为每个表项的主键(primaryKey)。

于是,在Go中我们定义一个名为Staff的结构体来表示上述的表模型。

type Staff struct {
    ID       uint      `gorm:"primaryKey"`
    BranchNo string    `gorm:"column:BranchNo;not null"`
    Name     string    `gorm:"column:Name"`
    Sex      string    `gorm:"column:Sex"`
    Position string    `gorm:"column:Position"`
    Birthday time.Time `gorm:"column:DOB"`
    Salary   uint      `gorm:"column:Salary"`
}

我们可以使用Go tag对结构体内对字段进行说明,以控制这些字段在数据库中的列名或者是对这些字段进行约束和规定。
例如:gorm:"primaryKey"用于指定主键、gorm:"column:[name]"用于指定列名、gorm:"not null;size;default"用于约束字段行为。

但是,给每个字段都指定列名会使开发变得非常麻烦,其实GORM会按照约定根据结构体的字段名自动指定列名。
这样在遵循约定的前提下,我们可以少写许多tag,只需要写产生约束的tag,可以提高开发效率。

GORM的默认约定:

GORM倾向于约定使用 ID 作为主键,使用结构体名的 蛇形复数作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间。

其中

  • 蛇形:BranchNo -> branch_no
  • 蛇形复数:Staff -> staffs

除此之外,我们可以使用GORM内置的结构体gorm.Model来嵌入我们自定义的结构体中,gorm.Model帮我们提前定义了ID、创建时间、更改时间等元信息字段。

type Model struct {   
    ID        uint           `gorm:"primaryKey"`   
    CreatedAt time.Time   
    UpdatedAt time.Time   
    DeletedAt gorm.DeletedAt `gorm:"index"` 
}

于是,最终我们这样定义Staff表模型。

type Staff struct {
    gorm.Model
    BranchNo string `gorm:"not null"`
    Name     string `gorm:"not null"`
    Sex      string
    Position string
    Birthday time.Time
    Salary   uint
}

连接数据库

GORM使用不同的驱动来连接不同类型的数据库。本文使用SQLite,因此首先需要导入对应的驱动依赖。

import "gorm.io/driver/sqlite"

然后使用gorm.Open连接至目标数据库。

db, err := gorm.Open(sqlite.Open("dreamHome.db"), &gorm.Config{})

gorm.Open接受两个参数DialectorOption,其中Option可以使用GORM内置结构体gorm.Config引用传递,空结构体即表示配置均为默认值。
Dialector则需要使用相应驱动包中的Open方法并通过传入数据库的DSN值来生成。例如,本文连接至本地SQLite数据库dreamHome.db
gorm.Open返回一个gorm.DB结构体和err错误。这个结构体便是之后我们所有数据库操作的对象,此时我们已经成功连接至数据库并可以对其进行操作了。
于是,我们就可以使用db.AutoMigrate(&Staff{})自动创建staffs表和对应的schema模式。

db, err := gorm.Open(sqlite.Open("dreamHome.db"), &gorm.Config{})
if err != nil {
    panic("failed to connect the database")
}
err = db.AutoMigrate(&Staff{})
if err != nil {
    log.Fatalln("auto migration failed")
}

运行go文件之后,使用SQLite3查看dreamHome.db的表结构是否创建成功。

image.png

我们成功地连接到本地数据库,并在其中按照定义的模型自动生成了staffs表以及表内的字段与约束。

数据库操作(增删改查)

上述代码中,通过gorm.Open返回的gorm.DB其实是对sql.DB的封装,实现了基本上所有的数据库操作接口。我们可以通过调用该结构体的方法来对数据库操作,而无需编写相应的sql语句。

创建记录

我们使用gorm.DBCreate()方法向数据库插入新记录,和insert into values()语句功能相似。
Create会根据传入数据的模型自动向数据库插入新记录并填充其字段。
Create可以插入单条或多条记录。

//单条
newResult := db.Create(&Staff{BranchNo: "B01", Name: "Chen", Sex: "M", Position: "Manager", Birthday: time.Now(), Salary: 4000})
//多条
newResults := db.Create([]*Staff{
    {BranchNo: "B02", Name: "Te", Sex: "M", Position: "Supervisor", Birthday: time.Now(), Salary: 8000},
    {BranchNo: "B02", Name: "Ru", Sex: "F", Position: "Secretary", Birthday: time.Now(), Salary: 6000}})

当传入单个结构体时,会向数据库插入单条记录。
当传入结构体切片时,会向数据库插入对应的多条记录。

DB.Create()将返回DB结构体的指针,其中包含ErrorRowsAffected字段,我们可以处理异常和获取插入记录的条数。

if newResult.Error != nil {
    log.Println("create new record failed")
} else {
    log.Printf("insert %v record\n", newResult.RowsAffected)
}

再次运行go文件并查看数据库。 image.png image.png

查询数据

检索单个对象

  1. First():按照主键升序检索第一条记录。
  2. Last():按照主键升序检索最后一条记录。
  3. Take():检索一条记录,随机检索,因此需要根据字段检索。

检索所有对象

使用Find(),等价于select * from table

根据条件查询

在调用检索方法前调用组合式方法构成符合需求的查询语句,可以使用Where()
传入与sql语句相似的字符串和值作为查询的条件。
例如:

results := db.Where("branch_no = ?", "B02").Find(&[]Staff{})

运行go文件,查看结果。

image.png 有两条记录被匹配。

更改数据

直接更新某条记录

使用Save():需要指定被更改数据的主键,并且被指明的字段将自动被零值填充。

更新部分字段

result := db.Model(&Staff{}).Where("id = ?", 1).Update("salary", 0)
results := db.Model(&Staff{}).Where("id = ?", 2).Updates(Staff{Salary: 1,Position: "Fired"})

运行go文件,发现记录已被更改。 image.png

删除数据

使用Delete()Where()删除符合条件的记录

results := db.Where("branch_no = ?", "B02").Delete(&Staff{})

运行go文件并查看数据库。 image.png 此时会发现,记录还是3条。但是符合条件的2条记录的deleted_at字段不再是空值,这是因为当我们定义的模型中包含DeletedAt字段时,会自动获得软删除的能力。
当我们删除记录时,不会物理上从数据库删除记录,而是会将记录的DeletedAt设置为当前时间,使一般的查询方法检索不到。
如果要使用物理删除,可以不再定义的模型中包含deleted_at字段。