在前面的文章中,我們已經建立起完整的身份驗證流程,從密碼加密到 JWT 的簽發與驗證。然而,我們還沒處理使用者資料的存取。是時候為這些資料打造一個永久的家了。
本文將引導您完成將 MySQL 資料庫整合到我們專案的完整過程。我們將選用 Go 生態中廣受歡迎的 ORM (Object-Relational Mapping) 函式庫——GORM 來簡化資料庫操作。
GORM 是一個功能強大、對開發者友善的 Go ORM 函式庫。它允許我們用 Go 的結構體 (struct) 來對應資料庫中的資料表 (table),讓我們可以透過操作物件的方式來執行 CRUD (建立、讀取、更新、刪除),而不需要手寫繁瑣的 SQL 語句。這不僅能提升開發效率,也能讓程式碼更易於維護。
更多關於 GORM 的介紹可以參考 GORM 官方網站
現在,讓我們開始動手,一步步將資料庫整合進我們的應用程式。
首先,我們需要將 GORM 和其對應的 MySQL 驅動程式加入專案依賴。
go get gorm.io/gorm
go get gorm.io/driver/mysql
接著,我們在 internal/database/mysql/
目錄下建立 mysql.go
,它將作為所有資料庫連線、設定及操作的中心。
// internal/database/mysql/mysql.go
const maxOpenConns = 25
const maxIdleConns = 3
const maxLiftTime = 5 * time.Minute
type Database struct {
db *gorm.DB
}
func InitDatabase(dsn string, logger *slog.Logger) (*Database, error) {
database, err := connectDB(dsn, maxOpenConns, maxIdleConns, maxLiftTime, logger)
if err != nil {
return nil, err
}
return &Database{database}, nil
}
func connectDB(dsn string, maxOpenConns, maxIdleConns int, maxLiftTime time.Duration, slogger *slog.Logger) (*gorm.DB, error) {
// ...
database, err := gorm.Open(mysql.Open(dsn), &config)
if err != nil {
return nil, err
}
sql, err := database.DB()
if err != nil {
return nil, err
}
sql.SetMaxOpenConns(maxOpenConns)
sql.SetMaxIdleConns(maxIdleConns)
sql.SetConnMaxLifetime(maxLiftTime)
return database, nil
}
在這段程式碼中,我們不只初始化了 GORM,更重要的是設定了資料庫連線池。
在 Go 中,database/sql
套件原生支援連線池,GORM 讓我們可以輕易地存取並設定它:
SetMaxOpenConns(25)
: 設定連線池中最多可開啟 25 個資料庫連線。SetMaxIdleConns(3)
: 設定連線池中最多可保留 3 個閒置連線,以供快速重複使用。SetConnMaxLifetime(5 * time.Minute)
: 設定每個連線的最長生命週期為 5 分鐘。這有助於自動回收老舊連線,避免因網路問題或資料庫重啟造成的連線失效。良好的連線池管理是高效能後端服務的基石。
以上的三個連線參數可以根據實際需求進行調整。
另外,我們也另外建立一個 Database struct 來持有 gorm.DB 實例,
將 gorm 這一個外部 Library 的影響,
限縮在 internal/database/mysql/
這個 package 裡面,
讓其他程式碼不會直接依賴到 gorm,達到鬆耦合的效果。
這樣未來如果要更換 ORM 或資料庫,只需要修改這個 package 即可。
我們從設定檔動態產生連線所需的 DSN (Data Source Name) 字串。
// internal/config/config_type.go
func (db *DatabaseConfig) DSN() string {
return db.User + ":" + db.Password + "@tcp(" + db.Host + ":" + db.Port + ")/" + db.Database + "?parseTime=true"
}
為了讓資料庫操作能被 usecase 層的商業邏輯使用,我們需要更新 register
usecase 中的 repository
介面,並為其方法加入 context.Context
,這是 Go 語言中處理請求範圍、超時和取消的標準實踐。
// internal/usecase/api/user/register/register.go
type repository interface {
CheckEmailIsExists(ctx context.Context, email string) (bool, error)
CreateUser(ctx context.Context, email, hashedPassword string) (int, error)
}
最後一步,是將我們新建立的 Database
服務注入到應用程式的核心 Application
中,並將其傳遞給真正需要它的 usecase。
首先,在 Application
結構體中加入 Database
:
// internal/application/application.go
type Application struct {
Config *config.Config
Logger *logger.Slogger
Database *mysql.Database // <--- 新增 Database 服務
// ...
}
接著,在 New
函式中初始化 Database
並注入:
// internal/application/application.go
func New(cfg *config.Config) (*Application, error) {
// ...
database, err := mysql.InitDatabase(cfg.MySQL.DSN(), logger.GetDatabaseLogger())
if err != nil {
return nil, err
}
app := &Application{
Config: cfg,
Logger: logger,
Database: database, // <--- 注入 Database 實例
}
// ...
}
最後,將 Database
服務傳遞給 register
usecase,完成依賴鏈的串接:
// internal/application/usecase.go
func NewUserUseCase(app *Application) *UserUseCase {
return &UserUseCase{
// 將 app.Database 作為 repository 傳遞進 usecase
Register: register.NewUseCase(app.Database, app.Service.Password, app.Service.Token),
}
}
至此,我們已經將 register
usecase 需要的相關功能完成注入。
恭喜!我們的應用程式現在已經成功連接到 MySQL 資料庫,並建立了一套穩健、可維護的資料庫服務層。透過 GORM 和完善的連線池設定,我們為接下來的開發工作打下了堅實的基礎。
在下一篇文章中,我們將會實作 user
資料表的遷移 (migration),並說明 migration 的重要性。
以上程式碼的完整內容可以到 Github 觀看