除了可以使用命令行客户端与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{})
}
- dsn是特别长的一个字符串,很难了解具体的含义
- 有的参数不能通过这个字符串传递,另外做字符串转义很难,例如的密码不能通过dsn来表示,还需要更改sql的密码才能正常使用
- 用户可能会忘记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
细节:
- 使用“查”的一种场景是判断表中是否有符合该条件的记录。
First(),Take()与Last()方法在没找到匹配的记录时会返回ErrRecordNotFound错误,而使用Find()方法则不会返回该记录,即使使用了db.Limit(1).Find(&user)来修饰。另外需要注意的是在&user原来的值为nil,在使用Find(&user)方法后,即使找不到匹配的记录,&user的值也将不是nil,因此不能通过检查&user的值是否为nil来判断是否找到了匹配的记录,我认为使用单一struct来接住Find()的查询结果,这个实现的行为是不确定的; - 若查找的数据很明确只有一条(例如按用户名查找,且用户名是不重复的),则使用
Find()是低效的,因为在查找到匹配的第一条数据后仍然会继续查找剩余行,这是性能不高,而First(),Take()与Last()则都是找到一条后就返回
归纳上述两条细节,我个人总结出以下gorm"查"api的用法:
- 对于明确只有一条匹配记录或者只需要其中一条的场景,使用
First(),Take()与Last(),并使用ErrRecordNotFound来判断是否找到了记录; - 对于可能匹配到多条记录,且都需要拿到的场景,使用
Find(),并使用slice接住结果,通过查看slice的长度来判断是否找到了记录。
03 数据类型的对应
时间
datetime 对应 time.Time。
注意在连接时需要在dsn中加上&parseTime=True