在前一章中,我們建立了資料庫連線和遷移機制,但還沒有實際操作資料庫。現在,我們將實作資料庫層。
在本章中,我們將實作一個基於 GORM 的 MySQL Adapter,並說明如何定義對應資料庫資料表的 Entity Model。
我們的 register
Use Case 需要兩個資料庫操作:檢查電子郵件是否存在,以及建立新使用者。
因此,我們之前在 Use Case 層已經定義了 repository
介面:
// 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)
}
這個介面就是我們應用程式的「Port」。任何想要與 register
Use Case 整合的資料庫實作,都必須滿足這個介面的合約。
Entity Model 是用來對應資料庫資料表結構的 Go struct。它通常位於 Adapter 內部,因為它屬於資料庫實作的細節。
在本專案中,我們建立一個 User
Entity 來對應 users
資料表。
// internal/database/mysql/entity/user.go
package entity
import "time"
type User struct {
ID int `gorm:"column:id"`
Email string `gorm:"column:email"`
Password string `gorm:"column:password_hash"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func (User) TableName() string {
return "users"
}
這裡有幾個重點:
GORM Tags: 我們使用 struct tags (gorm:"..."
) 來告訴 GORM 如何將 struct 欄位對應到資料庫的特定欄位名稱。例如,Password
欄位對應到 password_hash
欄位。這讓我們可以在 Go 中使用更慣用的命名方式(CamelCase),同時對應到資料庫的蛇行命名法(snake_case)。
TableName()
方法: 這個方法告訴 GORM 這個 struct 對應的資料表名稱是 users
。如果沒有這個方法,GORM 會根據 struct 名稱的複數形式(例如 users
)來猜測,但明確指定可以增加程式碼的清晰度。
Entity vs. Domain Model: 在更複雜的應用中,entity.User
(資料庫層模型)和 domain.User
(核心業務層模型)可能是分開的。
Entity Model 關注的是資料如何被儲存,而 Domain Model 關注的是業務規則和邏輯。
在大型專案中將 Entity Model 跟 Domain Model 分離是一種常見的最佳實踐。
在我們目前的專案中,還未提到 Domain Model,之後會在更複雜的 Use Case 中介紹。
Database Adapter 是 repository
介面的具體實作。它包含了與資料庫互動的所有邏輯,並使用我們前面定義的 User
Entity。
我們將延伸之前建立的 Database
struct,為它加上 CheckEmailIsExists
和 CreateUser
方法,使其滿足 repository
介面。
// internal/database/mysql/user.go
func (d *Database) CheckEmailIsExists(ctx context.Context, email string) (bool, error) {
var count int64
// 使用 WithContext 傳遞上下文,有助於追蹤和控制請求
if err := d.db.WithContext(ctx).
Model(entity.User{}).
Where("email = ?", email).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (d *Database) CreateUser(ctx context.Context, email, hashedPassword string) (int, error) {
user := &entity.User{
Email: email,
Password: hashedPassword,
}
// 使用 Create 方法將新的 user 紀錄插入資料庫
if err := d.db.WithContext(ctx).Create(user).Error; err != nil {
return 0, err
}
// 成功後,GORM 會將新紀錄的 ID 回填到 user.ID 中
return user.ID, nil
}
實作細節:
d.db
: 這是我們在 Database
struct 中持有的 GORM 資料庫連線實例。WithContext(ctx)
: 這是一個非常重要的實踐。將 context.Context
傳遞給資料庫操作,可以讓 Go 在請求被取消或超時時,也一併取消資料庫查詢,避免不必要的資源浪費。Model(entity.User{})
: 指定我們要查詢的資料表對應的模型。Where("email = ?", email)
: 加入查詢條件,防止 SQL 注入。Count(&count)
: 執行計數查詢。Create(user)
: 插入一筆新的紀錄。在本章中,我們遵循「Ports and Adapters」模式,成功地實作了資料庫層。我們定義了代表資料庫結構的 Entity
,並建立了一個 Database Adapter
來實作應用程式核心所定義的 repository
介面。
透過這種方式,我們的應用程式核心與資料庫實作完全解耦。
未來如果需要更換資料庫(例如從 MySQL 換到 PostgreSQL),
我們只需要重新實作一個新的 Adapter,而不需要修改任何核心業務邏輯。
以上程式碼的完整內容可以到 Github 觀看