Go Web笔记
net/http
请求
Get、Head、Post和PostForm函数发出HTTP/HTTPS请求。
1 | resp, err := http.Get("http://example.com/") |
程序在使用完response后必须关闭回复的主体。
1 | resp, err := http.Get("http://example.com/") |
- GET请求示例
关于GET请求的参数需要使用Go语言内置的net/url
这个标准库来处理。
1 | package main |
对应的Server端HandlerFunc如下:
1 | func getHandler(w http.ResponseWriter, r *http.Request) { |
- Post请求示例
上面演示了使用net/http
包发送GET
请求的示例,发送POST
请求的示例代码如下:
1 | package main |
对应的Server端HandlerFunc如下:
1 | func postHandler(w http.ResponseWriter, r *http.Request) { |
自定义Client
要管理HTTP客户端的头域、重定向策略和其他设置,创建一个Client:
1 | client := &http.Client{ |
自定义Transport
要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport:
1 | tr := &http.Transport{ |
Client和Transport类型都可以安全的被多个goroutine同时使用。出于效率考虑,应该一次建立、尽量重用。
服务端
默认的Server
ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。处理器参数通常是nil,这表示采用包变量DefaultServeMux作为处理器。
Handle和HandleFunc函数可以向DefaultServeMux添加处理器。
1 | http.Handle("/foo", fooHandler) // 将"/foo"路径的请求交给fooHandler进行处理 |
默认的Server示例
使用Go语言中的net/http
包来编写一个简单的接收HTTP请求的Server端示例,net/http
包是对net包的进一步封装,专门用来处理HTTP协议的数据。具体的代码如下:
1 | func sayHello(w http.ResponseWriter, r *http.Request) { |
fmt.Fprintln
函数用于将格式化的文本写入到一个 io.Writer
接口的实例中,并在最后添加一个换行符。这里将文本内容写入到 http.ResponseWriter
对象中,从而将该文本作为响应的一部分发送给客户端。
Gin
Gin 是一个用 Go (Golang) 编写的 web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架,由于 httprouter,速度提高了近 40 倍。
特性
快速
基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。
支持中间件
传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。
Crash 处理
Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!
JSON 验证
Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。
路由组
更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。
错误管理
Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。
内置渲染
Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。
可扩展性
新建一个中间件非常简单,去查看示例代码吧。
示例
1 | package main |
c.JSON
:返回JSON格式的数据gin.H
:Gin 框架提供的一种类型,类似于 Go 语言中的 map[string]interface{},用于构建 JSON 响应的键值对
其他请求
1 | func main() { |
Gin渲染
HTML渲染
我们首先定义一个存放模板文件的templates
文件夹,然后在其内部按照业务分别定义一个posts
文件夹和一个users
文件夹。
posts/index.html
文件的内容如下:
1 | {{define "posts/index.html"}} |
users/index.html
文件的内容如下:
1 | {{define "users/index.html"}} |
Gin框架中使用LoadHTMLGlob()
或者LoadHTMLFiles()
方法进行HTML模板渲染。
1 | func main() { |
自定义模板函数
定义一个不转义相应内容的safe
模板函数如下:
1 | func main() { |
在index.tmpl
中使用定义好的safe
模板函数:
1 | <!DOCTYPE html> |
静态文件处理
当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static
方法即可。
1 | func main() { |
使用模板继承
Gin框架默认都是使用单模板,如果需要使用block template
功能,可以通过"github.com/gin-contrib/multitemplate"
库实现,具体示例如下:
首先,假设我们项目目录下的templates文件夹下有以下模板文件,其中home.tmpl
和index.tmpl
继承了base.tmpl
:
1 | templates |
然后我们定义一个loadTemplates
函数如下:
1 | func loadTemplates(templatesDir string) multitemplate.Renderer { |
我们在main
函数中
1 | func indexFunc(c *gin.Context){ |
补充文件路径处理
关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
1 | func getCurrentPath() string { |
JSON渲染
1 | func main() { |
XML渲染
注意需要使用具名的结构体类型。
1 | func main() { |
YMAL渲染
1 | r.GET("/someYAML", func(c *gin.Context) { |
protobuf渲染
1 | r.GET("/someProtoBuf", func(c *gin.Context) { |
获取参数
获取querystring参数
相当于@RequestParam
querystring
获取请求的querystring参数的方法如下:
1 | func main() { |
获取form参数
当前端请求的数据通过form表单提交时,例如向/user/search
发送一个POST请求,获取请求数据的方式如下:
1 | func main() { |
获取json参数
类似于@ResponseBody
,当前端请求的数据通过JSON提交时,例如向/json
发送一个POST请求,则获取请求参数的方式如下:
1 | r.POST("/json", func(c *gin.Context) { |
更便利的获取请求参数的方式,参见下面的 参数绑定 。
获取path参数
请求的参数通过URL路径传递,例如:/user/search/小王子/沙河
。 获取请求URL路径中的参数的方式如下。
1 | r.GET("/user/search/:username/:address", func(c *gin.Context) { |
参数绑定
要将请求体绑定到结构体中,使用模型绑定。 Gin目前支持JSON、XML、YAML和标准表单值的绑定。
Type
- Must bind
- Methods -
Bind
,BindJSON
,BindXML
,BindQuery
,BindYAML
- Behavior - 这些方法属于
MustBindWith
的具体调用。 如果发生绑定错误,则请求终止,并触发c.AbortWithError(400, err).SetType(ErrorTypeBind)
。响应状态码被设置为 400 并且Content-Type
被设置为text/plain; charset=utf-8
。 如果您在此之后尝试设置响应状态码,Gin会输出日志[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422
。 - 如果希望更好地控制绑定,考虑使用
ShouldBind
等效方法。
Type
- Should bind
- Methods -
ShouldBind
,ShouldBindJSON
,ShouldBindXML
,ShouldBindQuery
,ShouldBindYAML
- Behavior - 这些方法属于
ShouldBindWith
的具体调用。 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求。
使用 Bind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定。
可以指定必须绑定的字段。 如果一个字段的 tag 加上了 binding:"required"
,但绑定时是空值, Gin 会报错。
1 | // Binding from JSON |
ShouldBind
会按照下面的顺序解析请求中的数据完成绑定:
- 如果是
GET
请求,只使用Form
绑定引擎(query
)。 - 如果是
POST
请求,首先检查content-type
是否为JSON
或XML
,然后再使用Form
(form-data
)。
文件上传
单个文件上传
文件上传前端页面代码:
1 |
|
后端代码:
1 | func main() { |
多个文件上传
1 | func main() { |
重定向
HTTP重定向
HTTP 重定向很容易。 内部、外部重定向均支持。
1 | r.GET("/test", func(c *gin.Context) { |
路由重定向
路由重定向,使用HandleContext
:
1 | r.GET("/test", func(c *gin.Context) { |
Gin路由
普通路由
1 | r.GET("/index", func(c *gin.Context) {...}) |
此外,还有一个可以匹配所有请求方法的Any
方法如下:
1 | r.Any("/test", func(c *gin.Context) {...}) |
为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html
页面。
1 | r.NoRoute(func(c *gin.Context) { |
路由组
我们可以将拥有共同URL前缀的路由划分为一个路由组。
习惯性一对{}
包裹同组的路由,这只是为了看着清晰,用不用{}
包裹功能上没什么区别。
1 | func main() { |
路由组也是支持嵌套的,例如:
1 | shopGroup := r.Group("/shop") |
Gin中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的Hook函数。
这个函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
类似于filter、拦截器
定义中间件
Gin中的中间件必须是一个gin.HandlerFunc
类型。
1 | // TokenIntercept Token校验拦截器 |
注册中间件
在gin框架中,我们可以为每个路由添加任意数量的中间件。
为全局路由注册
1 | func main() { |
为某个路由单独注册
1 | // 给/test2路由单独注册中间件(可注册多个) |
为路由组注册中间件
为路由组注册中间件有以下两种写法。
写法1:
1 | shopGroup := r.Group("/shop", Middleware()) |
写法2:
1 | shopGroup := r.Group("/shop") |
中间件注意事项
gin默认中间件
gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
。Recovery
中间件会recover任何panic
。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()
新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine
当在中间件或handler
中启动新的goroutine
时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()
)。
运行多个服务
我们可以在多个端口启动服务,例如:
1 | package main |
GORM
GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
连接数据库
连接不同的数据库都需要导入对应数据的驱动程序
1 | import _ "github.com/jinzhu/gorm/dialects/mysql" |
连接MySQL
1 | import ( |
注意:想要正确的处理
time.Time
,需要带上parseTime
参数, (更多参数) 要支持完整的 UTF-8 编码,您需要将charset=utf8
更改为charset=utf8mb4
查看 此文章 获取详情
示例
操作MySQL
使用GORM连接上面的db1
进行创建、查询、更新、删除操作。
1 | package main |
Model定义
在使用ORM工具时,通常我们需要在代码中定义模型(Models)与数据库中的数据表进行映射,在GORM中模型(Models)通常是正常定义的结构体、基本的go类型或它们的指针。 同时也支持sql.Scanner
及driver.Valuer
接口(interfaces)。
gorm.Model
为了方便模型定义,GORM内置了一个gorm.Model
结构体。gorm.Model
是一个包含了ID
, CreatedAt
, UpdatedAt
, DeletedAt
四个字段的Golang结构体。
1 | // gorm.Model 的定义 |
- 包含的字段:
ID
:每个记录的唯一标识符(主键)。CreatedAt
:在创建记录时自动设置为当前时间。UpdatedAt
:每当记录更新时,自动更新为当前时间。DeletedAt
:用于软删除(将记录标记为已删除,而实际上并未从数据库中删除)。
可以将它嵌入到你自己的模型中:
1 | // 将 `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`字段注入到`User`模型中 |
当然也可以完全自己定义模型:
1 | // 不使用gorm.Model,自行定义模型 |
模型定义示例
1 | type User struct { |
指针类型表示可空字段
在 Go 中,对于引用类型(如指针、slice、map、channel、interface)的变量,可以将它们初始化为
nil
,表示空值或者未指向任何有效对象。基本类型(如整型、浮点型、布尔型等)是不能被初始化为
nil
的,因为它们不是引用类型,而是值类型。基本类型的零值取决于具体类型,而不是nil
。
结构体标记(tags)
(最新版本的GORM推荐使用驼峰命名的标签)
使用结构体声明模型时,标记(tags)是可选项。gorm支持以下标记:
标签名 | 说明 |
---|---|
column | 指定 db 列名 |
type | 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not null 、size , autoIncrement … 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT |
serializer | 指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime |
size | 定义列数据类型的大小或长度,例如 size: 256 |
primaryKey | 将列定义为主键 |
unique | 将列定义为唯一键 |
default | 定义列的默认值 |
precision | 指定列的精度 |
scale | 指定列大小 |
not null | 指定列为 NOT NULL |
autoIncrement | 指定列为自动增长 |
autoIncrementIncrement | 自动步长,控制连续记录之间的间隔 |
embedded | 嵌套字段 |
embeddedPrefix | 嵌入字段的列名前缀 |
autoCreateTime | 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano /milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano |
autoUpdateTime | 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano /milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli |
index | 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情 |
uniqueIndex | 与 index 相同,但创建的是唯一索引 |
check | 创建检查约束,例如 check:age > 13 ,查看 约束 获取详情 |
<- | 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 |
-> | 设置字段读的权限,->:false 无读权限 |
- | 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限 |
comment | 迁移时为字段添加注释 |
关联标签(Association Tags)
GORM中的关联标签通常用于指定如何处理模型之间的关联。 这些标签定义了一些关系细节,比如外键,引用和约束。 理解这些标签对于有效地建立和管理模型之间的关系而言至关重要。
标签 | 描述 |
---|---|
foreignKey |
Specifies the column name of the current model used as a foreign key in the join table. |
references |
Indicates the column name in the reference table that the foreign key of the join table maps to. |
polymorphic |
Defines the polymorphic type, typically the model name. |
polymorphicValue |
Sets the polymorphic value, usually the table name, if not specified otherwise. |
many2many |
Names the join table used in a many-to-many relationship. |
joinForeignKey |
Identifies the foreign key column in the join table that maps back to the current model’s table. |
joinReferences |
Points to the foreign key column in the join table that links to the reference model’s table. |
constraint |
Specifies relational constraints like OnUpdate , OnDelete for the association. |
主键、表名、列名的约定
主键(Primary Key)
GORM 默认会使用名为ID的字段作为表的主键。
1 | type User struct { |
表名(Table Name)
表名默认就是结构体名称的复数,例如:
1 | type User struct {} // 默认表名是 `users` |
也可以通过Table()
指定表名:
1 | // 使用User结构体创建名为`deleted_users`的表 |
GORM还支持更改默认表名称规则:
1 | gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string { |
列名(Column Name)
列名由字段名称进行下划线分割来生成
1 | type User struct { |
可以使用结构体tag指定列名:
1 | type Animal struct { |
时间戳跟踪
CreatedAt
如果模型有 CreatedAt
字段,该字段的值将会是初次创建记录的时间。
1 | db.Create(&user) // `CreatedAt`将会是当前时间 |
UpdatedAt
如果模型有UpdatedAt
字段,该字段的值将会是每次更新记录的时间。
1 | db.Save(&user) // `UpdatedAt`将会是当前时间 |
DeletedAt
如果模型有DeletedAt
字段,调用Delete
删除该记录时,将会设置DeletedAt
字段为当前时间,而不是直接将记录从数据库中删除。
CRUD 接口
创建
创建记录
1 | user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} |
db.Create()
方法可以接受结构体实例的指针(&
)或者结构体实例本身作为参数。因为 GORM 在内部会检查参数类型,并根据需要进行处理。
但是后者会进行拷贝,消耗过多的内存,所以一般使用指针。
默认值
可以通过 tag 定义字段的默认值,比如:
1 | type Animal struct { |
生成的 SQL 语句会排除没有值或值为零值的字段。
1 | var animal = Animal{Age: 99, Name: ""} |
注意: 所有字段的零值, 比如 0
, ''
, false
或者其它零值,都不会保存到数据库内,但会使用他们的默认值。
如果在数据库中将字段标记为非空(NOT NULL),而在 GORM 中没有设置默认值,并且代码中传递了零值给这个字段,会导致数据库操作失败。
如果想避免这种情况,可以考虑使用指针或实现 Scanner/Valuer 接口,比如:
1 | // 使用指针 |
在Hooks中设置字段值
如果你想在BeforeCreate
hook 中修改字段的值,可以使用scope.SetColumn
,例如:
1 | func (user *User) BeforeCreate(scope *gorm.Scope) error { |
扩展创建选项
1 | // 为Instert语句添加扩展SQL选项 |
查询
检索单个对象
GORM 提供了 First
、Take
、Last
方法,以便从数据库中检索单个对象。
当查询数据库时它添加了 LIMIT 1
条件,且没有找到记录时,它会返回 ErrRecordNotFound
错误
1 | var user User |
如果你想避免
ErrRecordNotFound
错误,你可以使用Find
,比如db.Limit(1).Find(&user)
,Find
方法可以接受struct和slice的数据。
对单个对象使用
Find
而不带limit,db.Find(&user)
将会查询整个表并且只返回第一个对象,只是性能不高并且不确定的。
First
andLast
方法会按主键排序找到第一条记录和最后一条记录 (分别)。 只有在目标 struct 是指针或者通过db.Model()
指定 model 时,该方法才有效。 此外,如果相关 model 没有定义主键,那么将按 model 的第一个字段进行排序。
根据主键检索
1 | db.First(&user, 10) |
如果主键是字符串(例如像uuid),查询将被写成如下:
1 | db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a") |
当目标对象有一个主键值时,将使用主键构建查询条件,例如:
1 | var user = User{ID: 10} |
NOTE: 如果您使用 gorm 的特定字段类型(例如
gorm.DeletedAt
),它将运行不同的查询来检索对象。
1 | type User struct { |
检索全部对象
1 | // Get all records |
条件查询
String 条件
1 | // Get first matched record |
如果对象设置了主键,条件查询将不会覆盖主键的值,而是用 And 连接条件。 例如:
1
2
3 var user = User{ID: 10}
db.Where("id = ?", 20).First(&user)
// SELECT * FROM users WHERE id = 10 and id = 20 ORDER BY id ASC LIMIT 1这个查询将会给出
record not found
错误 所以,在你想要使用例如user
这样的变量从数据库中获取新值前,需要将例如id
这样的主键设置为nil。
Struct & Map 条件
1 | // Struct |
注意 当使用结构体进行查询时,GORM 只会使用非零值字段进行查询。如果字段值是
0
、''
、false
或其他零值,它不会被用来构建查询条件
1 | db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users) |
要在查询条件中包含零值,可以使用一个 map,该 map 将包含所有键值对作为查询条件,例如:
1 | db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users) |
指定结构体查询字段
在使用结构体进行搜索时,可以通过向 Where()
方法传递相关字段名或数据库字段名来指定在查询条件中使用结构体的哪些特定值,例如:
1 | db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users) |
内联条件
查询条件可以像 Where
方法一样内联到 First
和 Find
方法中。
1 | // Get by primary key if it were a non-integer type |
Not 条件
构建 NOT 条件,与 Where
方法类似工作。
1 | db.Not("name = ?", "jinzhu").First(&user) |
Or 条件
1 | db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) |
选择特定字段
Select
允许您指定要从数据库中检索的字段。否则,GORM 默认将选择所有字段。
1 | db.Select("name", "age").Find(&users) |
排序
在从数据库检索记录时,可以指定顺序。(默认升序)
1 | db.Order("age desc, name").Find(&users) |
Limit & Offset
Limit
指定要检索的记录的最大数量,Offset
指定在开始返回记录之前要跳过的记录数。
1 | db.Limit(3).Find(&users) |
分页查询
1 | func Paginate(r *http.Request) func(db *gorm.DB) *gorm.DB { |
Distinct
GORM 中的 Distinct
方法允许从模型中选择不同的数值,基于指定的字段。
1 | db.Distinct("name", "age").Order("name, age desc").Find(&results) |
值得注意的是,Distinct
方法也可以与 Pluck
和 Count
方法一起使用。
Joins
Joins
方法用于指定连接条件。
1 | type result struct { |
高级查询
智能选择字段
在 GORM 中,您可以使用 Select
方法有效地选择特定字段。 这在Model字段较多但只需要其中部分的时候尤其有用,比如编写API响应。
1 | type User struct { |
注意 在
QueryFields
模式中, 所有的模型字段(model fields)都会被根据他们的名字选择。
锁
GORM 支持多种类型的锁,例如:
1 | // 基本的 FOR UPDATE 锁 |
上述语句将会在事务(transaction)中锁定选中行(selected rows)。 可以被用于以下场景:当你准备在事务(transaction)中更新(update)一些行(rows)时,并且想要在本事务完成前,阻止(prevent)其他的事务(other transactions)修改你准备更新的选中行。
Strength
也可以被设置为 SHARE
,这种锁只允许其他事务读取(read)被锁定的内容,而无法修改(update)或者删除(delete)。
1 | db.Clauses(clause.Locking{ |
Table
选项用于指定将要被锁定的表。 这在你想要 join 多个表,并且锁定其一时非常有用。
你也可以提供如 NOWAIT
的Options,这将尝试获取一个锁,如果锁不可用,导致了获取失败,函数将会立即返回一个error。 当一个事务等待其他事务释放它们的锁时,此Options(Nowait)可以阻止这种行为
1 | db.Clauses(clause.Locking{ |
Options也可以是SKIP LOCKED
,设置后将跳过所有已经被其他事务锁定的行(any rows that are already locked by other transactions.)。 这次高并发情况下非常有用:那时你可能会想要对未经其他事务锁定的行进行操作(process )。
想了解更多高级的锁策略,请参阅 Raw SQL and SQL Builder。
子查询
子查询(Subquery)是SQL中非常强大的功能,它允许嵌套查询。 当你使用 *gorm.DB 对象作为参数时,GORM 可以自动生成子查询。
1 | // 简单的子查询 |
From 子查询
GORM 允许在 FROM 子句中使用子查询,从而支持复杂的查询和数据组织。
1 | // 在 FROM 子句中使用子查询 |
Group 条件
GORM 中的Group条件(Group Conditions)提供了一种可读性更强,操作性更强的方法来写复杂的,涉及多个条件的 SQL 查询。
1 | // 使用 Group 条件的复杂 SQL 查询 |
带多个列的 In
GROM 支持多列的 IN 子句(the IN clause with multiple columns),允许你在单次查询里基于多个字段值筛选数据。
1 | // 多列 IN |
命名参数
GORM 支持命名的参数,提高SQL 查询的可读性和可维护性。 此功能使查询结构更加清晰、更加有条理,尤其是在有多个参数的复杂查询中。 命名参数可以使用 sql.NamedArg
或 map[string]interface{}{}}
,你可以根据你的查询结构灵活提供。
1 | // 使用 sql.NamedArg 命名参数的例子 |
欲了解更多示例和详细信息,请参阅 Raw SQL 和 SQL Builder
Find 至 map
GORM 提供了灵活的数据查询,允许将结果扫描进(scanned into)map[string]interface{}
or []map[string]interface{}
,这对动态数据结构非常有用。
当使用 Find To Map
时,一定要在你的查询中包含 Model
或者 Table
,以此来显式地指定表名。 这能确保 GORM 正确的理解哪个表要被查询。
1 | // 扫描第一个结果到 map with Model 中 |
FirstOrInit
GORM 的 FirstOrInit
方法用于获取与特定条件匹配的第一条记录,如果没有成功获取,就初始化一个新实例。 这个方法与结构和map条件兼容,并且在使用 Attrs
和 Assign
方法时有着更多的灵活性。
1 | // 如果没找到 name 为 "non_existing" 的 User,就初始化一个新的 User |
使用 Attrs
进行初始化
当记录未找到,你可以使用 Attrs
来初始化一个有着额外属性的结构体。 这些属性包含在新结构中,但不在 SQL 查询中使用。
1 | // 如果没找到 User,根据所给条件和额外属性初始化 User |
为属性使用 Assign
Assign
方法允许您在结构上设置属性,不管是否找到记录。 这些属性设定在结构上,但不用于生成 SQL 查询,最终数据不会被保存到数据库。
1 | // 根据所给条件和分配的属性初始化,不管记录是否存在 |
FirstOrInit
, 以及 Attrs
和 Assign
, 提供了一种强大和灵活的方法来确保记录的存在,并且在一个步骤中以特定的属性初始化或更新。
FirstOrCreate
FirstOrCreate
用于获取与特定条件匹配的第一条记录,或者如果没有找到匹配的记录,创建一个新的记录。 这个方法在结构和map条件下都是有效的。 受RowsAffected的
属性有助于确定创建或更新记录的数量。
1 | // 如果没找到,就创建一个新纪录 |
配合 Attrs
使用 FirstOrCreate
Attrs
可以用于指定新记录的附加属性。 这些属性用于创建,但不在初始搜索查询中。
1 | // 如果没找到,根据额外属性创建新的记录 |
配合 Assign
使用 FirstOrCreate
不管记录是否被找到,Assign
方法都会设置记录中的属性。 并且这些属性被保存到数据库。
1 | // 如果没找到记录,通过 `Assign` 属性 初始化并且保存新的记录 |
优化器、索引提示
GORM 包括对优化器和索引提示的支持, 允许您影响查询优化器的执行计划。 这对于优化查询性能或处理复杂查询尤其有用。
优化器提示是说明数据库查询优化器应如何执行查询的指令。 GORM 通过 gorm.io/hints 包简化了优化器提示的使用。
1 | import "gorm.io/hints" |
索引提示
索引提示为数据库提供关于使用哪些索引的指导。 如果查询规划者没有为查询选择最有效的索引,它们(索引提示)将是有好处的。
1 | import "gorm.io/hints" |
这些提示会对查询性能和行为产生显著影响(significantly impact),特别是在大型数据库或复杂的数据模型中。 欲了解更详细的信息和其他示例,请参阅GORM 文档中的 Optimizer Hints/Index/Comment。
迭代
GORM 支持使用 Rows
方法对查询结果进行迭代。 当您需要处理大型数据集或在每个记录上单独执行操作时,此功能特别有用。
您可以通过对查询返回的行进行迭代,扫描每行到一个结构体中。 该方法提供了对如何处理每条记录的粒度控制。(granular control)。
1 | rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows() |
这种方法非常适合于使用标准查询方法无法轻松实现的复杂数据处理。
FindInBatches
FindInBatches
允许分批查询和处理记录。 这对于有效地处理大型数据集、减少内存使用和提高性能尤其有用。
使用FindInBatches
, GORM 处理指定批大小的记录。 在批处理功能中,您可以对每批记录应用操作。
1 | // 处理记录,批处理大小为100 |
FindInBatches
是处理大量可管理数据的有效工具,可以优化资源使用和性能。
查询钩子
GORM 提供了使用钩子的能力,例如 AfterFind
,这些钩子是在查询的生命周期中触发的。 允许开发者在从数据库中检索记录后执行自定义逻辑。这个钩子对于在查询后操纵数据或设置默认值非常有用。
此钩子对后查询数据操纵或默认值设置非常有用。 欲了解更详细的信息和额外的钩子,请参阅GORM文档中的 Hooks。
1 | func (u *User) AfterFind(tx *gorm.DB) (err error) { |
Pluck
GORM 中的 Pluck
方法用于从数据库中查询单列并扫描结果到片段(slice)。 当您需要从模型中检索特定字段时,此方法非常理想。
如果需要查询多个列,可以使用 Select
配合 Scan 或者 Find 来代替。
1 | // 检索所有用户的 age |
Scope
GORM中的 Scopes
是一个强大的特性,它允许您将常用的查询条件定义为可重用的方法。 这些作用域可以很容易地在查询中引用,从而使代码更加模块化和可读。
定义 Scopes
Scopes
被定义为被修改后返回一个 gorm.DB
实例的函数。 您可以根据您的应用程序的需要定义各种条件作为范围。
1 | // Scope for filtering records where amount is greater than 1000 |
在查询中使用 Scopes
你可以通过 Scopes
方法使用一个或者多个 Scope 来查询。 这允许您动态地连接多个条件。
1 | // 使用 scopes 来寻找所有的 金额大于1000的信用卡订单 |
Scopes
是封装普通查询逻辑的一种干净而有效的方式,增强了代码的可维护性和可读性。 更详细的示例和用法,请参阅GORM 文档中的 范围。
Count
GORM中的 Count
方法用于检索匹配给定查询的记录数。 这是了解数据集大小的一个有用的功能,特别是在涉及有条件查询或数据分析的情况下。
得到匹配记录的 Count
您可以使用 Count
来确定符合您的查询中符合特定标准的记录的数量。
1 | var count int64 |
配合 Distinct 和 Group 使用 Count
GORM还允许对不同的值进行计数并对结果进行分组。
1 | // 为不同 name 计数 |
更新
保存所有字段
Save
会保存所有的字段,即使字段是零值
1 | db.First(&user) |
保存
是一个组合函数。 如果保存值不包含主键,它将执行 Create
,否则它将执行 Update
(包含所有字段)。
1 | db.Save(&User{Name: "jinzhu", Age: 100}) |
NOTE不要将
Save
和Model
一同使用, 这是 未定义的行为。
更新单个列
当使用 Update
更新单列时,需要有一些条件,否则将会引起ErrMissingWhereClause
错误,查看 阻止全局更新 了解详情。 当使用 Model
方法,并且它有主键值时,主键将会被用于构建条件,例如:
1 | // 根据条件更新 |
更新多列
Updates
方法支持 struct
和 map[string]interface{}
参数。当使用 struct
更新时,默认情况下GORM 只会更新非零值的字段
1 | // 根据 `struct` 更新属性,只会更新非零值的字段 |
注意 使用 struct 更新时, GORM 将只更新非零值字段。 你可能想用
map
来更新属性,或者使用Select
声明字段来更新
更新选定字段
如果您想要在更新时选择、忽略某些字段,您可以使用 Select
、Omit
1 | // 选择 Map 的字段 |
更新 Hook
GORM 支持的 hook 包括:BeforeSave
, BeforeUpdate
, AfterSave
, AfterUpdate
. 更新记录时将调用这些方法,查看 Hooks 获取详细信息
1 | func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { |
批量更新
如果我们没有使用 Model
方法指定具有主键值的记录,GORM 将执行批量更新操作。
1 | // Update with struct |
阻止全局更新
如果执行批量更新时没有任何条件,GORM 默认情况下不会运行更新操作,并会返回 ErrMissingWhereClause
错误。您可以通过使用条件、原始 SQL 或启用 AllowGlobalUpdate
模式来解决这个问题,例如:
1 | db.Model(&User{}).Update("name", "jinzhu").Error // gorm.ErrMissingWhereClause |
更新的记录数
可以通过 RowsAffected
方法获取更新操作影响的记录数
1 | // Get updated records count with `RowsAffected` |
高级选项
使用 SQL 表达式更新
GORM 允许使用 SQL 表达式更新列,例如:
1 | // product's ID is `3` |
并且 GORM 还允许使用具有 自定义数据类型 的 SQL 表达式进行更新,例如:
1 | // Create from customized data type |
根据子查询进行更新
使用 子查询 更新表
1 | db.Model(&user).Update("company_name", db.Model(&Company{}).Select("name").Where("companies.id = users.company_id")) |
不使用 Hook 和时间追踪
如果想跳过 Hooks
方法,并且不跟踪更新时的更新时间,可以使用 UpdateColumn
、UpdateColumns
,其工作原理类似于 Update
、Updates
1 | // Update single column |
返回修改行的数据
返回更改的数据仅适用于支持返回功能的数据库,例如:
1 | // return all columns |
检查字段是否有变更?
GORM 提供了 Changed
方法,可用于 Before Update Hooks,它将返回字段是否已更改。
Changed
方法仅适用于方法 Update
、Updates
,它仅检查 Update
/ Updates
的更新值是否等于模型值。如果它已更改且未被省略,它将返回 true
1 | func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { |
在 Update 时修改值
要在 Before Hooks 中更改更新值,您应该使用 SetColumn
,除非它是使用 Save
进行完整更新,例如:
1 | func (user *User) BeforeSave(tx *gorm.DB) (err error) { |
删除
删除一条记录
删除一条记录时,删除对象需要指定主键,否则会触发 批量删除,例如:
1 | // Email 的 ID 是 `10` |
根据主键删除
GORM 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字(如以下例子。也可以使用字符串——译者注)。查看 查询-内联条件(Query Inline Conditions) 了解详情。
1 | db.Delete(&User{}, 10) |
钩子函数
对于删除操作,GORM 支持 BeforeDelete
、AfterDelete
Hook,在删除记录时会调用这些方法,查看 Hook 获取详情
1 | func (u *User) BeforeDelete(tx *gorm.DB) (err error) { |
批量删除
如果指定的值不包括主属性,那么 GORM 会执行批量删除,它将删除所有匹配的记录
1 | db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{}) |
可以将一个主键切片传递给Delete
方法,以便更高效的删除数据量大的记录
1 | var users = []User{{ID: 1}, {ID: 2}, {ID: 3}} |
阻止全局删除
当你试图执行不带任何条件的批量删除时,GORM将不会运行并返回ErrMissingWhereClause
错误
如果一定要这么做,你必须添加一些条件,或者使用原生SQL,或者开启AllowGlobalUpdate
模式,如下例:
1 | db.Delete(&User{}).Error // gorm.ErrMissingWhereClause |
返回删除行的数据
返回被删除的数据,仅当数据库支持回写功能时才能正常运行,如下例:
1 | // 回写所有的列 |
软删除
如果你的模型包含了 gorm.DeletedAt
字段(该字段也被包含在gorm.Model
中),那么该模型将会自动获得软删除的能力!
当调用Delete
时,GORM并不会从数据库中删除该记录,而是将该记录的DeleteAt
设置为当前时间,而后的一般查询方法将无法查找到此条记录。
1 | // user's ID is `111` |
如果你并不想嵌套gorm.Model
,你也可以像下方例子那样开启软删除特性:
1 | type User struct { |
查找被软删除的记录
你可以使用Unscoped
来查询到被软删除的记录
1 | db.Unscoped().Where("age = 20").Find(&users) |
永久删除
你可以使用 Unscoped
来永久删除匹配的记录
1 | db.Unscoped().Delete(&order) |
删除标志
默认情况下,gorm.Model
使用*time.Time
作为DeletedAt
的字段类型,不过软删除插件gorm.io/plugin/soft_delete
同时也提供其他的数据格式支持
提示 当使用DeletedAt创建唯一复合索引时,你必须使用其他的数据类型,例如通过
gorm.io/plugin/soft_delete
插件将字段类型定义为unix时间戳等等
1
2
3
4
5
6
7 import "gorm.io/plugin/soft_delete"
type User struct {
ID uint
Name string `gorm:"uniqueIndex:udx_name"`
DeletedAt soft_delete.DeletedAt `gorm:"uniqueIndex:udx_name"`
}
- Unix 时间戳
使用unix时间戳作为删除标志
1 | import "gorm.io/plugin/soft_delete" |
你同样可以指定使用毫秒 milli
或纳秒 nano
作为值,如下例:
1 | type User struct { |
- 使用
1
/0
作为 删除标志
1 | import "gorm.io/plugin/soft_delete" |
- 混合模式
混合模式可以使用 0
,1
或者unix时间戳来标记数据是否被软删除,并同时可以保存被删除时间
1 | type User struct { |
关联
Belongs To
Belongs To
belongs to
会与另一个模型建立了一对一的连接。 这种模型的每一个实例都“属于”另一个模型的一个实例。
例如,您的应用包含 user 和 company,并且每个 user 能且只能被分配给一个 company。下面的类型就表示这种关系。 注意,在 User
对象中,有一个和 Company
一样的 CompanyID
。 默认情况下, CompanyID
被隐含地用来在 User
和 Company
之间创建一个外键关系, 因此必须包含在 User
结构体中才能填充 Company
内部结构体。
1 | // `User` 属于 `Company`,`CompanyID` 是外键 |
请参阅 预加载 以了解内部结构的详细信息。
重写外键
要定义一个 belongs to 关系,数据库的表中必须存在外键。默认情况下,外键的名字,使用拥有者的类型名称加上表的主键的字段名字
例如,定义一个User实体属于Company实体,那么外键的名字一般使用CompanyID。
GORM同时提供自定义外键名字的方式,如下例所示。
1 | type User struct { |
重写引用
对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。
正如上面的例子,我们使用主表Company中的主键字段ID作为外键的参考值。
如果设置了User实体属于Company实体,那么GORM会自动把Company中的ID
属性保存到User的CompanyID
属性中。
同样的,您也可以使用标签 references
来更改它,例如:
1 | type User struct { |
NOTE 如果外键名恰好在拥有者类型中存在,GORM 通常会错误的认为它是
has one
关系。我们需要在belongs to
关系中指定references
1 | type User struct { |
Belongs to 的 CRUD
查看 关联模式 获取 belongs to 相关的用法
预加载
GORM 可以通过 Preload
、Joins
预加载 belongs to 关联的记录,查看 预加载 获取详情
外键约束
你可以通过 constraint
标签配置 OnUpdate
、OnDelete
实现外键约束,在使用 GORM 进行迁移时它会被创建,例如:
1 | type User struct { |
- 本文作者: NICK
- 本文链接: https://nicccce.github.io/TechNotes/Go-Web/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!