Gorm 预加载及输出处理(二)- 查询输出处理
上一篇《Gorm 预加载及输出处理(一)- 预加载应用》中留下的三个问题:
- 如何自定义输出结构,只输出指定字段?
- 如何自定义字段名,并去掉空值字段?
- 如何自定义时间格式?
这一篇先解决前两个问题。
模型结构体中指针类型的应用
先来看一个上一篇中埋下的坑,回顾下 User 模型的定义:
// 用户模型
type User struct {
gorm.Model
Username string `gorm:"type:varchar(20);not null;unique"`
Email string `gorm:"type:varchar(64);not null;unique"`
Role string `gorm:"type:varchar(32);not null"`
Active uint8 `gorm:"type:tinyint unsigned;default:1"`
Profile Profile `gorm:"foreignkey:UserID;association_autoupdate:false"`
}
其中 Active 字段类型为 uint8 类型,表示该用户是否处于激活状态,0 为未激活,1 为已激活,默认值为 1,看起来好像没什么问题,如果要创建一个默认未激活的用户,自然是指定 Active 的值为 0,然后调用 Create 方法即可。但是,你会发现数据库中写入的仍然是 1,一起来看下 Gorm 使用的 sql 语句:
INSERT INTO `user` (`created_at`,`updated_at`,`deleted_at`,`username`,`email`,`role`)
VALUES ('2020-03-15 12:41:14','2020-03-15 12:41:14',NULL,'test14','aaa@bbb.com','admin')
根本就没有往 active 列中插入数据,然后就使用了默认值 1。这是 Gorm 的写入机制引起的,Gorm 不会将零值写入数据库中,部分零值列举如下:
false // bool
0 // integer
0.0 // float
"" // string
nil // pointer, function, interface, slice, channel, map
解决此问题也很简单,将字段定义为对应的指针类型,赋值时也传指针即可,只要传的值不为 nil,即可正常写入数据库。
现调整 User 模型定义如下:
type User struct {
...
Active *uint8 `gorm:"type:tinyint unsigned;default:1"`
...
}
到这里,应该已经清楚 Gorm 模型字段定义中指针类型的应用场景了,即任何需要保存零值的字段,都应定义为指针类型。利用该特性,顺带把上一篇中直接查询 User 输出空值 Profile 结构体的问题一并解决掉。只要将 User 模型中 Profile 字段的类型修改为 Profile 的指针类型即可:
// 用户模型
type User struct {
...
Profile *Profile `gorm:"foreignkey:UserID;association_autoupdate:false"`
}
对应的,在创建 User 的时候,Profile 字段接收的也要是指针类型。这样处理以后,当直接查询 User 而不关联查询 Profile 时,User 中 Profile 字段将为 nil,而不是之前讨厌的空值结构体,清爽了很多不是吗。
自定义输出
Gorm 默认会查询模型的所有字段并按模型定义的结构返回数据,在实际应用中,往往并不需要输出全部字段,这就需要对输出字段进行过滤,通常有两种方式:
- 在查询时指定查询字段;
- 默认查询所有字段,序列化时对字段进行过滤;
第一种方式非常直观简单,要什么,查什么,输出什么,在输出比较固定的场景中非常实用。其缺点也很明显,就是灵活性不高,如果多个接口查一张表,但每个接口所需要的字段又不一样,那么就得为每个接口写一个独立的查询来实现这个需求,这显然不符合“少即是多”、“高复用”的编程思想。
第二种方式在 Model层(查询阶段)不做过滤或只做基础过滤,通过接口对 Service层(逻辑层)提供一份较为完整的数据,Service 层将数据按需映射到自定义输出结构体上然后序列化输出。这样,当需要反复修改输出结构时,Model 层几乎不用做任何改动,只需 Service 层调整输出结构并序列化即可,可最大限度将逻辑和源数据分离,便于维护。
下面通过实际应用来介绍如何自定义输出结构并序列化。
场景
用户列表,输出所有用户,并且用户数据只包含 id,username,role 字段;
用户详情,输出当前用户,除上述数据,还应包含 Profile 中的 Nickname,Phone 字段;
自定义输出结构体
这一步只要按需求创建对应结构体即可,直接上代码:
// 自定义用户输出结构
type CustomUser struct {
ID uint
Username string
Role string
Profile *CustomProfile
}
// 自定义用户信息输出结构
type CustomProfile struct {
Nickname string
Phone string
}
JSON Tag 的简单应用 - 自定义字段名,去掉空值字段
默认情况下,结构体序列化后的字段名和结构体的字段名保持一致,如在结构体中定义了对外公开的字段,字段名首字母都是大写的,JSON 序列化后得到的也是首字母大写的字段名,并不符合日常开发习惯。
其实 go 提供了在结构体中使用 JSON Tag 定制序列化输出的功能,本文仅使用了“自定义字段名”和“忽略空值字段”两个功能,详见 go 标准库 encoding/json 文档。
现在利用 JSON Tag 来改造上面两个结构体,这里要做的只有两步:
- 把字段名全部改为小写;
- 对 CustomUser 中的 Profile 设置 omitempty 标签,即当 Profile 的值为 nil 时,不输出 Profile 字段;
代码如下:
// 自定义用户输出结构
type CustomUser struct {
ID uint `json:"id"`
Username string `json:"username"`
Role string `json:"role"`
Profile *CustomProfile `json:"profile,omitempty"`
}
// 自定义用户信息输出结构
type CustomProfile struct {
Nickname string `json:"nickname"`
Phone string `json:"phone"`
}
这里有必要说明为什么要在自定义输出结构体中使用 JSON Tag,而不在模型结构体中直接定义。模型结构体定义的是数据模型,和数据库相关,因此模型结构体的 Tag 最好只和数据库相关,也就是 gorm Tag。而序列化往往根据业务需求经常调整,和数据库操作无关,因此在自定义输出结构体中使用 JSON Tag 更合理些,便于理解和维护。
数据映射 - 自定义序列化方法
重点来了,如何将 Gorm 查询得到的源数据映射到自定义输出结构体上?
思路比较简单,就是为 User 模型实现自定义的序列化方法,实现将源数据映射到自定义结构体上并输出自定义结构数据。为了降低耦合,不建议对原 User 模型进行操作,而是创建 User 的副本,再进行操作。
同时为了清楚地演示从 Model 层到 Service 层的流程,将会创建 GetUserListModel(),GetUserModel(),GetUserListService(),GetUserService() 四个函数,用于模拟 Model 层和 Service 层的操作,GetUserListModel(),GetUserModel() 函数仅做查询操作并返回查询源数据,GetUserListService(),GetUserService() 函数将源数据映射到自定义结构体并返回映射后的数据。
上代码:
// 第一步:创建模型结构体的副本
type UserCopy struct{
User
}
// 第二步:重写 MarshalJSON() 方法,实现自定义序列化
func (u *UserCopy) MarshalJSON() ([]byte, error) {
// 将 User 的数据映射到 CustomUser 上
user := CustomUser{
ID: u.ID,
Username: u.Username,
Role: u.Role,
}
// 如果 User 的 Profile 字段不为 nil,
// 则将 Profile 数据映射到 CustomUser 的 Profile 上
if u.Profile != nil {
user.Profile = &CustomProfile{
Nickname: u.Profile.Nickname,
Phone: u.Profile.Phone,
}
}
return json.Marshal(user)
}
// 第三步:获取源数据
// 获取用户列表源数据
func GetUserListModel() ([]*User, error) {
var users []*User
err := DB.Debug().Find(&users).Error
if err != nil {
return nil, errors.New("查询错误")
}
return users, nil
}
// 获取用户详情源数据
func GetUserModel(id uint) (*User, error) {
var user User
err := DB.Debug().
Where("id = ?", id).
Preload("Profile").
First(&user).
Error
if err != nil {
return nil, errors.New("查询错误")
}
return &user, nil
}
// 第四步:获取自定义结构数据
// 获取用户列表自定义数据
func GetUserListService() ([]*UserCopy, error) {
users, err := GetUserListModel()
if err != nil {
return nil, err
}
// 转换成带自定义序列化方法的 UserCopy 类型
list := make([]*UserCopy, 0)
for _, user := range users {
list = append(list, &UserCopy{*user})
}
return list, nil
}
// 获取用户详情自定义数据
func GetUserService(id uint) (*UserCopy, error) {
user, err := GetUserModel(id)
if err != nil {
return nil, err
}
// 转换成带自定义序列化方法的 UserCopy 类型
return &UserCopy{*user}, nil
}
最后,通过调用 GetUserListService(),GetUserService() 方法分别获取自定义结构的用户列表数据和用户详情数据,然后直接序列化输出即可。
列表输出类似这样:
[
{
"id": 1,
"username": "test",
"role": "admin"
},
{
"id": 2,
"username": "test2",
"role": "admin"
},
{
"id": 3,
"username": "test3",
"role": "admin"
}
]
用户详情输出类似这样:
{
"id": 1,
"username": "test",
"role": "admin",
"profile": {
"nickname": "test",
"phone": ""
}
}
数据映射 - Scan 方法的应用
其实 Gorm 提供了 Scan 方法,可直接将查询的数据映射到自定义结构体上,使用也很方便,但为什么前面一直不用,还要自己实现自定义序列化方法呢?原因在于,截止到 Gorm v1.9.12 版本,Scan 方法不支持预加载,需要自行解决预加载数据的支持问题,而且本文采用的 Model、Service 分离的方式,Model 层只负责输出模型数据,自定义输出的任务由 Service 层处理,因此也就没有必要在 Model 层查询时使用 Scan方法做映射了。
不过这里还是介绍下 Scan 方法的使用吧,毕竟不是所有项目都真的需要 MVC,需要分层,有时最简单的方法就是最有效的方法,按需而行才是上上策。
下面介绍如何使用 Scan 方法实现上述需求。这里依然使用上面的 CustomUser 和 CustomProfile 这两个自定义输出结构体。
先实现用户列表的输出,由前面的场景需求可知,用户列表不需要 Profile 信息,也就无需预加载了,可直接这样实现:
// 这里直接使用 CustomUser,而不是实现了自定义序列化方法的 UserCopy
// Scan 方法会自动做映射处理
var users []*CustomUser
DB.Debug().
Model(&User{}).
Scan(&users)
如果要实现带预加载的列表自定义输出,直接使用自定义序列化方法的方式吧。
接着来看下如何使用 Scan 方法实现用户详情的自定义输出,由于 Scan 不支持预加载,需要手动做些处理,代码如下:
var user User
var profile Profile
var userOutput CustomUser
// 将不带关联查询的数据直接按 userOutput 结构扫描赋值
err := DB.Debug().
Model(&user).
Where("id = ?", 1).
Scan(&userOutput).
Error
// 这里要判断查询是否出错,可能查询本身出错,也可能是查询不到对应数据
if err != nil {
return
}
// 只有正常查询到 User 数据,才能继续查询其关联的 Profile 数据,
// 可以简单构造一个对应的 User 数据用于下面的关联查询,
// 这里简单构造一个 ID = 1 的 User 数据用于演示,并不严谨,实际应用需要根据需要进行调整
user.ID = 1
// 获取 Profile 关联数据,并赋值给变量 profile,
// 注意,分步查询中,Model方法中不能传 &User{},而要传递同一个实例,否则无法保证两次查询数据的关联性
DB.Debug().
Model(&user).
Related(&profile, "UserID")
// 手动赋值
userOutput.Profile = &CustomProfile{
Nickname: profile.Nickname,
Phone: profile.Phone,
}
然后将 userOutput 序列化输出即可。
小结
本篇介绍了如何自定义输出结构体,并使用“自定义序列化方法”、“Scan 方法”两种数据映射方式,实现自定义结构的数据输出。
在关键的数据映射方式的选择上,两种方式各有优劣,个人认为:
- 简单应用场景下,使用 Scan 方法方便快捷,代码量也少,但是不支持预加载,需自行处理;
- 复杂应用场景下,推荐使用自定义序列化方法这种方式,虽然代码量多了,但这种方式更灵活,低耦合,便于理解和维护,代码的可读性和可维护性更重要。
顺带抛出一个疑问,在 Restful API 盛行的今天,关联查询是否还那么重要?欢迎一起探讨。
下一篇将介绍如何自定义时间输出格式。
本文仅提供一种解决问题的思路,并不能以点概全,如发现任何问题,欢迎指正,有其他解决方案的也欢迎提出一起交流,谢谢观看!
参考资料:
本文出处:https://www.cnblogs.com/zhenfengxun/
本文链接:https://www.cnblogs.com/zhenfengxun/p/12525365.html
Gorm 预加载及输出处理(二)- 查询输出处理的更多相关文章
- Gorm 预加载及输出处理(三)- 自定义时间格式
前言 Gorm 中 time.Time 类型的字段在 JSON 序列化后呈现的格式为 "2020-03-11T18:26:13+08:00",在 Go 标准库文档 - time 的 ...
- Gorm 预加载及输出处理(一)- 预加载应用
单条关联查询 先创建两个关联模型: // 用户模型 type User struct { gorm.Model Username string `gorm:"type:varchar(20) ...
- jquery实现图片预加载
使用jquery实现图片预加载提高页面加载速度和用户体,本就为大家详细分析jquery图片预加载的实现原理. 什么时候使用图片预加载? 如果页面使用了很多不是最初加载便可见的图片,有必要进行预加载: ...
- jQuery实现图片预加载提高页面加载速度和用户体验
我们在做网站的时候经常会遇到这样的问题:一个页面有大量的图片导致页面加载速度缓慢,经常会出现一个白页用户体验很不好.那么如何解决这个问题呢?首先我们会想到的是提高服务器性能,使用静态缓存等手段来加快图 ...
- jquery实现图片预加载提高页面加载速度
使用jquery实现图片预加载提高页面加载速度和用户体 我们在做网站的时候经常会遇到这样的问题:一个页面有大量的图片导致页面加载速度缓慢,经常会出现一个白页用户体验很不好.那么如何解决这个问题 呢?首 ...
- laravel 关联中的预加载
预加载 当作为属性访问 Eloquent 关联时,关联数据是「懒加载」的.意味着在你第一次访问该属性时,才会加载关联数据.不过,是当你查询父模型时,Eloquent 可以「预加载」关联数据.预加载避免 ...
- Multidex(二)之Dex预加载优化
Multidex(二)之Dex预加载优化 https://www.jianshu.com/p/2891599511ff
- gorm 结构体 预加载
结构体构建 type PlansApproval struct { ID uint Plans_Id int //plans编号 UpdateUser int //更新者 ...
- flex 实现图片播放 方案二 把临时3张图片预加载放入内存
该方案,是预加载:前一张,当前,下一张图片,一共3张图片放入内存中.这样对内存的消耗可以非常小,加载之后的图片就释放内存. 下面示例一个是类ImagePlayers,一个是index.mxml pac ...
随机推荐
- <luogu1347>排序
本来打算当打了个拓扑的板子 后来发现并不只是个板子 差不多 管他呢 #include<cstdio> #include<cstring> #include<iostrea ...
- [hdu4630] No Pain No Game
某次模拟赛的T1. 刚开始怀疑是RMQ......我真是太弱了QAQ 题目传送门 正解是离线操作,把所有询问按r从小到大排序. 然后把数从左到右处理,处理完第i个数,就可以回答所有r==i的询问了. ...
- get请求直接通过浏览器发请求传数组或者list到后台
原文链接: http://blog.csdn.net/qq_27093465/article/details/76160419 感谢原作者 例如: http://localhost:27001/tes ...
- 亚马逊Prime会员的杀价,能说明会员+会越来越便宜吗?
前段时间,京东又坑了!京东调整了物流方案--从原来的购物不满49元只需6元运费,调整到购物不满46元运费15元,运费猛涨了9元!原本京东PLUS会员每月有5张免运费券,但在运费涨价后运费券限制在6元, ...
- 在JavaScript里的“对象字面量”是什么意思?
字面量表示如何表达这个值,一般除去表达式,给变量赋值时,等号右边都可以认为是字面量.字面量分为字符串字面量(string literal ).数组字面量(array literal)和对象字面量(ob ...
- 安全测试——利用Burpsuite密码爆破(Intruder入侵)
本文章仅供学习参考,技术大蛙请绕过. 最近一直在想逛了这么多博客.论坛了,总能收获一堆干货,也从没有给博主个好评什么的,想想着实有些不妥.所以最近就一直想,有时间的时候自己也撒两把小米,就当作是和大家 ...
- 【51nod1462】树据结构
Source and Judge 51nod1462 Analysis 请先思考后再展开 dffxtz师兄出的题 做法一:暴力树剖+分块,时间复杂度为 $O(nlognsqrt n)$ 做法二:利用矩 ...
- 安卓权威编程指南-笔记(第22章 深入学习intent和任务)
本章,我们会使用隐式intent创建一个替换android默认启动器的应用.名为NerdLauncher. NerdLauncher应用能列出设备上的其他应用,点选任意列表项会启动相应应用. 1. 解 ...
- 【原创】从零开始搭建Electron+Vue+Webpack项目框架(五)预加载和Electron自动更新
导航: (一)Electron跑起来(二)从零搭建Vue全家桶+webpack项目框架(三)Electron+Vue+Webpack,联合调试整个项目(四)Electron配置润色(五)预加载及自动更 ...
- js进阶之重复的定时器
使用setInterval()创建的定时器确保了定时器代码规则的插入队列中,这个的问题是:定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行了好几次,而之间没有任何停顿 ...