Go 语言流行 ORM 框架 GORM 使用介绍
GORM是Go语言中最受欢迎的ORM库之一,它提供了强大的功能和简洁的API,让数据库操作变得更加简单和易维护。本文将详细介绍GORM的常见用法,包括数据库连接、模型定义、CRUD、事务管理等方面,帮助大家快速上手使用GORM进行Web后端开发。
安装
通过如下命令安装GORM:
1$goget-ugorm.io/gorm
你也许见过使用goget-ugithub.com/jinzhu/gorm命令来安装GORM,这个是老版本v1,现已过时,不建议使用。新版本v2已经迁移至github.com/go-gorm/gorm仓库下。
快速开始
如下示例代码带你快速上手GORM的使用:
1234567891011121314151617181920212223242526272829303132333435363738394041packagemainimport("gorm.io/driver/sqlite""gorm.io/gorm")//Product定义结构体用来映射数据库表typeProductstruct{gorm.ModelCodestringPriceuint}funcmain{//建立数据库连接db,err:=gorm.Open(sqlite.Open("test.db"),&gorm.Config{})iferr!=nil{panic("failedtoconnectdatabase")}//迁移表结构db.AutoMigrate(&Product{})//增加数据db.Create(&Product{Code:"D42",Price:100})//查找数据varproductProductdb.First(&product,1)//findproductwithintegerprimarykeydb.First(&product,"code=?","D42")//findproductwithcodeD42//更新数据-updateproduct'spriceto200db.Model(&product).Update("Price",200)//更新数据-updatemultiplefieldsdb.Model(&product).Updates(Product{Price:200,Code:"F42"})//non-zerofieldsdb.Model(&product).Updates(map[string]interface{}{"Price":200,"Code":"F42"})//删除数据-deleteproductdb.Delete(&product,1)}
提示:这里使用了SQLite数据库驱动,需要通过goget-ugorm.io/driver/sqlite命令安装。
将以上代码保存在main.go中并执行。
1$gorunmain.go
执行完成后,我们将在当前目录下得到test.dbSQLite数据库文件。
SQLite
(1)进入SQLite命令行。
(2)查看已存在的数据库表。
(3)设置稍后查询表数据时的输出模式为按列左对齐。
(4)查询表中存在的数据。
有过使用ORM框架经验的同学,以上代码即使我不进行讲解也能看懂个大概。
这段示例代码基本能够概括GORM框架使用套路:
定义结构体映射表结构:Product结构体在GORM中称作「模型」,一个模型对应一张数据库表,一个结构体实例对象对应一条数据库表记录。
连接数据库:GORM使用gorm.Open方法与数据库建立连接,连接建立好后,才能对数据库进行CRUD操作。
自动迁移表结构:调用db.AutoMigrate方法能够自动完成在数据库中创建Product结构体所映射的数据库表,并且,当Product结构体字段有变更,再次执行迁移代码,GORM会自动对表结构进行调整,非常方便。不过,我不推荐在生产环境项目中使用此功能。因为数据库表操作都是高风险操作,一定要经过多人Review并审核通过,才能执行操作。GORM自动迁移功能虽然理论上不会出现问题,但线上操作谨慎为妙,个人认为只有在小项目或数据不那么重要的项目中使用比较合适。
CRUD操作:迁移好数据库后,就有了数据库表,可以进行CRUD操作了。
有些同学可能有个疑问,以上示例代码中并没有类似deferdb.Close主动关闭连接的操作,那么何时关闭数据库连接?
其实GORM维护了一个数据库连接池,初始化db后所有的连接都由底层库来管理,无需程序员手动干预,GORM会在合适的时机自动关闭连接。GORM框架作者jinzhu也有在源码仓库Issue中回复过网友的提问,感兴趣的同学可以点击进入查看。
接下来我将对GORM的使用进行详细讲解。
声明模型
GORM使用模型(Model)来映射一张数据库表,模型是标准的Gostruct,由Go的基本数据类型、实现了Scanner和Valuer接口的自定义类型及其指针或别名组成。
例如:
1234567891011typeUserstruct{IDuintNamestringEmail*stringAgeuint8Birthday*time.TimeMemberNumbersql.NullStringActivatedAtsql.NullTimeCreatedAttime.TimeUpdatedAttime.Time}
我们可以使用gorm字段标签来控制数据库表字段的类型、列大小、默认值等属性,比如使用column字段标签来映射数据库中字段名称。
12345678910111213typeUserstruct{gorm.ModelNamestring`gorm:"column:name"`Email*string`gorm:"column:email"`Ageuint8`gorm:"column:age"`Birthday*time.Time`gorm:"column:birthday"`MemberNumbersql.NullString`gorm:"column:member_number"`ActivatedAtsql.NullTime`gorm:"column:activated_at"`}func(u*User)TableNamestring{return"user"}
在不指定column字段标签情况下,GORM默认使用字段名的snake_case作为列名。
GORM默认使用结构体名的snake_cases作为表名,为结构体实现TableName方法可以自定义表名。
我更喜欢「显式胜于隐式」的做法,所以数据库名和表名都会显示写出来。
因为我们不使用自动迁移的功能,所以其他字段标签都用不到,就不在此一一介绍了,感兴趣的同学可以查看官方文档进行学习。
User结构体中有一个嵌套的结构体gorm.Model,它是GORM默认提供的一个模型struct,用来简化用户模型定义。
GORM倾向于约定优于配置,默认情况下,使用ID作为主键,使用CreatedAt、UpdatedAt、DeletedAt字段追踪记录的创建、更新、删除时间。而这几个字段就定义在gorm.Model中:
123456typeModelstruct{IDuint`gorm:"primarykey"`CreatedAttime.TimeUpdatedAttime.TimeDeletedAtDeletedAt`gorm:"index"`}
由于我们不使用自动迁移功能,所以需要手动编写SQL语句来创建user数据库表结构:
123456789101112131415CREATETABLE`user`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(50)DEFAULT''COMMENT'用户名',`email`varchar(255)NOTNULLDEFAULT''COMMENT'邮箱',`age`tinyint(4)NOTNULLDEFAULT'0'COMMENT'年龄',`birthday`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'生日',`member_number`varchar(50)COMMENT'成员编号',`activated_at`datetimeCOMMENT'激活时间',`created_at`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMP,`updated_at`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,`deleted_at`datetime,PRIMARYKEY(`id`),UNIQUEKEY`u_email`(`email`),INDEX`idx_deleted_at`(`deleted_at`))ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='用户表';
数据库中字段类型要跟Go中模型的字段类型相对应,不兼容的类型可能导致错误。
连接数据库
GORM官方支持的数据库类型有:MySQL、PostgreSQL、SQLite、SQLServer和TiDB。
这里使用最常见的MySQL作为示例,来讲解GORM如何连接到数据库。
在前文快速开始的示例代码中,我们使用SQLite数据库时,安装了sqlite驱动程序。要连接MySQL则需要使用mysql驱动。
在GORM中定义了gorm.Dialector接口来规范数据库连接操作,实现了此接口的程序我们将其称为「驱动」。针对每种数据库,都有对应的驱动,驱动是独立于GORM库的,需要单独引入。
连接MySQL数据库的代码如下:
1234567891011121314packagemainimport("fmt""gorm.io/driver/mysql""gorm.io/gorm")funcConnectMySQL(host,port,user,pass,dbnamestring)(*gorm.DB,error){dsn:=fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",user,pass,host,port,dbname)returngorm.Open(mysql.Open(dsn),&gorm.Config{})}
可以发现,这段代码与连接SQLite数据库的代码如出一辙,这就是面向接口编程的好处。
首先,mysql.Open接收一个字符串dsn,DSN全称DataSourceName,翻译过来叫数据库源名称。DSN定义了一个数据库的连接信息,包含用户名、密码、数据库IP、数据库端口、数据库字符集、数据库时区等信息。DSN遵循特定格式:
1username:password@protocol(address)/dbname?param=value
通过DSN所包含的信息,mysql驱动就能够知道以什么方式连接到MySQL数据库了。
mysql.Open返回的正是一个gorm.Dialector对象,将其传递给gorm.Open方法后,我们将得到*gorm.DB对象,这个对象可以用来操作数据库。
GORM使用database/sql来维护数据库连接池,对于连接池我们可以设置如下几个参数:
1234567891011funcSetConnect(db*gorm.DB)error{sqlDB,err:=db.DBiferr!=nil{returnerr}sqlDB.SetMaxOpenConns(100)//设置数据库的最大打开连接数sqlDB.SetMaxIdleConns(100)//设置最大空闲连接数sqlDB.SetConnMaxLifetime(10*time.Second)//设置空闲连接最大存活时间returnnil}
现在,数据库连接已经建立,我们可以对数据库进行操作了。
创建
可以使用Create方法创建一条数据库记录:
要创建记录,我们需要先实例化User对象,然后将其指针传递给db.Create方法。
db.Create方法执行完成后,依然返回一个*gorm.DB对象。
user.ID会被自动填充为创建数据库记录后返回的真实值。
result.RowsAffected可以拿到此次操作影响行数。
result.Error可以知道执行SQL是否出错。
在这里,我将db.Create(&user)这句ORM代码所生成的原生SQL语句放在了注释中,方便你对比学习。并且,之后的示例中我也会这样做。
Create方法不仅支持创建单条记录,它同样支持批量操作,一次创建多条记录:
代码主要逻辑不变,只需要将单个的User实例换成User切片即可。GORM会使用一条SQL语句完成批量创建记录。
查询
查询记录是我们在日常开发中使用最多的场景了,GORM提供了多种方法来支持SQL查询操作。
使用First方法可以查询第一条记录:
123varuserUser//SELECT*FROM`user`WHERE`user`.`deleted_at`ISNULLORDERBY`user`.`id`LIMIT1result:=db.First(&user)
First方法接收一个模型指针,通过模型的TableName方法则可以拿到数据库表名,然后使用SELECT*语句从数据库中查询记录。
根据生成的SQL可以发现First方法查询数据默认根据主键ID升序排序,并且只会过滤删除时间为NULL的数据,使用LIMIT关键字来限制数据条数。
使用Last方法可以查询最后一条数据,排序规则为主键ID降序:
123varlastUserUser//SELECT*FROM`user`WHERE`user`.`deleted_at`ISNULLORDERBY`user`.`id`DESCLIMIT1result=db.Last(&lastUser)
使用Where方法可以增加查询条件:
123varusers[]User//SELECT*FROM`user`WHEREname!='unknown'AND`user`.`deleted_at`ISNULLresult=db.Where("name!=?","unknown").Find(&users)
这里不再查询单条数据,所以改用Find方法来查询所有符合条件的记录。
以上介绍的几种查询方法,都是通过SELECT*查询数据库表中的全部字段,我们可以使用Select方法指定需要查询的字段:
123varuser2User//SELECT`name`,`age`FROM`user`WHERE`user`.`deleted_at`ISNULLORDERBY`user`.`id`LIMIT1result=db.Select("name","age").First(&user2)
使用Order方法可以自定义排序规则:
123varusers2[]User//SELECT*FROM`user`WHERE`user`.`deleted_at`ISNULLORDERBYiddescresult=db.Order("iddesc").Find(&users2)
GORM也提供了对Limit&Offset的支持:
123varusers3[]User//SELECT*FROM`user`WHERE`user`.`deleted_at`ISNULLLIMIT2OFFSET1result=db.Limit(2).Offset(1).Find(&users3)
使用-1可以取消Limit&Offset的限制条件:
12345varusers4[]Uservarusers5[]User//SELECT*FROM`user`WHERE`user`.`deleted_at`ISNULLLIMIT2OFFSET1;(users4)//SELECT*FROM`user`WHERE`user`.`deleted_at`ISNULL;(users5)result=db.Limit(2).Offset(1).Find(&users4).Limit(-1).Offset(-1).Find(&users5)
这段代码会执行两条查询语句,之所以能够采用这种「链式调用」的方式执行多条SQL,是因为每个方法返回的都是*gorm.DB对象,这也是一种编程技巧。
使用Count方法可以统计记录条数:
123varcountint64//SELECTcount(*)FROM`user`WHERE`user`.`deleted_at`ISNULLresult=db.Model(&User{}).Count(&count)
有时候遇到比较复杂的业务,我们可能需要使用SQL子查询,子查询可以嵌套在另一个查询中,GORM允许将*gorm.DB对象作为参数时生成子查询:
1234varavgages[]float64//SELECTAVG(age)asavgageFROM`user`WHERE`user`.`deleted_at`ISNULLGROUPBY`name`HAVINGAVG(age)>(SELECTAVG(age)FROM`user`WHEREnameLIKE'user%')subQuery:=db.Select("AVG(age)").Where("nameLIKE?","user%").Table("user")result=db.Model(&User{}).Select("AVG(age)asavgage").Group("name").Having("AVG(age)>(?)",subQuery).Find(&avgages)
Having方法签名如下:
1func(db*DB)Having(queryinterface{},args...interface{})(tx*DB)
第二个参数是一个范型interface{},所以不仅可以接收字符串,GORM在判断其类型为*gorm.DB时,就会构造一个子查询。
更新
为了讲解更新操作,我们需要先查询一条记录,之后的更新操作都是基于这条被查询出来的User对象:
123varuserUser//SELECT*FROM`user`WHERE`user`.`deleted_at`ISNULLORDERBY`user`.`id`LIMIT1result:=db.First(&user)
更新操作只要修改User对象的属性,然后调用db.Save(&user)方法即可完成:
在更新操作时,User对象要保证ID属性存在值,不然就变成了创建操作。
Save方法会保存所有的字段,即使字段是对应类型的零值。
除了使用Save方法更新所有字段,我们还可以使用Update方法更新指定字段:
12//UPDATE`user`SET`name`='Jianghushinian',`updated_at`='2023-05-2222:24:34.215'WHERE`user`.`deleted_at`ISNULLAND`id`=1result=db.Model(&user).Update("name","Jianghushinian")
Update只能支持更新单个字段,要想更新多个字段,可以使用Updates方法:
12//UPDATE`user`SET`updated_at`='2023-05-2222:29:35.19',`name`='JiangHu'WHERE`user`.`deleted_at`ISNULLAND`id`=1result=db.Model(&user).Updates(User{Name:"JiangHu",Age:0})
注意,Updates方法与Save方法有一个很大的不同之处,它只会更新非零值字段。Age字段为零值,所以不会被更新。
如果一定要更新零值字段,除了可以使用上面的Save方法,还可以将User结构体换成map[string]interface{}类型的map对象:
12//UPDATE`user`SET`age`=0,`name`='JiangHu',`updated_at`='2023-05-2222:29:35.623'WHERE`user`.`deleted_at`ISNULLAND`id`=1result=db.Model(&user).Updates(map[string]interface{}{"name":"JiangHu","age":0})
此外,更新数据时,还可以使用gorm.Expr来实现SQL表达式:
12//UPDATE`user`SET`age`=age+1,`updated_at`='2023-05-2222:24:34.219'WHERE`user`.`deleted_at`ISNULLAND`id`=1result=db.Model(&user).Update("age",gorm.Expr("age+?",1))
gorm.Expr("age+?",1)方法调用会被转换成age=age+1SQL表达式。
删除
可以使用Delete方法删除数记录:
123varuserUser//UPDATE`user`SET`deleted_at`='2023-05-2222:46:45.086'WHEREname='JiangHu'AND`user`.`deleted_at`ISNULLresult:=db.Where("name=?","JiangHu").Delete(&user)
对于删除操作,GORM默认使用逻辑删除策略,不会对记录进行物理删除。
所以Delete方法在对数据进行删除时,实际上执行的是SQLUPDATE操作,而非DELETE操作。
将deleted_at字段更新为当前时间,表示当前数据已删除。这也是为什么前文在讲解查询和更新的时候,生成的SQL语句都自动附加了deleted_atISNULLWhere条件的原因。
这样就实现了逻辑层面的删除,数据在数据库中仍然存在,但查询和更新的时候会将其过滤掉。
记录被删除后,我们无法通过如下代码直接查询到被逻辑删除的记录:
12345//SELECT*FROM`user`WHEREname='JiangHu'AND`user`.`deleted_at`ISNULLORDERBY`user`.`id`LIMIT1result=db.Where("name=?","JiangHu").First(&user)iferr:=result.Error;err!=nil{fmt.Println(err)//recordnotfound}
这将得到一个错误recordnotfound。
不过,GORM提供了Unscoped方法,可以绕过逻辑删除:
12//SELECT*FROM`user`WHEREname='JiangHu'ORDERBY`user`.`id`LIMIT1result=db.Unscoped.Where("name=?","JiangHu").First(&user)
以上代码能够查询出被逻辑删除的记录,生成的SQL语句中没有包含deleted_atISNULLWhere条件。
对于比较重要的数据,建议使用逻辑删除,这样可以在需要的时候恢复数据,也便于故障追踪。
不过,如果明确想要物理删除一条记录,同理可以使用Unscoped方法:
12//DELETEFROM`user`WHEREname='JiangHu'AND`user`.`id`=1result=db.Unscoped.Where("name=?","JiangHu").Delete(&user)
关联
日常开发中,多数情况下不只是对单表进行操作,还要对存在关联关系的多表进行操作。
这里以一个博客系统最常见的三张表「文章表、评论表、标签表」为例,对GORM如何操作关联表进行讲解。
这里涉及最常见的关联关系:一对多和多对多。一篇文章可以有多条评论,所以文章和评论是一对多关系;一篇文章可以存在多个标签,每个标签也可以包含多篇文章,所以文章和标签是多对多关系。
模型定义如下:
123456789101112131415161718192021222324252627282930313233typePoststruct{gorm.ModelTitlestring`gorm:"column:title"`Contentstring`gorm:"column:content"`Comments[]*Comment`gorm:"foreignKey:PostID;constraint:OnUpdate:CASCADE,OnDelete:SETNULL;references:ID"`Tags[]*Tag`gorm:"many2many:post_tags"`}func(p*Post)TableNamestring{return"post"}typeCommentstruct{gorm.ModelContentstring`gorm:"column:content"`PostIDuint`gorm:"column:post_id"`Post*Post}func(c*Comment)TableNamestring{return"comment"}typeTagstruct{gorm.ModelNamestring`gorm:"column:name"`Post[]*Post`gorm:"many2many:post_tags"`}func(t*Tag)TableNamestring{return"tag"}
我准备了对应的建表SQL,可以点击链接进行查看:GitHub地址。
在模型定义中,Post文章模型使用Comments和Tags分别保存关联的评论和标签,这两个字段不会保存在数据库表中。
Comments字段标签使用foreignKey来指明Comments表中的外键,并使用constraint指明了约束条件,references指明Comments表外键引用Post表的ID字段。
其实现在生产环境中都不再推荐使用外键,各个表之间不再有数据库层面的外键约束,在做CRUD操作时全部通过代码层面来进行业务约束。这里为了演示GORM的外键和级联操作功能,所以定义了这些结构体标签。
Tags字段标签使用many2many来指明多对多关联表名。
对于Comment模型,PostID字段就是外键,用来保存Post.ID。Post字段同样不会保存在数据库中,这种做法在ORM框架中非常常见。
接下来,我将同样对关联表的CRUD操作进行一一讲解。
创建
创建Post时会自动创建与之关联的Comments和Tags:
1234567891011121314varpostPostpost=Post{Title:"post1",Content:"content1",Comments:[]*Comment{{Content:"comment1",Post:&post},{Content:"comment2",Post:&post},},Tags:[]*Tag{{Name:"tag1"},{Name:"tag2"},},}result:=db.Create(&post)
这里定义了一个文章对象post,并且包含两条评论和两个标签。
注意Comment的Post字段引用了&post,并没有指定PostID外键字段,GORM能够正确处理它。
以上代码将生成并依次执行如下SQL语句:
123456BEGINTRANSACTION;INSERTINTO`tag`(`created_at`,`updated_at`,`deleted_at`,`name`)VALUES('2023-05-2222:56:52.923','2023-05-2222:56:52.923',NULL,'tag1'),('2023-05-2222:56:52.923','2023-05-2222:56:52.923',NULL,'tag2')ONDUPLICATEKEYUPDATE`id`=`id`INSERTINTO`post`(`created_at`,`updated_at`,`deleted_at`,`title`,`content`)VALUES('2023-05-2222:56:52.898','2023-05-2222:56:52.898',NULL,'post1','content1')ONDUPLICATEKEYUPDATE`id`=`id`INSERTINTO`comment`(`created_at`,`updated_at`,`deleted_at`,`content`,`post_id`)VALUES('2023-05-2222:56:52.942','2023-05-2222:56:52.942',NULL,'comment1',1),('2023-05-2222:56:52.942','2023-05-2222:56:52.942',NULL,'comment2',1)ONDUPLICATEKEYUPDATE`post_id`=VALUES(`post_id`)INSERTINTO`post_tags`(`post_id`,`tag_id`)VALUES(1,1),(1,2)ONDUPLICATEKEYUPDATE`post_id`=`post_id`COMMIT;
可以发现,与文章形成一对多关系的评论以及与文章形成多对多关系的标签,都会被创建,并且GORM会维护其关联关系,而且这些操作全部在一个事务下完成。
此外,前文介绍的Save方法不仅能够更新记录,实际上它还支持创建记录,当Post对象不存在主键ID时,Save方法将会创建一条新的记录:
123456789101112varpost3Postpost3=Post{Title:"post3",Content:"content3",Comments:[]*Comment{{Content:"comment33",Post:&post3},},Tags:[]*Tag{{Name:"tag3"},},}result=db.Save(&post3)
以上代码生成的SQL如下:
123456BEGINTRANSACTION;INSERTINTO`tag`(`created_at`,`updated_at`,`deleted_at`,`name`)VALUES('2023-05-2223:17:53.189','2023-05-2223:17:53.189',NULL,'tag3')ONDUPLICATEKEYUPDATE`id`=`id`INSERTINTO`post`(`created_at`,`updated_at`,`deleted_at`,`title`,`content`)VALUES('2023-05-2223:17:53.189','2023-05-2223:17:53.189',NULL,'post3','content3')ONDUPLICATEKEYUPDATE`id`=`id`INSERTINTO`comment`(`created_at`,`updated_at`,`deleted_at`,`content`,`post_id`)VALUES('2023-05-2223:17:53.19','2023-05-2223:17:53.19',NULL,'comment33',0)ONDUPLICATEKEYUPDATE`post_id`=VALUES(`post_id`)INSERTINTO`post_tags`(`post_id`,`tag_id`)VALUES(0,0)ONDUPLICATEKEYUPDATE`post_id`=`post_id`COMMIT;
查询
可以使用如下方式,根据Post的ID查询与之关联的Comments:
12345678var(postPostcomments[]*Comment)post.ID=1//SELECT*FROM`comment`WHERE`comment`.`post_id`=1AND`comment`.`deleted_at`ISNULLerr:=db.Model(&post).Association("Comments").Find(&comments)
注意⚠:传递给Association方法的参数是Comments,即在Post模型中定义的字段,而非评论的模型名Comment。这点一定不要搞错了,不然执行SQL时会报错。
Post是源模型,主键ID不能为空。Association方法指定关联字段名,在Post模型中关联的评论使用Comments表示。最后使用Find方法来查询关联的评论。
在查询Post时,我们可以预加载与之关联的Comments:
123456789post2:=Post{}result:=db.Preload("Comments").Preload("Tags").First(&post2)fmt.Println(post2)fori,comment:=rangepost2.Comments{fmt.Println(i,comment)}fori,tag:=rangepost2.Tags{fmt.Println(i,tag)}
我们可以像往常一样使用First方法查询一条Post记录,同时搭配使用Preload方法来指定预加载的关联字段名,这样在查询Post记录时,会将关联字段表的记录全部查询出来,并赋值给关联字段。
以上代码将执行如下SQL:
123456BEGINTRANSACTION;SELECT*FROM`post`WHERE`post`.`deleted_at`ISNULLORDERBY`post`.`id`LIMIT1SELECT*FROM`comment`WHERE`comment`.`post_id`=1AND`comment`.`deleted_at`ISNULLSELECT*FROM`post_tags`WHERE`post_tags`.`post_id`=1SELECT*FROM`tag`WHERE`tag`.`id`IN(1,2)AND`tag`.`deleted_at`ISNULLCOMMIT;
GORM通过多条SQL语句查询出所有关联记录,并且将关联Comments和Tags分别赋值给Post模型对应字段。
当遇到多表查询时,我们通常还会使用JOIN来连接多张表:
12345678910typePostCommentstruct{TitlestringCommentstring}postComment:=PostComment{}post3:=Post{}post3.ID=3//SELECTpost.title,comment.ContentAScommentFROM`post`LEFTJOINcommentONcomment.post_id=post.idWHERE`post`.`deleted_at`ISNULLAND`post`.`id`=3result:=db.Model(&post3).Select("post.title,comment.ContentAScomment").Joins("LEFTJOINcommentONcomment.post_id=post.id").Scan(&postComment)
使用Select方法来指定需要查询的字段,使用Joins方法来实现JOIN功能,最终使用Scan方法可以将查询结果扫描到postComment对象中。
针对一对多关联关系,Joins方法同样支持预加载:
12345678varcomments2[]*Comment//SELECT`comment`.`id`,`comment`.`created_at`,`comment`.`updated_at`,`comment`.`deleted_at`,`comment`.`content`,`comment`.`post_id`,`Post`.`id`AS`Post__id`,`Post`.`created_at`AS`Post__created_at`,`Post`.`updated_at`AS`Post__updated_at`,`Post`.`deleted_at`AS`Post__deleted_at`,`Post`.`title`AS`Post__title`,`Post`.`content`AS`Post__content`FROM`comment`LEFTJOIN`post``Post`ON`comment`.`post_id`=`Post`.`id`AND`Post`.`deleted_at`ISNULLWHERE`comment`.`deleted_at`ISNULLresult=db.Joins("Post").Find(&comments2)fori,comment:=rangecomments2{fmt.Println(i,comment)fmt.Println(i,comment.Post)}
JOIN功能的预加载无需显式使用Preload来指明,只需要在Joins方法中指明一对多关系中一这一端模型Post即可,使用Find查询Comment记录。
根据生成的SQL可以发现查询主表为comment,副表为post。并且副表的字段都被重命名为模型名__字段名的格式,如Post__title(题外话:如果你使用过Python的DjangoORM框架,那么对这个双下划线命名字段的做法应该有种似曾相识的感觉)。
更新
同讲解单表更新时一样,我们需要先查询出一条记录,用来演示更新操作:
123varpostPost//SELECT*FROM`post`WHERE`post`.`deleted_at`ISNULLORDERBY`post`.`id`LIMIT1result:=db.First(&post)
可以使用如下方法替换Post关联的Comments:
1234comment:=Comment{Content:"comment3",}err:=db.Model(&post).Association("Comments").Replace([]*Comment{&comment})
仍然使用Association方法指定Post关联的Comments,Replace方法用来完成替换操作。
这里要注意,Replace方法返回结果不再是*gorm.DB对象,而是直接返回error。
生成SQL如下:
12345BEGINTRANSACTION;INSERTINTO`comment`(`created_at`,`updated_at`,`deleted_at`,`content`,`post_id`)VALUES('2023-05-2309:07:42.852','2023-05-2309:07:42.852',NULL,'comment3',1)ONDUPLICATEKEYUPDATE`post_id`=VALUES(`post_id`)UPDATE`post`SET`updated_at`='2023-05-2309:07:42.846'WHERE`post`.`deleted_at`ISNULLAND`id`=1UPDATE`comment`SET`post_id`=NULLWHERE`comment`.`id`8AND`comment`.`post_id`=1AND`comment`.`deleted_at`ISNULLCOMMIT;
删除
使用Delete删除文章表时,不会删除关联表的数据:
123varpostPost//UPDATE`post`SET`deleted_at`='2023-05-2309:09:58.534'WHEREid=1AND`post`.`deleted_at`ISNULLresult:=db.Where("id=?",1).Delete(&post)
对于存在关联关系的记录,删除时默认同样采用UPDATE操作,且不影响关联数据。
如果想要在删除评论时,顺便删除与文章的关联关系,可以使用Association方法:
12//UPDATE`comment`SET`post_id`=NULLWHERE`comment`.`post_id`=6AND`comment`.`id`IN(NULL)AND`comment`.`deleted_at`ISNULLerr:=db.Model(&post2).Association("Comments").Delete(post2.Comments)
事务
GORM提供了对事务的支持,这在复杂的业务逻辑中是必要的。
要在事务中执行一系列操作,可以使用Transaction方法实现:
123456789101112131415161718funcTransactionPost(db*gorm.DB)error{returndb.Transaction(func(tx*gorm.DB)error{post:=Post{Title:"HelloWorld",}iferr:=tx.Create(&post).Error;err!=nil{returnerr}comment:=Comment{Content:"HelloWorld",PostID:post.ID,}iferr:=tx.Create(&comment).Error;err!=nil{returnerr}returnnil})}
在Transaction方法内部的代码,都将在一个事务中被处理。Transaction方法接收一个函数,其参数为tx*gorm.DB,事务中所有数据库的操作,都应该使用这个tx而非db。
在执行事务的函数中,返回任何错误,整个事务都将被回滚,返回nil则事务被提交。
除了使用Transaction自动管理事务,我们还可以手动管理事务:
123456789101112131415161718192021funcTransactionPostWithManually(db*gorm.DB)error{tx:=db.Beginpost:=Post{Title:"HelloWorldManually",}iferr:=tx.Create(&post).Error;err!=nil{tx.Rollbackreturnerr}comment:=Comment{Content:"HelloWorldManually",PostID:post.ID,}iferr:=tx.Create(&comment).Error;err!=nil{tx.Rollbackreturnerr}returntx.Commit.Error}
db.Begin用于开启事务,并返回tx,稍后的事务操作都应使用这个tx对象。如果在处理事务的过程中遇到错误,可以使用tx.Rollback回滚事务,如果没有问题,最终可以使用tx.Commit提交事务。
注意:手动事务,事务一旦开始,你就应该使用tx处理数据库操作。
钩子
GORM还支持Hook功能,Hook是在创建、查询、更新、删除等操作之前、之后调用的函数,用来管理对象的生命周期。
钩子方法的函数签名为func(*gorm.DB)error,比如以下钩子函数在创建操作之前触发:
1234567func(u*User)BeforeCreate(tx*gorm.DB)(errerror){u.UUID=uuid.Newifu.Name=="admin"{returnerrors.New("invalidname")}returnnil}
比如我们为User模型定义BeforeCreate钩子,这样在创建User对象前,GORM会自动调用此函数,完成为User对象创建UUID以及用户名合法性验证功能。
GORM支持的钩子函数以及执行时机如下:
钩子函数执行时机BeforeSave调用Save前AfterSave调用Save后BeforeCreate插入记录前AfterCreate插入记录后BeforeUpdate更新记录前AfterUpdate更新记录后BeforeDelete删除记录前AfterDelete删除记录后AfterFind查询记录后
原生SQL
虽然我们使用ORM框架往往是为了将原生SQL的编写转为面向对象编程,不过对原生SQL的支持是一款ORM框架必备的功能。
可以使用Raw方法执行原生查询SQL,并将结果Scan到模型中:
12345varuserResUserResultdb.Raw(`SELECTid,name,ageFROMuserWHEREid=?`,3).Scan(&userRes)fmt.Printf("affectedrows:%d\n",db.RowsAffected)fmt.Println(db.Error)fmt.Println(userRes)
原生SQL同样支持使用表达式:
12varsumageintdb.Raw(`SELECTSUM(age)assumageFROMuserWHEREmember_number?`,gorm.Expr("ISNULL")).Scan(&sumage)
此外,我们还可以使用Exec执行任意原生SQL:
12345db.Exec("UPDATEuserSETage=?WHEREidIN?",18,[]int64{1,2})//使用表达式db.Exec(`UPDATEuserSETage=?WHEREname=?`,gorm.Expr("age*?+?",1,2),"Jianghu")//删除表db.Exec("DROPTABLEuser")
使用Exec无法拿到执行结果,可以用来对表进行操作,比如增加、删除表等。
编写SQL时支持使用@name语法命名参数:
1234567varpostPostdb.Where("titleLIKE@nameORcontentLiKE@name",sql.Named("name","%Hello%")).Find(&post)varuserUser//SELECT*FROMuserWHEREname1="Jianghu"ORname2="shinian"ORname3="Jianghu"db.Raw("SELECT*FROMuserWHEREname1=@nameORname2=@name2ORname3=@name",sql.Named("name","Jianghu"),sql.Named("name2","shinian")).Find(&user)
使用DryRun模式可以直接拿到由GORM生成的原生SQL,而不执行,方便后续使用:
1234varuserUserstmt:=db.Session(&gorm.Session{DryRun:true}).First(&user,1).Statementfmt.Println(stmt.SQL.String)//SQL:SELECT*FROM`user`WHERE`user`.`id`=?AND`user`.`deleted_at`ISNULLORDERBY`user`.`id`LIMIT1fmt.Println(stmt.Vars)//参数:[1]
DryRun模式可以翻译为空跑,意思是不执行真正的SQL,这在调试时非常有用。
调试
GORM常用功能我们已经基本讲解完成了,最后再来介绍下在日常开发中,遇到问题如何进行调试。
GORM调试方法我总结了如下5点:
全局开启日志
还记得在连接数据库时gorm.Open方法的第二个参数吗,我们当时传递了一个空配置&gorm.Config{},这个可选的参数可以改变GORM的一些默认功能配置,比如我们可以设置日志级别为Info,这样就能够在控制台打印所有执行的SQL语句:
123db,err:=gorm.Open(mysql.Open(dsn),&gorm.Config{Logger:logger.Default.LogMode(logger.Info),})
打印慢查询SQL
有时候某段ORM代码执行很慢,我们可以通过开启慢查询日志,来检测SQL中的慢查询语句:
12345678910111213141516funcConnectMySQL(host,port,user,pass,dbnamestring)(*gorm.DB,error){slowLogger:=logger.New(log.New(os.Stdout,"\r\n",log.LstdFlags),logger.Config{//设定慢查询时间阈值为3ms(默认值:200*time.Millisecond)SlowThreshold:3*time.Millisecond,//设置日志级别LogLevel:logger.Warn,},)dsn:=fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",user,pass,host,port,dbname)returngorm.Open(mysql.Open(dsn),&gorm.Config{Logger:slowLogger,})}
打印指定SQL
使用Debug能够打印当前ORM语句执行的SQL:
1db.Debug.First(&User{})
全局开启DryRun模型
在连接数据库时,我们可以全局开启「空跑」模式:
123db,err:=gorm.Open(mysql.Open(dsn),&gorm.Config{DryRun:true,})
开启DryRun模型后,任何SQL语句都不会真正执行,方便测试。
局部开启DryRun模型
在当前Session中局部开启「空跑」模型,可以在不执行操作的情况下生成SQL及其参数,用于准备或测试生成的SQL:
1234varuserUserstmt:=db.Session(&gorm.Session{DryRun:true}).First(&user,1).Statementfmt.Println(stmt.SQL.String)//=>SELECT*FROM`users`WHERE`id`=$1ORDERBY`id`fmt.Println(stmt.Vars)//=>[]interface{}{1}
总结
本文对Go语言中最流行的ORM框架GORM进行了讲解,介绍了如何编写模型,如何连接数据库,以及最常使用的CRUD操作。并且还对关联表中的一对多、多对多两种关联关系操作进行了讲解。我们还介绍了必不可少的功能「事务」,GORM还提供了钩子函数方便我们在CRUD操作前后插入一些自定义逻辑。最后对如何使用原生SQL以及如何调试也进行了介绍。
只要你原生SQL基础扎实,ORM框架学习起来并不会太费力,并且我们还有各种调试方式来打印GORM所生成的SQL,方便排查问题。
由于文章篇幅所限,这里只介绍了GORM常用功能,不过也基本能够覆盖日常开发中多数场景。更多高级功能如自定义Logger、读写分离、从数据库表反向生成模型等操作,可以参考官方文档进行学习。
希望此文能对你有所帮助。
联系我
博客地址:https://jianghushinian.cn/
参考
GORM源码:https://github.com/go-gorm/gorm
GORM文档:https://gorm.io/zh_CN/