这里有必要说明为什么要在自定义输出结构体中使用 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)如果要实现带预加载的列表自定义输出,直接使用自定义序列化方法的方式吧。