使用 GORM连接数据库,并实现增删改查操作 | 青训营

195 阅读5分钟

除了可以使用命令行客户端与GUI客户端操作数据库之外,我们还需要在自己开发的程序中完成数据库的增删改查的操作。

数据库保存的表,字段与程序中的实体类之间是没有关联的,因此操作数据库完成数据持久化需要特殊的解决方案。这个方案主要可以分为两种,一是硬编码的方式,例如在golang中使用原生库"github.com/go-sql-driver/mysql" 来访问MySQL数据库;二是使用ORM库,例如gorm、xorm、xsql。

硬编码方案存在以下缺点
1.持久化层缺乏弹性。一旦出现业务需求的变更,就必须修改持久化层的接口;
2.持久化层同时与域模型与关系数据库模型绑定,不管域模型还是关系数据库模型发生变化,修改持久化的相关程序代码,增加了软件的维护难度

因此就有了ORM技术。ORM全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。

本文将简要介绍GORM的增删改查操作,且由于MySQL的使用较为广泛,下文都将以MySQL作为例子。 更为详细的用法,还是需要大家查看文档gorm.io/zh_CN/docs/…

01 连接数据库

连接数据库有两种方式:

DSN

import (   
    "gorm.io/driver/mysql"   
    "gorm.io/gorm" 
)  

func main() {      
    dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"   
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 
}
  1. dsn是特别长的一个字符串,很难了解具体的含义
  2. 有的参数不能通过这个字符串传递,另外做字符串转义很难,例如的密码不能通过dsn来表示,还需要更改sql的密码才能正常使用
  3. 用户可能会忘记import driver,因为没有强制的编译检查,只会出现运行时的panic

新连接方式

尚未找到该连接方式的资料,且未能成功使用该方式连接数据库。

func main() {
    connector, err := mysql.NewConnector(&mysql.Config{
        User:   "gorm",
        Passwd: "gorm",
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "gorm",
        ParseTime:true,
    })
    db := sql.OpenDB(connector)
}

02 基本操作

// 创建记录并为指定的字段分配值
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("username", 18, "2023-08-12 11:05:21.775") 

// 创建记录并忽略指定的字段分配值
db.Omit("Name", "Age", "CreatedAt").Create(&user) 
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")

Create()方法接收一个结构体实例指针,在成功插入后,gorm会将整个记录回写到该实例中。

我们团队现在正在使用go-zero框架,顺理成章的,也一并使用了go-zero集成的sqlx。 与gorm不同,sqlx并不能在“增”的api中传入一个相应的结构体,也因此无法直接得到插入表后的各个字段的值。

幸运的是,sqlx的“增”api会返回一个result,从result中可以拿到主键的值。但在某些场景下,我们除了要获取插入记录的主键的值,还可能要获取到其它字段的值,例如mysql自动生成的created_at的值,如果我们使用sqlx,则我们在插入后还需要用主键再回到mysql中获取该记录,这与使用gorm相比就显得不那么优雅。

关于性能,我目前还没有研究过gorm在将记录插入表后再回写到传入的结构体中是如何实现的,因此还不能下定论说sqlx在该场景下会有性能的损失。

email.id = 10
db.Delete(&email)
// DELETE from emails where id = 10;
// 带额外条件的删除  
db.Where("name = ?", "jinzhu").Delete(&email)  
// DELETE from emails where id = 10 AND name = "jinzhu";

db.Delete(&User{}, 10) 
// DELETE FROM users WHERE id = 10;  
db.Delete(&User{}, "10") 
// DELETE FROM users WHERE id = 10; 
db.Delete(&users, []int{1,2,3}) 
// DELETE FROM users WHERE id IN (1,2,3); 

db.First(&user)  
user.Name = "ggg"
db.Save(&user)

或者

db.Save(&User{id: 10, Name: "ggg", Age: 100})

需要注意,如果传入Save()的结构体中不含有id字段,则会插入新的记录

db.Save(&User{Name: "ggg"})

// 获取第一条记录 
res := db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
res := db.Where("name = ?",  "username").First(&user)

// Take, Last, Find同样可以使用Where()来条件检索

// 获取一条记录,没有指定排序字段
res := db.Take(&user)
// SELECT * FROM users LIMIT 1;

// 获取最后一条记录(主键降序)
res := db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

res := db.Find(&user)
// SELECT * FROM user ORDER BY id;

res.RowsAffected   // 返回找到的记录数
res.Error          // returns error or nil

细节:

  1. 使用“查”的一种场景是判断表中是否有符合该条件的记录。First()Take()Last()方法在没找到匹配的记录时会返回ErrRecordNotFound错误,而使用Find()方法则不会返回该记录,即使使用了db.Limit(1).Find(&user)来修饰。另外需要注意的是在&user原来的值为nil,在使用Find(&user)方法后,即使找不到匹配的记录,&user的值也将不是nil,因此不能通过检查&user的值是否为nil来判断是否找到了匹配的记录,我认为使用单一struct来接住Find()的查询结果,这个实现的行为是不确定的;
  2. 若查找的数据很明确只有一条(例如按用户名查找,且用户名是不重复的),则使用Find()是低效的,因为在查找到匹配的第一条数据后仍然会继续查找剩余行,这是性能不高,而First()Take()Last()则都是找到一条后就返回

归纳上述两条细节,我个人总结出以下gorm"查"api的用法:

  1. 对于明确只有一条匹配记录或者只需要其中一条的场景,使用First()Take()Last(),并使用ErrRecordNotFound来判断是否找到了记录;
  2. 对于可能匹配到多条记录,且都需要拿到的场景,使用Find(),并使用slice接住结果,通过查看slice的长度来判断是否找到了记录。

03 数据类型的对应

时间

datetime 对应 time.Time。
注意在连接时需要在dsn中加上&parseTime=True