iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Software Development

Go Clean Architecture API 開發全攻略系列 第 11

資料庫整合 (一):使用 GORM 連接 MySQL

  • 分享至 

  • xImage
  •  

在前面的文章中,我們已經建立起完整的身份驗證流程,從密碼加密到 JWT 的簽發與驗證。然而,我們還沒處理使用者資料的存取。是時候為這些資料打造一個永久的家了。

本文將引導您完成將 MySQL 資料庫整合到我們專案的完整過程。我們將選用 Go 生態中廣受歡迎的 ORM (Object-Relational Mapping) 函式庫——GORM 來簡化資料庫操作。

為何選擇 GORM?

GORM 是一個功能強大、對開發者友善的 Go ORM 函式庫。它允許我們用 Go 的結構體 (struct) 來對應資料庫中的資料表 (table),讓我們可以透過操作物件的方式來執行 CRUD (建立、讀取、更新、刪除),而不需要手寫繁瑣的 SQL 語句。這不僅能提升開發效率,也能讓程式碼更易於維護。

更多關於 GORM 的介紹可以參考 GORM 官方網站

實作步驟

現在,讓我們開始動手,一步步將資料庫整合進我們的應用程式。

1. 安裝 GORM 與 MySQL 驅動

首先,我們需要將 GORM 和其對應的 MySQL 驅動程式加入專案依賴。

go get gorm.io/gorm
go get gorm.io/driver/mysql

2. 建立核心 Database 服務與連線管理

接著,我們在 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 即可。

3. 彈性的資料庫連線設定 (DSN)

我們從設定檔動態產生連線所需的 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"
}

4. 更新 Repository 介面

為了讓資料庫操作能被 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)
}

5. 依賴注入 (Dependency Injection)

最後一步,是將我們新建立的 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 觀看


上一篇
身份驗證詳解 (二):JWT (JSON Web Token) 的生成與解析
下一篇
資料庫整合 (二):資料庫遷移 (Migration)
系列文
Go Clean Architecture API 開發全攻略19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言