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 來改造上面兩個結構體,這里要做的只有兩步:

  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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM