上一篇《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