提示:
- 所有體系課見專欄:Go 項目開發極速入門實戰課;
- 歡迎加入 云原生 AI 實戰 星球,12+ 高質量體系課、20+ 高質量實戰項目助你在 AI 時代建立技術競爭力(聚焦于 Go、云原生、AI Infra);
- 本節課最終源碼位于 fastgo 項目的 feature/s11 分支;
- 更詳細的課程版本見:Go 項目開發中級實戰課:24 | 業務實現(1):實現 Store 層數據結構定義
在完成基礎功能開發后,需要進一步開發與業務邏輯相關的代碼。相較于基礎功能,業務邏輯代碼不僅占據了代碼倉庫的大部分代碼量,其復雜性也更高。因此,需要設計一種合理的代碼架構,以確保代碼的可讀性、可維護性和可擴展性。目前,業界較為流行且被廣泛認可的代碼架構是簡潔架構。
本課程第 14 節課詳細介紹了 fastgo 項目的簡潔架構設計。本節課,將根據第 14 節課的簡潔架構設計,實現 fastgo 的業務邏輯。
三層架構開發
在第 14 節課中介紹了 fastgo 三層簡潔架構的依賴關系:Handler 層依賴 Biz 層,Biz 層依賴 Store 層,Store 層依賴數據庫。依賴關系如下圖所示。
為了能夠隨時測試所開發的代碼功能,最優的方式是優先開發依賴較少的組件。否則,需要先 Mock 或開發所依賴的功能(層)。因此,開發順序應為:先開發 Store 層,接著是 Biz 層,最后是 Handler 層。
本節課及接下來幾節課代碼改動量較大,其中有很多同類改動。為了提高你的學習效率,本節課不會對代碼進行逐行解讀,相反主要講解其中的核心設計和實現。
Store 層數據結構定義
根據依賴關系,需要先開發 Store 層代碼。Store 層依賴一些數據類型。如果項目持久存儲用的是 MySQL/MariaDB 數據庫,這些數據類型其實就是 GORM Model。GORM Model 實際上是數據庫表字段到 Go 結構體的映射。可以根據 fastgo 數據庫中的表來創建對應的 Model。
fastgo 數據庫中包含以下三張表,這些表的結構在項目設計階段已完成設計和創建:
user
表:該表用于存儲用戶數據;post
表:該表用于存儲博客數據。
需要根據表名、表字段及表字段類型創建 Store 層的 Go 結構體,以映射數據庫中的對應表。
GORM Model 結構體定義可以手動編寫,也可以借助工具自動生成。建議使用工具自動生成,具體步驟如下:
- 創建數據庫和數據庫表;
- 根據數據庫表生成 Model 文件;
- 修改生成的 Go 代碼。
根據數據庫表生成 Model 文件
在 Go 項目開發中,編寫 GORM Model 文件通常有多種方式,例如根據數據庫表結構手動編寫 GORM Model,或使用讀取數據庫表結構并自動生成 GORM Model 的工具,例如 GORM 官方提供 gentool。
$ go install gorm.io/gen/tools/gentool@latest
$ gentool -db mysql -dsn 'fastgo:fastgo1234@tcp(1127.0.0.1:3306)/fastgo' -onlyModel -modelPkgName internal/apiserver/model
上述命令會在 internal/apiserver/model/ 目錄下生成 user.gen.go 和 post.gen.go GORM Model 文件。這 2 個文件中包含了 GORM Model 結構體定義。
提示:在第 02 節課中,我們安裝了數據庫,并創建了 fastgo 數據庫及表。
添加 GORM 鉤子
在生成了 GORM Model 結構體之后,可根據需要給這些結構體添加一些 GORM 鉤子。常用的 GORM 鉤子見下表所示:
返回頭 | 說明 |
---|---|
BeforeCreate | 在執行 INSERT 語句前觸發(比如對數據進行校驗或補充字段) |
AfterCreate | 在執行 INSERT 語句后觸發(比如生成關聯值或更新其他表數據) |
BeforeFind | 在執行查詢(SELECT)語句前觸發(比如動態調整查詢條件) |
AfterFind | 在執行查詢(SELECT)操作后觸發(比如格式化數據或處理查詢返回值) |
BeforeUpdate | 在執行 UPDATE 語句前觸發(比如校驗更新字段的合法性) |
AfterUpdate | 在執行 UPDATE 語句后觸發(比如記錄操作日志或更新緩存) |
BeforeDelete | 在執行 DELETE 語句前觸發(比如檢查業務邏輯或軟刪除處理) |
AfterDelete | 在執行 DELETE 語句后觸發(比如記錄日志或同步其他系統的數據) |
BeforeSave | 在執行任何保存(INSERT 或 UPDATE)操作之前觸發(適用于既需要創建也需要更新的公共邏輯) |
AfterSave | 在執行任何保存(INSERT 或 UPDATE)操作之后觸發(適用于既需要創建也需要更新的公共邏輯) |
fastgo 項目在 internal/apiserver/model/hook.go 文件中,添加了數據庫表 userID
和 postID
字段的自動生成鉤子,用來生成并保存記錄的唯一標識符,代碼如下所示:
package modelimport ("gorm.io/gorm""github.com/onexstack/fastgo/internal/pkg/rid"
)// AfterCreate 在創建數據庫記錄之后生成 postID.
func (m *PostM) AfterCreate(tx *gorm.DB) error {m.PostID = rid.PostID.New(uint64(m.ID))return tx.Save(m).Error
}// AfterCreate 在創建數據庫記錄之后生成 userID.
func (m *UserM) AfterCreate(tx *gorm.DB) error {m.UserID = rid.UserID.New(uint64(m.ID))return tx.Save(m).Error
}
上述代碼在創建完數據庫表記錄之后,會調用 rid
包,基于數據庫生成的自增 ID 生成一個形如 user-uvalgf
的英文唯一 ID,并調用 tx.Save()
方法將 ID 更新到表記錄中。
Go 項目開發中,需要為每一個條 REST 資源生成唯一標識符(Unique Identifier,UID),以唯一定位該 REST 資源。例如更新資源、刪除資源時,需要提供該唯一 ID。
fastgo 項目的 github.com/onexstack/fastgo/internal/pkg/rid 包用來生成唯一 ID。rid(resource id)包核心實現如下:
package ridimport ("github.com/onexstack/onexstack/pkg/id"
)const defaultABC = "abcdefghijklmnopqrstuvwxyz1234567890"type ResourceID stringconst (// UserID 定義用戶資源標識符.UserID ResourceID = "user"// PostID 定義博文資源標識符.PostID ResourceID = "post"
)// String 將資源標識符轉換為字符串.
func (rid ResourceID) String() string {return string(rid)
}// New 創建帶前綴的唯一標識符.
func (rid ResourceID) New(counter uint64) string {// 使用自定義選項生成唯一標識符uniqueStr := id.NewCode(counter,id.WithCodeChars([]rune(defaultABC)),id.WithCodeL(6),id.WithCodeSalt(Salt()),)return rid.String() + "-" + uniqueStr
}
上述代碼,定義了一個 ResourceID
數據類型,其 String
方法和 New
方法,分別用來返回資源的字符串標識和資源的唯一 ID,格式為 <資源標識符>-<6 位隨機數>
。使用帶資源前綴的唯一 ID,有利于通過唯一 ID 辨別資源類型,通過前綴避免可能的隨機數沖突。
ResourceID
是一個簡單的、可擴展的統一表示形式,未來可根據需要添加更多的自定義資源,并復用 ResourceID
的方法,生成新資源的唯一 ID,例如 comment-w6irkg
。New(counter uint64)
方法中的 counter
通常為數據庫自增 ID,基于數據庫自增 ID 生成唯一標識,不僅可以生成短小、易讀的唯一 ID,還可以隱藏掉自增 ID 的背后的數據規模。