GoFrame是一款模块化、高性能、企业级的Go基础开发框架。GoFrame是一款通用性的基础开发框架,是Golang标准库的一个增强扩展级,包含通用核心的基础开发组件,优点是实战化、模块化、文档全面、模块丰富、易用性高、通用性强、面向团队。GoFrame既可用于开发完整的工程化项目,由于框架基础采用模块化解耦设计,因此也可以作为工具库使用。
官方文档:https://goframe.org/pages/viewpage.action?pageId=1114119
视频教程:https://www.bilibili.com/video/BV1Uu4y1u7kX
学习笔记:https://gitee.com/unlimited13/code
准备工作
前置条件
- 已安装Go语言开发环境,已配置好
GOROOT
、GOPATH
环境变量 - 熟悉Go语言语法与基本使用
安装框架
下载地址:https://github.com/gogf/gf/releases
下载对应操作系统的包并安装,推荐安装到GOROOT
的bin目录中
使用gf -v
命令查看是否安装成功
项目初始化
先修改Go语言开发环境:一次性配置,如果已经设置过可以跳过
1
2go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn-u
参数表示获取最新版本的框架依赖,如果之前创建过项目,可省略
1
gf init hellogf -u
项目启动
进入项目中main.go
文件的所在目录运行该命令启动项目
1
gf run main.go
http://127.0.0.1:8000/hello
查看结果
项目目录结构
1 | / |
路由管理
路由管理是HTTP Web Server
开发的基础 ### 路由规则
最基础的路由绑定方法是BindHandler
方法,我们来看一下之前一直使用的BindHandler
的原型:
1
func (s *Server) BindHandler(pattern string, handler interface{})
pattern
参数用于指定路由注册规则的字符串,参数格式如下:
1
[HTTPMethod:]路由规则[@域名]
HTTPMethod:
和@域名
为可选参数,如果指定了HTTPMethod
,那么路由规则仅会在该请求方式下有效。@域名
可以指定生效的域名名称,那么该路由规则仅会在该域名下生效。比如:
1 | // 该路由规则仅会在GET请求及localhost域名下有效 |
❗
BindHandler
是最原生的路由注册方法,在大部分场景中,我们通常使用分组路由方式来管理理由,后续会介绍
- 其中
handler
参数用于指定路由函数,这个路由函数只需要满足以下要求,即只要能接收请求对象*ghttp.Request
即可让我们先关注1
2
3func(r *ghttp.Request) {
// ...
}internal/cmd
目录下的cmd.go
文件,cmd层负责引导程序启动,显著的工作是初始化逻辑、注册路由对象、启动server监听、阻塞运行程序直至server退出
了解该文件作用后,我们在cmd.go
中为一个函数注册路由:
1
2
3
4
5
6
7
8
9
10Main = gcmd.Command{
// ...
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
s := g.Server()
s.BindHandler("POST:/hello", func(r *ghttp.Request){
r.Response.WriteJson(r.Router)
})
// ...
},
}http://127.0.0.1:8000/hello
并以POST方法进行访问测试,结果应当是该函数返回的r.Router
,即当前匹配的路由规则信息
- 路由规则分为精准匹配规则和动态匹配规则
精准匹配规则是指已确定名称的规则,比如/user
、/article
这种。在大多数场景下,我们会同时使用两种匹配规则,比如user/:userId
或user/id={id}
动态匹配规则分为命名匹配规则、模糊匹配规则、字段匹配规则,详细内容在官方文档中写得相当清楚,这里便不再具体展开
官方文档:https://goframe.org/pages/viewpage.action?pageId=1114257
路由注册
函数注册
函数注册在上文中的例子中已经出现过了,其中handler
不单单可以是一个简单函数,它还可以是包方法,也可以是实例化对象的方法,只要这些函数接收*ghttp.Request
类型的参数就能作为handler
官方文档:https://goframe.org/pages/viewpage.action?pageId=1114240
对象注册
对象注册是通过一个实例化的对象来执行路由注册,以后每一个请求都交给该对象(同一对象)处理,该对象常驻内存不释放。
相关方法: 1
2
3
4
5func (s *Server) BindObject(pattern string, object interface{}, [methods ...string]) error
func (s *Server) BindObjectMethod(pattern string, object interface{}, method string) error
func (s *Server) BindObjectRest(pattern string, object interface{}) error*ghttp.Request
类型的参数
绑定对象公开方法
通过BindObject
方法对实例化对象中的所有方法或指定方法完成注册
先在/internal/controller/user
中准备一个user
控制类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45package user
import "github.com/gogf/gf/v2/net/ghttp"
type Controller struct{}
func New() *Controller {
return &Controller{}
}
func (c *Controller) AddUser(r *ghttp.Request) {
r.Response.Writeln("添加用户")
}
func (c *Controller) UpdateUser(r *ghttp.Request) {
r.Response.Writeln("更新用户")
}
func (c *Controller) DeleteUser(r *ghttp.Request) {
r.Response.Writeln("删除用户")
}
func (c *Controller) ListUser(r *ghttp.Request) {
r.Response.Writeln("用户列表")
}
func (c *Controller) GetUser(r *ghttp.Request) {
r.Response.Writeln("查询一个用户")
}
func (c *Controller) Post(r *ghttp.Request) {
r.Response.Writeln("添加用户")
}
func (c *Controller) Put(r *ghttp.Request) {
r.Response.Writeln("更新用户")
}
func (c *Controller) Delete(r *ghttp.Request) {
r.Response.Writeln("删除用户")
}
func (c *Controller) Get(r *ghttp.Request) {
r.Response.Writeln("查询一个用户")
}cmd.go
中实例化对象,并绑定该对象的所有公共方法:
1
2
3
4
5
6
7
8
9
10import (
// ...
hellogf/internal/controller/user
)
// ...
usercontroller := user.New()
// 绑定user控制器中所有公共方法
s.BindObject("/user", usercontroller)BindObject
方法提供了第三个参数,即指定对象里的方法名,表示只注册该对象中的该方法
1
s.BindObject("/user", userController, "AddUser, UpdateUser")
绑定对象指定方法
通过BindObjectMethod
方法和上文的BindObject
方法中提供第三个参数效果是类似的
1
2
3
4
5usercontroller := user.New()
// 绑定user控制器中多个方法
s.BindObject("/user", usercontroller, "AddUser,UpdateUser")
// 绑定单个方法
s.BindObjectMethod("/deluser", usercontroller, "DeleteUser")
以RESTFul风格绑定对象方法
通过BindObjectRest
方法对对象中的以Get
、Post
、Delete
等命名的方法做绑定
1
2usercontroller := user.New()
s.BindObjectRest("/user", usercontroller)/user
为路径的路由有四个,但请求方法都不同,而且其他如AddUser
命名的方法没有被注册
分组路由⭐
前面我们都是使用类似BindHandler
方法的方式进行路由注册,而分组路由才是业务项目中主要使用的路由注册方式
我们可以给分组路由指定一个prefix
前缀(也可以直接给定/
前缀,表示注册在根路由下),在该分组下的所有路由注册都将注册在该路由前缀下
官方文档:https://goframe.org/pages/viewpage.action?pageId=1114517
比如我们将实例化后的userController
绑定到以/user
为前缀的路由组里
1
2
3
4
5
6
7
8
9
10
11
12usercontroller := user.New()
s.Group("/user", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(
usercontroller, // 绑定到控制器对象
)
// 可以用GET POST PUT等定义路由
group.GET("/get", func(r *ghttp.Request) {
r.Response.Writeln("/user/get")
})
})
规范路由⭐
规范路由是为了解决一些规范化和自动化管理接口的场景,更适用于团队多人协作使用
规范路由需要使用YAML
配置文件:manifest/config/config.yaml
1
2
3
4server:
address: ":8000"
openapiPath: "/api.json"
swaggerPath: "/swagger"
在定义规范路由时使用固定的格式:它不再像前文中只需要接收*ghttp.Request
类型参数的函数
1
func Handler(ctx context.Context, req *Request) (res *Response, err error)
其中形参和返回值各为两个,并且都是必须的一个都不能少:
参数 | 说明 | 注意事项 |
---|---|---|
ctx context.Context |
上下文 | Server 组件会自动从请求中获取并传递给接口方法 |
req *Request |
请求对象 | 就算没有接收参数也要定义,因为请求结构体中不仅仅包含请求参数的定义,也包含了接口的请求定义。 |
res *Response |
返回对象 | 就算没有返回参数也要定义,因为返回结构体中不仅仅包含返回参数的定义,也可以包含接口返回定义。 |
err error |
错误对象 | Server 通过该参数判断接口执行成功或失败。 |
请求 / 返回结构体
在规范化路由注册中,非常重要的是请求 /
返回结构体的定义,在该结构体不仅仅包含了输入参数的定义,也包含了接口的定义,特别是路由地址、请求方法、接口描述等信息。为保证命名规范化,输入数据结构以XxxReq
方式命名,输出数据结构以XxxRes
方式命名。即便输入或者输出参数为空,也需要定义相应的数据结构,这样的目的一个是便于后续扩展,另一个是便于接口信息的管理
❗
这个Xxx
是控制类的方法名,个人感觉定义结构体类似我们在自定义Spring框架中的注解,比如@PostMapping
之类的(当然这个注解是框架提供的,如果看到后文,其实有点@PostMapping
、@PathVariable
、@RequestParam
等等注解混合使用的感觉)
其写法参照上图,需要将请求 /
返回结构体封装到/api/xxx
目录下的xxx.go
文件中
在请求结构体中一定有g.meta
元数据,通过后面打tag
的方式定义路由地址、请求方法、接口描述等信息,而结构体其他具体写什么内容会在后文中具体展开
在控制类中导入请求 /
返回结构体,比如导入user
(通常可以自动导入)
1
import hellogf/api/user"
然后实现控制器方法,比如实现添加用户的方法,注意请求 /
返回结构体的使用 1
2
3
4
5
6
7
8
9
10
11func (c *Controller) AddUser(ctx context.Context, req *user.AddReq) (res *user.AddRes, err error) {
res = &user.AddRes{
Name: "张三",
Age: 18,
}
// 自定义返回数据
// g.RequestFromCtx(ctx).Response.Writeln("Hello")
return
}
规范路由注册
推荐使用实例化对象的方式来管理所有路由方法,并通过分组路由的Bind
方法执行统一注册
我们知道,在规范化路由方式下,路由地址以及请求方式将由请求结构体在g.Meta
中定义,但我们依然可以通过分组路由定义分组下的所有路由前缀(很像Spring框架中的@RequestMapping
注解)
比如注册user
和hello
控制类的对象(/internal/cmd/cmd.go
)
1
2
3
4
5
6
7
8
9userController := user.New()
helloController := hello.NewHello()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(
userController,
helloController,
)
})
😕 为什么推荐使用规范路由? 从我对Java语言的Spring框架的理解来对官方文档中所描述的特性进行展开阐述:
👍 请求 / 返回结构体很类似Java中注解的使用,这在上文中也提到过; - 规范化API按照结构化编程设计 - 规范化API接口方法参数风格定义
👍
我们通过实例化控制器对象,注册对象中的全部公有方法,另外可以通过分组路由,类似@RequestMapping
类注解添加路由前缀;
- 更加简化的路由注册与维护
👍 返回数据直接是通用返回结果类的形式了,因此不需要再额外定义这个类了; - 统一接口返回数据格式设计
👍 不再需要我们手动整合Swagger组件(需要在配置文件中进行配置,但通常初始化项目的时候已经自动配置); - 保障代码与接口文档同步维护 - 自动生成基于标准OpenAPIv3协议的接口文档 - 自动生成SwaggerUI页面
👍
通过打tag
的方式实现类似Spring
Validation参数校验框架的效果,这个后文会介绍 -
自动的API参数对象化接收与校验
请求与返回
请求输入
基本使用
如何获取请求参数有很多种API,比如获取Query
数据的GetQuery
、Form
数据的GetForm
等等,但这里先仅介绍GetRequest
,这个API可以获取客户端提交的所有参数,按照参数优先级进行覆盖,不区分提交方式。(这些API是*ghttp.Request
对象的,但如果我们使用规范路由,其实也都不常用)
如果想知道其他API的用法,参照官方文档:https://goframe.org/pages/viewpage.action?pageId=1114483
首先,需要知道在Goframe
框架中,有以下几种提交类型:
提交类型 | 描述 |
---|---|
Router |
路由参数。来源于路由规则匹配,比如/article/id={id} |
Query |
Query 参数。URL 中的Query String 参数解析,如:http://127.0.0.1/index?id=1&name=john 中的id=1&name=john |
Form |
表单参数。最常见的提交方式,提交的Content-Type 往往为:application/x-www-form-urlencoded 、multipart/form-data 、multipart/mixed 。 |
Body |
内容参数。从Body 中获取并解析得到的参数,JSON /XML 请求往往使用这种方式提交。 |
Custom |
自定义参数,往往在服务端的中间件、服务函数中通过SetParam/GetParam 方法管理。 |
如果我们使用GetRequest
方法获取请求参数,那么客户端提交什么参数,我们都可以获取到
另外Get
方法是GetRequest
的别名,GetRequest
方法需要传参,用于指定获取哪个参数,比如GetRequest("username")
,而GetRequestMap
不需要传参,用于获取全部请求参数,其别名是GetMap
我们通过下面的例子来了解GetRequest
的使用: 1.
准备hello
的params
方法的请求 / 返回结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package hello
import "github.com/gogf/gf/v2/frame/g"
type ParamsReq struct {
g.Meta `path:"/params/age={age}" method:"all"`
UserName string
Password string
Age int
}
type ParamsRes struct {
}
- 在
hello
控制器中实现Params
方法1
2
3
4
5
6
7
8
9func (c *Hello) Params(ctx context.Context, req *hello.ParamsReq) (res *hello.ParamsRes, err error) {
r := g.RequestFromCtx(ctx)
data := r.GetMap()
r.Response.Writeln(data)
r.Response.Writeln(req)
return
}
要知道GetRequest
这个方法是*ghttp.Request
的API,如果我们使用规范路由,不能直接获取*ghttp.Request
对象了,因此我们需要通过g.RequestFromCtx(ctx)
来获取,然后调GetMap
(或者GetRequestMap
)获取客户端的所有请求参数
对象处理 ⭐
对象转换在请求处理中非常常见。我们推荐将输入和输出定义为struct
结构体对象,以便于结构化的参数输入输出维护
客户端提交的所有参数(上文提到的任何提交类型)都能转换为指定的struct
结构体,并且支持提交参数与struct
属性的映射关系维护
简单来说,客户端无论通过表单还是URL
的?
后面跟参数等等提交请求参数的形式,只要XxxReq
结构体中定义了对应的属性,都能被映射并且维护
参数映射
客户端提交的参数如果需要映射到服务端定义的struct
属性上,可以采用默认的映射关系,这一点非常方便。默认的转换规则如下:
1. struct
中需要匹配的属性必须为公开属性(首字母大写) 2.
参数名称会自动按照不区分大小写且忽略-
、_
、空格
符号的形式与struct
属性进行匹配
3. 如果匹配成功,那么将键值赋值给属性,如果无法匹配,那么忽略该键值
以下是匹配的示例: 1
2
3
4
5
6
7
8
9
10
11
12map键名 struct属性 是否匹配
name Name match
Email Email match
nickname NickName match
NICKNAME NickName match
Nick-Name NickName match
nick_name NickName match
nick name NickName match
NickName Nick_Name match
Nick-name Nick_Name match
nick_name Nick_Name match
nick name Nick_Name match
另外,除了默认规则,我们还可以通过p
标签使请求参数映射为指定的struct
属性,例如:
1
2
3
4
5
6
7
8type ParamsReq struct {
g.Meta `path:"/params" method:"all"`
Id string
UserName string
Password string `p:"mima"`
Age int `p:"nianling"`
}Id
、Username
、Password
、Age
,其中Password
和Age
追加了自定义规则外,四个属性都遵循默认规则的匹配
我们在控制器中实现Params
方法:结果是输出客户端的全部请求参数和对象映射后的请求参数req
1
2
3
4
5
6
7
8
9func (c *Hello) Params(ctx context.Context, req *hello.ParamsReq) (res *hello.ParamsRes, err error) {
r := g.RequestFromCtx(ctx)
data := r.GetMap()
r.Response.Writeln(data)
r.Response.Writeln(req)
return
}
最后在 Postman 中测试接口:
我们通过query
的方式请求id
和sex
两个参数,并且通过表单的方式请求username
、mima
、a_ge
三个参数
通过r.Response.Writeln(data)
输出的内容来看,服务端能够收到所有请求参数(无论什么请求类型)
但通过r.Response.Writeln(req)
输出的内容来看,所有请求参数中除了sex
参数,其他参数都能通过默认规则或者自定义规则实现映射关系
默认值绑定
支持使用d
标签(或者default
标签)为属性绑定默认值,例如:
1
2
3
4
5
6
7
8type ParamsReq struct {
g.Meta `path:"/params" method:"all"`
Id string
UserName string `d:"Anonymous"`
Password string `p:"mima" d:"123456"`
Age int `p:"nianling"`
}
最后使用 Postman 测试接口:
参数校验
在使用中,经常需要验证前端提交过来的数据是否符合规则,比如非空、长度限制、是否为数字等一系列验证。在GoFrame中,基本上都不用手动写验证规则,框架里已经提供了很多内置的验证规则可以用来验证数据,类似Spring Validation
https://goframe.org/pages/viewpage.action?pageId=1114367
指定单个规则 在规则路由中使用
v
标签为每个字段指定校验规则1
2
3
4
5
6type ValidReq struct {
g.Meta `url:"/valid" method:"all"`
UserName string `p:"username" v:"required"`
Password string `p:"password"`
Age int `p:"age"`
}指定校验失败后的错误提示信息 在规则后面使用
#
符号添加错误提示信息1
2
3
4
5
6type ValidReq struct {
g.Meta `url:"/valid" method:"all"`
UserName string `p:"username" v:"required#username不能为空"`
Password string `p:"password"`
Age int `p:"age"`
}指定多个规则 使用
|
符号区分每个校验规则和错误提示信息,注意校验规则和错误提示信息的顺序要一一对应1
2
3
4
5
6type ValidReq struct {
g.Meta `url:"/valid" method:"all"`
UserName string `p:"username" v:"required#username不能为空"`
Password string `p:"password"`
Age int `p:"age" v:"required|integer|min:0#age不能为空|age必须是整数|age不能小于0"`
}
除了校验规则,还有修饰规则,比如ci
可以为某些需要比较值的校验规则(same
、different
、in
等等)指定不区分大小写的规则
1
2
3
4
5
6type ValidReq struct {
g.Meta `url:"/valid" method:"all"`
Account string `v:"required"`
Password string `v:"required|same:Password2"`
Password2 string `v:"required"`
}Password
和Password2
仅大小写不同也不会报错
响应输出
文本数据返回
除了返回文本外,还能返回HTML
模板。但现在基本上都是前后端分离了,大多数场景下返回的是通用返回结果类,因此这里只简单展示如何返回文本数据
1
2
3
4
5
6func (c *Hello) Respons(ctx context.Context, req *hello.ParamsReq) (res *hello.ParamsRes, err error) {
r := g.RequestFromCtx(ctx)
r.Response.Writeln("你好 GoFrame !")
return
}
API数据返回 ⭐
在实际的前后端分离开发中,返回的JSON数据通常为以下结构(通用返回结果类)
1
2
3
4
5{
"code":0, // 自定义编码,用来表示请求成功与失败
"msg":"请求成功", // 提示信息,如果请求出错则为错误信息
"data":{} // 请求返回数据,请求出错时一般为null
}
GoFrame为前后端分离的API开发提供了很好的支持,只需要借助api
模块就可以方便完成类似的返回结构,不需要自行定义
在
/api
中定义请求 / 响应结构体,比如在/api/hello/hello.go
中定义hello
的respons
方法的请求 / 响应结构体如果没有定义路径,那么就默认以方法名做路径1
2
3
4
5
6
7
8
9type ResponsReq struct {
g.Meta `method:"all"`
}
type ResponsRes struct {
UserName string
Password string
List g.Array
}在控制器中定义
Respons
方法,同时实例化数据并返回1
2
3
4
5
6
7
8
9func (c *Hello) Respons(ctx context.Context, req *hello.ResponsReq) (res *hello.ResponsRes, err error) {
res = &hello.ResponsRes{
UserName: "zhangsan",
Password: "123456",
List: g.Array{1, 2, 3, 4},
}
return
}使用 Postman 测试接口
如果有错误,定义错误信息并直接返回 1
2
3
4
5
6
7
8
9
10func (c *Hello) Respons(ctx context.Context, req *hello.ResponsReq) (res *hello.ResponsRes, err error) {
// res = &hello.ResponsRes{
// UserName: "zhangsan",
// Password: "123456",
// List: g.Array{1, 2, 3, 4},
// }
err = gerror.New("服务器开小差了")
return
}
使用 Postman 测试接口
数据库ORM ⭐
准备数据库
本教程将以MySQL为例,使用命令行工具或者任意可视化工具创建名为goframe
的数据库,字符集为utf8
,排序规则为utf8mb4_general_ci
创建完成后,运行以下SQL脚本,创建测试数据表: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114USE `goframe`;
/*Table structure for table `book` */
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` VARCHAR(50) NOT NULL COMMENT '书名',
`author` VARCHAR(30) NOT NULL COMMENT '作者',
`price` DOUBLE NOT NULL COMMENT '价格',
`publish_time` DATE COMMENT '出版时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
/*Data for the table `book` */
INSERT INTO `book`(`id`,`name`,`author`,`price`) VALUES
(1,'MySQL数据库从入门到精通','王飞飞',59.8),
(2,'设计模式','刘伟',45),
(3,'数据库原理及应用','刘亮',33),
(4,'Linux驱动开发入门与实践','郑强',69),
(5,'Linux驱动开发入门与实践','郑强',69),
(6,'Linux驱动开发入门与实践','郑强',69);
/*Table structure for table `dept` */
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`pid` INT(10) UNSIGNED DEFAULT NULL COMMENT '上级部门ID',
`name` VARCHAR(30) DEFAULT NULL COMMENT '部门名称',
`leader` VARCHAR(20) DEFAULT NULL COMMENT '部门领导',
`phone` VARCHAR(11) DEFAULT NULL COMMENT '联系电话',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=108 DEFAULT CHARSET=utf8;
/*Data for the table `dept` */
INSERT INTO `dept`(`id`,`pid`,`name`,`leader`,`phone`) VALUES
(100,0,'哪都通','赵方旭','10000000000'),
(101,100,'华北大区','徐四','10000000001'),
(102,100,'东北大区','高廉','10000000002'),
(103,100,'华东大区','窦乐','10000000003'),
(104,100,'华中大区','任菲','10000000004'),
(105,100,'华南大区',NULL,NULL),
(106,100,'西北大区','华风','10000000005'),
(107,100,'西南大区','郝意','10000000006');
/*Table structure for table `emp` */
DROP TABLE IF EXISTS `emp`;
CREATE TABLE `emp` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`dept_id` INT(10) UNSIGNED NOT NULL COMMENT '所属部门',
`name` VARCHAR(30) NOT NULL COMMENT '姓名',
`gender` TINYINT(1) DEFAULT NULL COMMENT '性别: 0=男 1=女',
`phone` VARCHAR(11) DEFAULT NULL COMMENT '联系电话',
`email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
`avatar` VARCHAR(100) DEFAULT NULL COMMENT '照片',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8;
/*Data for the table `emp` */
INSERT INTO `emp`(`id`,`dept_id`,`name`,`gender`,`phone`,`email`) VALUES
(1,100,'赵方旭',0,'10000000000','zhaofx@nadoutong.com'),
(2,100,'毕游龙',0,'10000000007','biyoulong@nadoutong.com'),
(3,100,'黄伯仁',0,'10000000008','huangboren@nadoutong.com'),
(4,101,'徐四',0,'10000000001','xusi@nadoutong.com'),
(5,101,'徐三',0,'10000000009','xusan@nadoutong.com'),
(6,101,'冯宝宝',1,'10000000010','fengbaobao@nadoutong.com'),
(7,101,'张楚岚',0,'10000000011','zhangchulan@nadoutong.com'),
(8,102,'高廉',0,'10000000002','gaolian@nadoutong.com'),
(9,102,'高二壮',1,'10000000012','gaoerzhuang@nadoutong.com'),
(10,103,'窦乐',0,'10000000003','doule@nadoutong.com'),
(11,103,'肖自在',0,'10000000013','xiaozizai@nadoutong.com'),
(12,104,'任菲',0,'10000000004','renfei@nadoutong.com'),
(13,106,'华风',0,'10000000005','huafeng@nadoutong.com'),
(14,107,'郝意',0,'10000000006','huafeng@nadoutong.com');
DROP TABLE IF EXISTS `hobby`;
CREATE TABLE `hobby` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`emp_id` INT UNSIGNED NOT NULL COMMENT 'EmpID',
`hobby` VARCHAR(50) COMMENT '爱好',
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
INSERT INTO `hobby` (`id`, `emp_id`, `hobby`) VALUES
(1, 6, '埋人'),
(2, 4, '看美女'),
(3, 7, '月下遛鸟');
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`username` VARCHAR(20) NOT NULL COMMENT '用户名',
`nickname` VARCHAR(30) COMMENT '昵称',
`password` VARCHAR(32) COMMENT '密码',
`avatar` VARCHAR(100) COMMENT '头像',
`created_at` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
INSERT INTO
`user` (`id`, `username`, `nickname`, `password`, `avatar`, `created_at`) VALUES
(1, 'libai', '李白', '123456', '', '2023-10-08 16:57:24'),
(2, 'dufu', '杜甫', '123456', '', '2023-10-08 16:57:24'),
(3, 'baijuyi', '白居易', '123456', '', '2023-10-08 16:57:24');
数据库配置
数据库内容完毕后,在配置文件/manifest/config/config.yaml
中添加数据库的配置:
1
2
3
4
5
6
7
8
9database:
type: "mysql"
host: "127.0.0.1"
port: "3306"
user: "root"
pass: "root"
name: "goframe"
timezone: "Asia/Shanghai"
debug: true
各配置项的内容: - type
:数据库类型,MySQL、SQLite、SQL
Server等等 - host
:数据库主机 -
port
:数据库端口 - user
:数据库连接用户名 -
pass
:数据库连接密码 -
name
:需要连接的数据库名 -
timezone
:时区,设置为Asis/Shanghai
或者Local
-
debug
:是否开启调试,学习开发阶段可以打开,用于查看数据库操作相关信息输出
其他配置项参考官方文档:https://goframe.org/pages/viewpage.action?pageId=1114245
驱动添加与导入
在
main.go
中进行MySQL驱动的初始化导入1
2
3import (
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
)在
go.mod
中添加驱动库与版本1
2
3
4require (
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.5.3
github.com/gogf/gf/v2 v2.5.3
)在命令行中进行依赖更新
1
go mod tidy
最后等待依赖的下载更新完成
数据库基本操作
在github.com/gogf/gf/v2/frame/g
包里的Model
函数将返回gdb.Model
对象,提供了一系列用于操作数据库的API。Model
函数接收一个参数:数据表名
1
md := g.Model("book")
基础查询
查询单个数据One
1 | md := g.Model("book") |
该结果将返回数据表中的第一条数据,查询成功返回的数据为map[string]*gvar.Var
类型,所以可以访问该数据的每一个字段
1
req.Response.WriteJson(book["name"]) // 返回结果中"name"字段
*gvar.Var
提供的API对字符进行类型转换(*gvar.Var
是框架提供的泛型):
1 | book["name"].String() // 转为string类型 |
另外,可以查询除了指定字段的其他字段 1
2
3book, err := md.FieldsEx("name, price").One() //查询除了name price两个字段的其他字段
// 也可以写为
book, err := md.FieldsEx("name", "price").One()
查询所有数据All
1 | md := g.Model("book") |
该方法以切片返回数据表中的所有数据,我们可以通过range
关键字来循环操作每一条数据
1
2
3for _, v := range bookList {
req.Response.Writeln(v)
}
查询单个数据的指定字段值Value
1 | md := g.Model("book") |
查询所有数据的指定字段值Array
1 | md := g.Model("book") |
聚合函数
注意,一个Model
只能使用一次查询 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18min, err := g.Model("book").Min("price")
max, err := g.Model("book").Max("price")
avg, err := g.Model("book").Avg("price")
count, err := g.Model("book").Count()
if err == nil {
r.Response.WriteJson(g.Map{
"min": min,
"max": max,
"avg": avg,
"count": count,
})
}
// 生成的SQL语句
// SELECT MIN(`price`) FROM `book` LIMIT 1
// SELECT MAX(`price`) FROM `book` LIMIT 1
// SELECT AVG(`price`) FROM `book` LIMIT 1
// SELECT COUNT(1) FROM `book`
条件查询
查询数据时可以通过Where
方法指定条件,如果调用了多个Where
,那么多个条件之间会用AND
连接
- 等于
默认情况下条件会用=
连接 1
2
3
4md := g.Model("book")
book, err := md.Where("id", 3).All()
// 生成的SQL语句
// SELECT * FROM `book` WHERE `id`=3
- 不等 如果是不等关系,则需要在字段后面加上不等符号
1
2
3
4md := g.Model("book")
book, err := md.Where("id>", 3).All()
// 生成的SQL语句
// SELECT * FROM `book` WHERE id>3
另一种写法是加上?
占位符,效果是一样的 1
2md := g.Model("book")
book, err := md.Where("id>?", 3).All()
- 多个条件叠加
当存在多个条件时可以多个
Where
函数进行链式调用,条件之间会用AND
连接1
2
3
4md := g.Model("book")
books, err := md.Where("id>=?", 2).Where("id<?", 6).All()
// 生成的SQL语句
// SELECT * FROM `book` WHERE (id>=2) AND (id<6)
以下是Where
系列方法:
方法 | 生成的SQL条件表达式 |
---|---|
WhereLT(column, value) |
column < value |
WhereLTE(column, value) |
column <= value |
WhereGT(column, value) |
column > value |
WhereGTE(column, value) |
column >= value |
WhereBetween(column, min, max) |
column BETWEEN min AND max |
WhereNotBetween(column, min, max) |
column NOT BETWEEN min AND max |
WhereLike(column, like) |
column LIKE like |
WhereIn(column, in) |
column IN (in) |
WhereNotIn(column, in) |
column NOT IN (in) |
WhereNot(column, value) |
column != value |
WhereNull(columns1, columns2... ) |
columns1 IS NULL AND columns2 IS NULL... |
WhereNotNull(columns1, columns2... ) |
columns1 IS NOT NULL AND columns2 IS NOT NULL ... |
使用示例: 1
2
3
4
5md := g.Model("book")
books, err := md.WhereIn("id", g.Array{1, 2, 3}).WhereLike("name", "%数据%").All()
// 生成的SQL语句
// SELECT * FROM `book` WHERE (`id` IN (1,2,3)) AND (`name` LIKE '%数据%')
以上方法如果链式调用会生成以AND
连接的条件,如果需要生成以OR
连接的条件,则需要用到以下方法:
|方法|生成的SQL条件表达式| |:--:|:--:|
|WhereOrLT(column, value)
|OR (column < value)
|
|WhereOrLTE(column, value)
|OR (column <= value)
|
|WhereOrGT(column, value)
|OR (column > value)
|
|WhereOrGTE(column, value)
|OR (column >= value)
|
|WhereOrBetween(column, min, max)
|OR (column BETWEEN min AND max)
|
|WhereOrNotBetween(column, min, max)
|OR (column NOT BETWEEN min AND max)
|
|WhereOrLike(column, like)
|OR (column LIKE like)
|
|WhereOrIn(column, in)
|OR (column IN (in))
|
|WhereOrNotIn(column, in)
|OR (column NOT IN (in))
|
|WhereOrNot(column, value)
|OR (column != value)
|
|WhereOrNull(columns1, columns2... )
|OR (columns1 IS NULL AND columns2 IS NULL...)
|
|WhereOrNotNull(columns1, columns2... )
|OR (columns1 IS NOT NULL AND columns2 IS NOT NULL ...)
|
|WhereOr(column, value)
|OR (column = value)
|
使用示例: 1
2
3
4
5md := g.Model("book")
books, err := md.WhereIn("id", g.Array{1, 2, 3}).WhereOrLike("name", "%数据%").All()
// 生成的SQL语句
// SELECT * FROM `book` WHERE (`id` IN (1,2,3)) OR (`name` LIKE '%数据%')
排序与分组
按字段排序: 1
2
3
4
5
6
7
8
9
10
11md := g.Model("book")
books, err := md.Order("price", "DESC").All()
// 多字段排序
books, err := md.Order("price", "DESC").Order("id", "ASC").All()
// 生成的SQL语句
// SELECT * FROM `book` ORDER BY `price` DESC,`id` ASC
// 排序封装方法
books, err := md.OrderDesc("price").OrderAsc("id").All()
按字段分组: 1
2
3
4md := g.Model("book")
books, err := md.Group("name").Count() // 4
// 生成的SQL语句
// SELECT COUNT(1) FROM (SELECT COUNT(1) FROM `book` GROUP BY `name`)
分页
使用Limit
方法用来限制查询条数或者自定义起始位置与数据限制
1
2
3
4
5
6
7
8
9
10md := g.Model("book")
// 限制条数
books, err := md.Limit(5).All()
// 生成的SQL语句
// SELECT * FROM `book` LIMIT 5
// 指定起始位置与限制条数:此处指第三条数据开始后面五条数据(本身不算)
books, err := md.Limit(3, 5).All()
// 生成的SQL语句
// SELECT * FROM `book` LIMIT 3,5
GoFrame
提供了Page
方法可以很方便实现分页查询,只需提供两个参数:页数和每页数据数量
1
2
3
4md := g.Model("book")
books, err := md.Page(2, 3).All()
// 生成的SQL语句
// SELECT * FROM `book` LIMIT 3,3
将查询结果转为结构体
在上文中提到过One
和All
方法返回的数据是Map
或者Map
切片,在实际使用中查询到的数据可能需要转换为特定的数据结构方便使用。
Scan
方法可以将查询到的数据转为自定义结构体或结构体数组,下面是推荐写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18type Book struct {
Id uint
Name string
Author string
Price float64
PublishTime *gtime.Time
}
var book *Book
md := g.Model("book")
error := md.Where("id", 10).Scan(&book)
if error == nil {
r.Response.WriteJson(&book)
} else {
r.Response.WriteJson(error.Error())
}
Scan
会将数据库字段下划线命名对应到结构体中相应的驼峰命名上,如果对应不上,则该成员为nil
或者零值。如果结构体中成员名称与数据表中字段不对应,可以用orm:
标签来指定对应字段,比如:
1
2
3
4
5
6
7
8
9
10
11
12type Book struct {
BookId uint `orm:"id" `
BookName string `orm:"name"`
BookAuthor string `orm:"author"`
BookPrice float64 `orm:"price"`
PubTime *gtime.Time `orm:"publish_time"`
}
var book *Book
md := g.Model("book")
error := md.Scan(&book)
Scan
方法可以如上查询单独结构体,也可以查询一个结构体数组,只需要将结构体指针改为结构体切片传入即可
1
2
3
4
5
6
7
8
9
10
11
12type Book struct {
Id uint
Name string
Author string
Price float64
PublishTime *gtime.Time
}
var book []Book
md := g.Model("book")
error := md.Scan(&book)Book
组成的结构体数组,存放多条数据。
查询结果为空判断
All
1
2
3
4
5
6
7
8
9md := g.Model("book")
books, _ := md.All()
if len(books) == 0 {
r.Response.Writeln("结果为空")
}
// 或者
if books.IsEmpty() {
r.Response.Writeln("结果为空")
}One
1
2
3
4
5
6
7
8
9md := g.Model("book")
book, _ := md.Where("id", 100).One()
if len(book) == 0 {
r.Response.Writeln("结果为空")
}
// 或者
if book.IsEmpty() {
r.Response.Writeln("结果为空")
}Value
1
2
3
4
5md := g.Model("book")
name, _ := md.Where("id", 10).Value("name")
if name.IsEmpty() {
r.Response.Writeln("结果为空")
}Array
1
2
3
4
5md := g.Model("book")
names, _ := md.WhereLT("id", 10).Array("name")
if len(names) == 0{
r.Response.Writeln("结果为空")
}Scan
结构体对象1
2
3
4
5
6
7var book *Book
md := g.Model("book")
md.Scan(&book)
if book == nil {
r.Response.Writeln("结果为空")
}
md.Save(data)Scan
结构体数组1
2
3
4
5
6var books []Book
md := g.Model("book")
md.Scan(&books)
if len(books) == 0 {
r.Response.Writeln("结果为空")
}
插入数据
有三个方法可以插入数据,分别是Insert
、Replace
、Save
,区别在于当主键字段(或唯一索引)冲突时,处理的方式不同:
|方法|主键(或唯一索引)冲突时| |:--:|:--:|
|Insert
|报错:主键(或唯一索引)冲突|
|Replace
|用新数据替换已存在的同主键(或唯一索引)的数据|
|Save
|用新数据更新已存在的同主键(或唯一索引)的数据|
写入单条数据
1 | md := g.Model("book") |
result
返回结果将会返回影响行数,另外以上方法也可配合Data
方法使用:
1
2
3
4
5
6// Insert
result, err := md.Data(data).Insert()
// Replace
result, err := md.Data(data).Replace()
// Save
result, err := md.Data(data).Save()
除了使用Map
类型之外,还可以用结构体。结构体成员名称与数据表字段名称不对应时,用orm
标签指定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18type Book struct {
Id uint
Name string
Author string
Price float64
PubTime *gtime.Time `orm:"publish_time"`
}
md := g.Model("book")
data := Book{
Id: 8,
Name: "Linux驱动开发入门与实践",
Author: "郑强",
Price: 69.3,
PubTime: gtime.New("2023-10-10"),
}
result, err := md.Data(data).Save()
批量写入数据
1 | data := g.List{ |
如果使用的是结构体,将g.List
改为g.Array
或者g.Slice
InsertAndGetId
插入数据并返回ID 1
2
3
4
5
6
7
8data := g.Map{
"name": "Linux驱动开发入门与实践",
"author": "郑强",
"price": 69.3,
"publish_time": gtime.New("2023-10-10"),
}
result, err := md.Data(data).InsertAndGetId()
gdb.Raw
对于某些字段,可能需要调用SQL里面的操作来获得结果,例如,publish_time
字段可以用SQL中的CURRENT_DATE()
来获取当前日期,这时就需要用到gdb.Raw
:
1
2
3
4
5
6
7
8data := g.Map{
"name": "Linux驱动开发入门与实践",
"author": "郑强",
"price": 69.3,
"publish_time": gdb.Raw("CURRENT_DATE()"),
}
result, err := md.Data(data).InsertAndGetId()
更新数据
使用Update
方法进行更新数据操作 1
2
3
4
5
6data := g.Map{
"author": "郑强强",
"price": 69.333,
}
result, err := md.Where("author", "郑强").Update(data)
也可以配合Data
使用 1
2
3
4
5
6data := g.Map{
"author": "郑强强",
"price": 69.333,
}
result, err := md.Where("author", "郑强").Data(data).Update()
使用Increment
或Decrement
用来给指定字段增加或减少指定值
1
2result, err := md.WhereBetween("id", 7, 10).Increment("price", 2.5)
result, err := md.WhereBetween("id", 7, 10).Decrement("price", 1.5)
删除数据
使用Delete
方法进行删除数据操作 1
result, err := md.WhereGT("id", 10).Delete()
时间维护与逻辑删除
在实际应用中,数据表通常会有三个时间字段:创建时间、更新时间、删除时间。GoFrame
支持这三个时间字段的自动填充,这三个字段支持的类型为DATE
、DATETIME
、TIMESTAMP
比如我们在book
表中添加created_at
、updated_at
、deleted_at
三个字段
我们使用Insert
方法添加数据,注意我们并没有给定创建时间和更新时间的值
1
2
3
4
5
6
7
8data := Book{
Id: 11,
Name: "Linux驱动开发入门与实践",
Author: "郑强",
Price: 69.3,
PubTime: gtime.New("2023-10-10"),
}
result, err := md.Data(data).Insert()
得到的结果是:
我们发现 GoFrame 框架为我们自动生成了创建时间和更新时间
Update
方法也是同理
删除时间涉及到逻辑删除的概念,我们使用Delete
方法删除数据:
1
result, err := md.Where("id", 10).Data(data).Delete()
得到的结果是:
只要数据的deleted_at
字段非空,那么就表示该数据被逻辑删除了,如果这时使用比如All
方法查询数据是查不到该数据的。因为查询数据时,会自动加上WHERE deleted_at IS NULL
的条件,过滤已被逻辑删除的数据
如果还是要查询所有数据,需要使用Unscoped
方法
1
ls, _ := md.Unscoped().All()
事务
1 | g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error { |
原生SQL
由Model
提供的方法能组合出绝大多数使用场景所需要的数据操作,但如果需要的操作过于复杂,可能就没法通过已有的方法组合出来,就需要使用写SQL来实现
首先获取ORM对象 1
2
3
4
5// 获取默认配置的数据库对象
db := g.DB()
// 或获取配置分组名称为"user-center"的数据库对象
db := g.DB("user-center")
查询数据列表:
1
2
3list, err := db.GetAll(ctx, "select * from user limit 2")
list, err := db.GetAll(ctx, "select * from user where age > ? and name like ?", g.Slice{18, "%john%"})
list, err := db.GetAll(ctx, "select * from user where status= ?", g.Slice{1})查询单条数据
1
2
3one, err := db.GetOne(ctx, "select * from user where uid=1000")
one, err := db.GetOne(ctx, "select * from user where uid=?", 1000)
one, err := db.GetOne(ctx, "SELECT * FROM `book` WHERE `id` > ? AND `id` < ?", g.Array{3, 7})增删改数据
1
2
3sql := "INSERT INTO `book` (`name`, `author`, `price`) VALUES (?, ?, ?)"
data := g.Array{"Go语言从入门到精通", "Go语言研讨组", 99.98}
result, err := db.Exec(ctx, sql, data)
1 | sql := "DELETE FROM `book` WHERE `id` = ?" |
1 | sql := "UPDATE `book` SET author = ? WHERE id = ?" |
但是注意,当我们使用原生SQL语句增删改数据时,上文中的三个时间字段(创建时间、更新时间、删除时间)不会被自动填充
DAO自动生成与使用
数据库相关的操作与数据结构放在dao
与model
中,在GoFrame中,dao
与model
的内容可以自动生成。生成步骤如下:
1. 在hack/config.yaml
中配置dao
1
2
3
4
5gfcli:
gen:
dao:
link: "mysql:root:2002@tcp(localhost:3306)/goframe?loc=Local&parseTime=True"
tables: "book,user,emp,dept,hobby"link
表示数据库连接的URL;tables
表示需要生成的dao
和model
的数据表,多个表用逗号隔开
- 在命令行中执行该命令生成
dao
和model
的代码1
gf gen dao
生成成功后,使用各个表对应的Model
对象时,不再用g.Model
获取,而是使用下面的方式:
1
2md := dao.Book.Ctx(ctx)
books, err := md.All()Model
对象可以通过下面的方式多次叠加方法
1
2
3
4
5
6
7md := dao.Book.Ctx(ctx)
md = md.WhereGT("id", 3)
md = md.WhereLT("id", 6)
books, err := md.All()
// 以上代码相当于
// books, err := dao.Book.Ctx(ctx).WhereGT("id", 3).WhereLT("id", 6).All()
字段过滤
使用结构体数据更新数据的时候,有些字段可能不需要更新,因此对应的字段就不进行赋值,例如以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14type Book struct {
Id uint
Name string
Author string
Price float64
PubTime *gtime.Time `orm:"publish_time"`
}
data := Book{
Name: "Linux驱动开发入门与实践",
PubTime: gtime.New("2023-10-11"),
}
_, err = dao.Book.Ctx(ctx).Where("id", 13).Data(data).Update()id
、author
、price
也会被对应类型的零值更新,分别被更新为0
、""
、nil
如果要解决这样的问题,有以下几种解决方案: -
用Fields
指定需要更新的字段 1
dao.Book.Ctx(ctx).Fields("name", "publish_time").Where("id", 13).Data(data).Update()
用
FieldsEx
排除不需要更新的字段1
dao.Book.Ctx(ctx).FieldsEx("id,author,price").Where("id", 13).Data(data).Update()
用
OmitEmpty
过滤空值如果用1
2
3
4
5
6data := Book{
Name: "Linux驱动开发入门与实践",
Price: 0,
PubTime: nil,
}
dao.Book.Ctx(ctx).Where("id", 13).OmitEmpty().Data(data).Update()OmitEmpty
方法,即使数据中有零值,也无法更新对应字段的值用
do
对象进行字段过滤 使用gf gen dao
会为每个表生成对应的do
对象,使用do
对象作为参数传递,将会自动过滤空值如果使用这种方法,非1
2
3
4
5
6data := do.Book{
Name: "Linux驱动开发入门与实践",
Price: 0,
PublishTime: nil,
}
dao.Book.Ctx(ctx).Where("id", 13).Data(data).Update()nil
的零值都可以更新
do
对象也可以用于传递查询条件,也会自动过滤空值
1
2
3
4
5
6
7
8
9where := do.Book{
Author: "郑强",
Id: 13,
PublishTime: nil,
}
books, err := dao.Book.Ctx(ctx).Where(where).All()
// 相当于
books, err := dao.Book.Ctx(ctx).Where("id", 13).Where("author", "郑强").All()
关联查询
多表数据联查时可以用连接,但是数据量大时连接效率不高,GoFrame中提供了模型关联查询,可以简化一些多表联查操作
以dept
、emp
、hobby
三个表为例,每个部门可以有多个员工,每个员工只有一个部门,每个员工对应一条爱好
- 查询所有员工,并关联查询出其所在部门
在
internal/model/entity/emp.go
中修改entity.Emp
,加入关联信息注意,在1
2
3
4
5
6
7
8
9
10
11type Emp struct {
Id uint `json:"id" ` // ID
DeptId uint `json:"dept_id" ` // 所属部门
Name string `json:"name" ` // 姓名
Gender int `json:"gender" ` // 性别: 0=男 1=女
Phone string `json:"phone" ` // 联系电话
Email string `json:"email" ` // 邮箱
Avatar string `json:"avatar" ` // 照片
Dept *Dept `json:"dept" orm:"with:id=dept_id"`
}with:id=dept_id
中前面的字段id
必须是Emp
表中的字段id
,而后面的字段dept_id
必须是Dept
被关联表中的字段dept_id
使用
With
方法指定关联模型查询1
2var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).With(entity.Dept{}).Scan(&emps)
- 查询所有员工,并关联查询出其所在部门和爱好
在
internal/model/entity/emp.go
中修改entity.Emp
,加入关联信息1
2
3
4
5
6
7
8
9
10
11
12type Emp struct {
Id uint `json:"id" ` // ID
DeptId uint `json:"dept_id" ` // 所属部门
Name string `json:"name" ` // 姓名
Gender int `json:"gender" ` // 性别: 0=男 1=女
Phone string `json:"phone" ` // 联系电话
Email string `json:"email" ` // 邮箱
Avatar string `json:"avatar" ` // 照片
Dept *Dept `json:"dept" orm:"with:id=dept_id" `
Hobby *Hobby `json:"hobby" orm:"with:emp_id=id"`
}使用
With
指定需要关联的内容也可以使用1
2var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).With(entity.Dept{}, entity.Hobby{}).Where("dept_id", 101).Scan(&emps)WithAll
关联所有1
2var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).WithAll().Where("dept_id", 101).Scan(&emps)
- 查询部门,关联查询每个部门的员工
在
internal/model/entity/dept.go
修改entity.Dept
,加入关联信息1
2
3
4
5
6
7
8
9type Dept struct {
Id uint `json:"id" ` // ID
Pid uint `json:"pid" ` // 上级部门ID
Name string `json:"name" ` // 部门名称
Leader string `json:"leader" ` // 部门领导
Phone string `json:"phone" ` // 联系电话
Emps []*Emp `json:"emps" orm:"with:dept_id=id"`
}使用
With
指定需要关联的内容1
2var depts []*entity.Dept
err = dao.Dept.Ctx(ctx).With(entity.Emp{}).Scan(&depts)
如果不想查询关联表(或被关联表的)所有字段,可以自己重新定义结构体,只需要保留需要查询的字段(用于关联的字段必须存在)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20type MyDept struct {
g.Meta `orm:"table:dept"`
Id uint `json:"id" ` // ID
Name string `json:"name" ` // 部门名称
Leader string `json:"leader" ` // 部门领导
Phone string `json:"phone" ` // 联系电话
}
type MyEmp struct {
g.Meta `orm:"table:emp"`
Id uint `json:"id" ` // ID
DeptId uint `json:"dept_id" ` // 所属部门
Name string `json:"name" ` // 姓名
Phone string `json:"phone" ` // 联系电话
Dept *MyDept `json:"dept" orm:"with:id=dept_id" `
}
var emps []*MyEmp
err = dao.Emp.Ctx(ctx).With(MyDept{}).Scan(&emps)g.Meta
和orm
标签指定对应的数据表
业务层
业务层的接口定义层,具体的接口实现在logic
中进行注入,类似Spring项目中的service
目录
logic
业务封装,用于业务逻辑封装管理,特定的业务逻辑实现和封装。往往是项目中最复杂的部分,类似Spring项目中的serviceImpl
目录
数据模型与业务模型
因此,综合上文提到的全部内容,所有数据模型和业务模型的关系可以总结为下图
- 数据模型
数据模型又叫做实体模型,主要是来自于底层持久化数据库的数据结构,例如:
MySQL
、Redis
等等。这部分数据模型的代码位于/internal/model/entity
目录下。开发者应当在数据库设计并建立完成后,使用gf gen dao
生成数据模型的代码
- 业务模型 业务模型主要包括:接口输入/输出模型与业务输入/输出模型
接口输入/输出模型用于系统/服务间的接口交互,定义在api
接口层中,供工程项目所有的层级调用,例如controller
,
logic
,
model
中均可以调用api
层的输入输出模型,但是api
层仅用于与外部服务的接口交互,该模型中不能调用或者引用内部的模型如model
模型。在GoFrame框架规范中,这部分输出输出模型名称以XxxReq
和XxxRes
格式命名。
业务输入/输出模型用于服务内部模块/组件之间的方法调用交互,特别是controller->service
或者service->service
之间的调用。这部分模型定义在model
模型层中。在GoFrame框架规范中,这部分输入输出模型名称通常以XxxInput
和XxxOutput
格式命名。
还有一种特殊的业务模型DO
,介于业务模型与数据模型之间,主要用于结合框架强大的ORM
组件大大简便DAO
数据访问操作。
业务接口
使用业务接口需要按照以下四条步骤: 1. 定义接口,并给定形参和返回值 2. 定义接口变量,用于存储接口实现的实例 3. 定义获取接口实例的函数,用于返回接口变量存储的接口实例 4. 定义接口实现的注册方法,用于将接口实现的实例注册到接口变量中
以book
类为例,在internal/service
中创建book.go
以实现book
逻辑层的功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31package service
import (
"context"
"hellogf/internal/model/do"
"hellogf/internal/model/entity"
)
// 1. 定义接口
type IBook interface {
GetList(ctx context.Context) (books []entity.Book, err error)
Add(ctx context.Context, book do.Book) (err error)
Edit(ctx context.Context, book do.Book) (err error)
Del(ctx context.Context) (err error)
}
// 2. 定义接口变量
var localBook IBook
// 3. 定义获取接口实例的函数
func Book() IBook {
if localBook == nil {
panic("implement not found for interface IBook, forgot register?")
}
return localBook
}
// 4. 定义接口实现的注册方法
func RegisterBook(i IBook) {
localBook = i
}
个人认为可以在VSCode中注册一个代码片段,方便使用,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37"service": {
"prefix": "XxxService",
"body": [
"package service",
"",
"import (",
"\t\"context\"",
"",
")",
"",
"// 1. 定义接口",
"type IXxx$0 interface {",
"\tGetXxxList(ctx context.Context) (xxxList []entity.Xxx, err error)",
"\tAddXxx(ctx context.Context, xxx do.Xxx) (err error)",
"\tEditXxx(ctx context.Context, xxx do.Xxx) (err error)",
"\tDeleteXxx(ctx context.Context) (err error)",
"}",
"",
"// 2. 定义接口变量",
"var localXxx IXxx",
"",
"// 3. 定义获取接口实例的函数",
"func Xxx() IXxx {",
"\tif localXxx == nil {",
"\t\tpanic(\"implement not found for interface IBook, forgot register?\")",
"\t}",
"\treturn localXxx",
"}",
"",
"// 4. 定义接口实现的注册方法",
"func RegisterXxx(i IXxx) {",
"\tlocalXxx = i",
"}",
"",
],
"description": "业务接口代码片段"
},
业务封装
业务封装用于实现业务接口的全部方法
通常,我们在internal/logic
中还需要创建目录,以book
为例,就创建book
目录,并在该目录下再创建book.go
用于实现接口方法
定义方法结构体
1
type sBook struct{}
将方法结构体传给注册方法
1
2
3func init() {
service.RegisterBook(&sBook{})
}鼠标悬停并点击
&sBook{}
,会出现“快速修复”功能,点击后自动生成接口中每个方法的实现 以GetList
方法为例,实现该方法的具体功能1
2
3
4
5// GetList implements service.IBook.
func (*sBook) GetList(ctx context.Context) (books []entity.Book, err error) {
err = dao.Book.Ctx(ctx).Scan(&books)
return
}
以下是book
业务封装的完整代码 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36package book
import (
"context"
"hellogf/internal/dao"
"hellogf/internal/model/do"
"hellogf/internal/model/entity"
"hellogf/internal/service"
)
func init() {
service.RegisterBook(&sBook{})
}
type sBook struct{}
// Add implements service.IBook.
func (*sBook) Add(ctx context.Context, book do.Book) (err error) {
panic("unimplemented")
}
// Del implements service.IBook.
func (*sBook) Del(ctx context.Context) (err error) {
panic("unimplemented")
}
// Edit implements service.IBook.
func (*sBook) Edit(ctx context.Context, book do.Book) (err error) {
panic("unimplemented")
}
// GetList implements service.IBook.
func (*sBook) GetList(ctx context.Context) (books []entity.Book, err error) {
err = dao.Book.Ctx(ctx).Scan(&books)
return
}
但此时还需要在internal/logic
中创建logic.go
用于导入业务封装代码的目录
1
2
3
4
5package logic
import (
_ "hellogf/internal/logic/book"
)book
为例
最后在main.go
中导入logic
目录
1
2
3
4import (
_ "hellogf/internal/logic"
...
)
文件上传下载
文件上传
单文件上传
准备前端页面的表单结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传文件</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="ufile"> <br>
<input type="file" name="ufiles" multiple> <br>
<input type="submit" value="上传">
</form>
</body>
</html>在控制类中实现单文件上传的功能代码
注意,1
2
3
4
5
6
7
8
9
10func (c *Controller) Upload(req *ghttp.Request) {
file := req.GetUploadFile("ufile")
if file != nil {
file.Filename = "20231001.png" // 可以根据需要给文件重命名
name, err := file.Save("./upload")
if err == nil {
req.Response.Writeln(name)
}
}
}GetUploadFile
的方法参数需要和表单中file
输入框的name
属性名保持一致
Save
方法用于指定文件的存放路径
多文件上传
1 | func (c *Controller) Upload(req *ghttp.Request) { |
如果使用用api
规范路由,还可以用如下方式获取上传文件:
1
2
3
4
5type UploadReq struct {
g.Meta `path:"/upload" method:"post"`
Ufile ghttp.UploadFile `json:"ufile"`
UFiles ghttp.UploadFiles `json:"ufiles"`
}
1 | type UploadRes struct { |
使用这种方式,如果文件允许为空, 则可能会发生转换错误。
文件下载
展示
ServeFile
向客户端返回一个文件内容,如果是文本或者图片,将会直接展示,不能直接在浏览器中展示的将进行下载
1
2
3func (c *Controller) Download(req *ghttp.Request) {
req.Response.ServeFile("upload/1.png")
}ServeFileDownload
直接引导客户端进行下载,并且可以给下载文件重命名
1
2
3func (c *Controller) Download(req *ghttp.Request) {
req.Response.ServeFileDownload("upload/1.png", "download.png")
}
上传限制
如果需要限制单次上传文件大小,可以用clientMaxBodySize
配置(config.yaml
)。如果完全不需要限制,直接设为0即可
1
2server:
clientMaxBodySize: "0"
Cookie/Session
Cookie
Cookie是保存在浏览器的一些数据,在请求的时候会放在请求头当中一同发送,通常用来保存sessionid
、token
等一些数据。
1
2
3
4
5
6
7
8
9func (c *Controller) Cookie(req *ghttp.Request) {
req.Cookie.Set("id", "kslfjojklcjkldjfsie")
req.Cookie.Set("user_name", "诸葛青")
name := req.Cookie.Get("user_name")
req.Response.Writeln("name from cookie: " + name.String())
req.Cookie.Remove("id")
}
Session
Session机制用于判断请求由哪一用户发起,Session数据保存在服务器。
以前常用于保存登录数据,进行登录验证,不过现在只是有些比较小的,前后端不分离的项目还在使用。
1
2
3
4
5
6
7
8
9
10func (c *Controller) Session(req *ghttp.Request) {
op := req.GetQuery("op").String()
if op == "set" {
req.Session.Set("user", g.Map{"name": "张三", "id": 18})
} else if op == "get" {
req.Response.Writeln(req.Session.Get("user"))
} else if op == "rm" {
req.Session.Remove("user")
}
}
登录验证
前后端分离的项目更常用的登录验证是JWT(JSON Web Token)
。GoFrame中没有提供相关生成与验证,需要添加第三方库,例如golang-jwt
添加库
1
go get -u github.com/golang-jwt/jwt/v5
导入库
1
import "github.com/golang-jwt/jwt/v5"
生成token