iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

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

Entity Model 與實作 Database Adapter

  • 分享至 

  • xImage
  •  

在前一章中,我們建立了資料庫連線和遷移機制,但還沒有實際操作資料庫。現在,我們將實作資料庫層。

在本章中,我們將實作一個基於 GORM 的 MySQL Adapter,並說明如何定義對應資料庫資料表的 Entity Model。

Repository 介面 (The Port)

我們的 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

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 (The Adapter)

Database Adapter 是 repository 介面的具體實作。它包含了與資料庫互動的所有邏輯,並使用我們前面定義的 User Entity。

我們將延伸之前建立的 Database struct,為它加上 CheckEmailIsExistsCreateUser 方法,使其滿足 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 在請求被取消或超時時,也一併取消資料庫查詢,避免不必要的資源浪費。
  • GORM 方法鏈: GORM 提供了流暢的 API,讓我們可以像鏈式呼叫一樣組合查詢。
    • Model(entity.User{}): 指定我們要查詢的資料表對應的模型。
    • Where("email = ?", email): 加入查詢條件,防止 SQL 注入。
    • Count(&count): 執行計數查詢。
    • Create(user): 插入一筆新的紀錄。

總結

在本章中,我們遵循「Ports and Adapters」模式,成功地實作了資料庫層。我們定義了代表資料庫結構的 Entity,並建立了一個 Database Adapter 來實作應用程式核心所定義的 repository 介面。

透過這種方式,我們的應用程式核心與資料庫實作完全解耦。
未來如果需要更換資料庫(例如從 MySQL 換到 PostgreSQL),
我們只需要重新實作一個新的 Adapter,而不需要修改任何核心業務邏輯。

以上程式碼的完整內容可以到 Github 觀看


上一篇
資料庫整合 (二):資料庫遷移 (Migration)
下一篇
Controller 層:API 的守門員
系列文
Go Clean Architecture API 開發全攻略19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言