亲宝软件园·资讯

展开

Gorm 预加载及输出处理(二)- 查询输出处理

大漠风起沙飞扬 人气:2
上一篇《Gorm 预加载及输出处理(一)- 预加载应用》中留下的三个问题: - 如何自定义输出结构,只输出指定字段? - 如何自定义字段名,并去掉空值字段? - 如何自定义时间格式? 这一篇先解决前两个问题。 ## 模型结构体中指针类型的应用 先来看一个上一篇中埋下的坑,回顾下 User 模型的定义: ```go // 用户模型 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 语句: ```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 不会将零值写入数据库中,部分零值列举如下: ```go false // bool 0 // integer 0.0 // float "" // string nil // pointer, function, interface, slice, channel, map ``` 解决此问题也很简单,将字段定义为对应的指针类型,赋值时也传指针即可,只要传的值不为 nil,即可正常写入数据库。 现调整 User 模型定义如下: ```go type User struct { ... Active *uint8 `gorm:"type:tinyint unsigned;default:1"` ... } ``` 到这里,应该已经清楚 Gorm 模型字段定义中指针类型的应用场景了,即任何需要保存零值的字段,都应定义为指针类型。利用该特性,顺带把上一篇中直接查询 User 输出空值 Profile 结构体的问题一并解决掉。只要将 User 模型中 Profile 字段的类型修改为 Profile 的指针类型即可: ```go // 用户模型 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 字段; ### 自定义输出结构体 这一步只要按需求创建对应结构体即可,直接上代码: ```go // 自定义用户输出结构 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 文档](https://studygolang.com/static/pkgdoc/pkg/encoding_json.htm)。 现在利用 JSON Tag 来改造上面两个结构体,这里要做的只有两步: 1. 把字段名全部改为小写; 2. 对 CustomUser 中的 Profile 设置 omitempty 标签,即当 Profile 的值为 nil 时,不输出 Profile 字段; 代码如下: ```go // 自定义用户输出结构 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() 函数将源数据映射到自定义结构体并返回映射后的数据。 上代码: ```go // 第一步:创建模型结构体的副本 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() 方法分别获取自定义结构的用户列表数据和用户详情数据,然后直接序列化输出即可。 列表输出类似这样: ```json [ { "id": 1, "username": "test", "role": "admin" }, { "id": 2, "username": "test2", "role": "admin" }, { "id": 3, "username": "test3", "role": "admin" } ] ``` 用户详情输出类似这样: ```json { "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 信息,也就无需预加载了,可直接这样实现: ```go // 这里直接使用 CustomUser,而不是实现了自定义序列化方法的 UserCopy // Scan 方法会自动做映射处理 var users []*CustomUser DB.Debug(). Model(&User{}). Scan(&users) ``` 如果要实现带预加载的列表自定义输出,直接使用自定义序列化方法的方式吧。 接着来看下如何使用 Scan 方法实现用户详情的自定义输出,由于 Scan 不支持预加载,需要手动做些处理,代码如下: ```go 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 盛行的今天,关联查询是否还那么重要?欢迎一起探讨。 下一篇将介绍如何自定义时间输出格式。 本文仅提供一种解决问题的思路,并不能以点概全,如发现任何问题,欢迎指正,有其他解决方案的也欢迎提出一起交流,谢谢观看! ----- 参考资料: - [Gorm官方文档](http://gorm.io/zh_CNhttps://img.qb5200.com/download-x/docs/) - [go 标准库 encoding/json 文档](https://studygolang.com/static/pkgdoc/pkg/encoding_json.htm)

加载全部内容

相关教程
猜你喜欢
用户评论