上一篇《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 来改造上面两个结构体,这里要做的只有两步:

  1. 把字段名全部改为小写;
  2. 对 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 预加载及输出处理(二)- 查询输出处理的更多相关文章

  1. Gorm 预加载及输出处理(三)- 自定义时间格式

    前言 Gorm 中 time.Time 类型的字段在 JSON 序列化后呈现的格式为 "2020-03-11T18:26:13+08:00",在 Go 标准库文档 - time 的 ...

  2. Gorm 预加载及输出处理(一)- 预加载应用

    单条关联查询 先创建两个关联模型: // 用户模型 type User struct { gorm.Model Username string `gorm:"type:varchar(20) ...

  3. jquery实现图片预加载

    使用jquery实现图片预加载提高页面加载速度和用户体,本就为大家详细分析jquery图片预加载的实现原理. 什么时候使用图片预加载? 如果页面使用了很多不是最初加载便可见的图片,有必要进行预加载: ...

  4. jQuery实现图片预加载提高页面加载速度和用户体验

    我们在做网站的时候经常会遇到这样的问题:一个页面有大量的图片导致页面加载速度缓慢,经常会出现一个白页用户体验很不好.那么如何解决这个问题呢?首先我们会想到的是提高服务器性能,使用静态缓存等手段来加快图 ...

  5. jquery实现图片预加载提高页面加载速度

    使用jquery实现图片预加载提高页面加载速度和用户体 我们在做网站的时候经常会遇到这样的问题:一个页面有大量的图片导致页面加载速度缓慢,经常会出现一个白页用户体验很不好.那么如何解决这个问题 呢?首 ...

  6. laravel 关联中的预加载

    预加载 当作为属性访问 Eloquent 关联时,关联数据是「懒加载」的.意味着在你第一次访问该属性时,才会加载关联数据.不过,是当你查询父模型时,Eloquent 可以「预加载」关联数据.预加载避免 ...

  7. Multidex(二)之Dex预加载优化

    Multidex(二)之Dex预加载优化 https://www.jianshu.com/p/2891599511ff

  8. gorm 结构体 预加载

    结构体构建 type PlansApproval struct {     ID uint     Plans_Id int //plans编号     UpdateUser int //更新者    ...

  9. flex 实现图片播放 方案二 把临时3张图片预加载放入内存

    该方案,是预加载:前一张,当前,下一张图片,一共3张图片放入内存中.这样对内存的消耗可以非常小,加载之后的图片就释放内存. 下面示例一个是类ImagePlayers,一个是index.mxml pac ...

随机推荐

  1. 合并cookie,提取json数据

    发送的第3个请求需要前两个请求的cookie,需要对cookie进行合并 发送的请求数据来自于json数据中的某个键值. 这里是删除所有的对话主题目录,每一个目录有一个id,发起删除对话主题目录的请求 ...

  2. SpringBoot:三十五道SpringBoot面试题及答案

    SpringBoot面试前言今天博主将为大家分享三十五道SpringBoot面试题及答案,不喜勿喷,如有异议欢迎讨论! Spring Boot 是微服务中最好的 Java 框架. 我们建议你能够成为一 ...

  3. Docker快速安装kafka

    Docker快速安装kafka | 沈健的技术博客 盒子 盒子 博客 分类 标签 友链 搜索 文章目录 同样基于docker-compose安装,Docker快速部署nginx中有讲到,不在重述 1. ...

  4. python入门机器学习,3行代码搞定线性回归

    本文着重是重新梳理一下线性回归的概念,至于几行代码实现,那个不重要,概念明确了,代码自然水到渠成. “机器学习”对于普通大众来说可能会比较陌生,但是“人工智能”这个词简直是太火了,即便是风云变化的股市 ...

  5. ubuntu 16 下安装 Ubuntu Make

    第一步:下载安装包 地址:http://ftp.gnu.org/gnu/make/ 第二步:解压 先进入存放文件目录,图示: 进行解压,图示: 解压并copy到安装目录 第三步:编译 1.查看目录, ...

  6. 这些科学家用DNA做的鲜为人知事,你估计都没见过!

    DNA世界的每一步都给人类带来奇妙甚至吃惊的发现.研究人员越来越多地探索和掌握了生命中的分子.生物与技术之间的界限以前所未有的方式模糊,有时甚至更糟.但DNA也为复杂疾病带来简单的答案,存储奇怪的文件 ...

  7. useful_tool

    记录工作学习中遇到的经典好用的工具软件. 工作篇 AxeSlide斧子演示 AxeSlide是PPT的良好替代品,跨平台,基于Html5 2D/3D技术开发.动画特效等效果十分不错.工具提供很多免费模 ...

  8. Visual Studio 2013编译Tesseract 3.04

    文章目录 去年时候使用了VS2008编译了Tesseract 3.02版本,主要是参考了一份官方文档,但是对于目前的最新版本并没有给出说明. 本文主要参考了Paul Vorbach的How to bu ...

  9. Android apk签名详解——AS签名、获取签名信息、系统签名、命令行签名

    Apk签名,每一个Android开发者都不陌生.它就是对我们的apk加了一个校验参数,防止apk被掉包.一开始做Android开发,就接触到了apk签名:后来在微信开放平台.高德地图等平台注册时,需要 ...

  10. 前端开发个人小结 · Retrospection的博客

    序 2018年转眼来到了最后一个月,算下来我进入前端之门也有一年了,虽然下半年由于忙于筹备毕业论文的相关事项,前端这一块有所放下,但是想想还是给自己这一年的学习做一个总结. 现代化软件开发确实是一个复 ...