首頁 >資料庫 >mysql教程 >資料庫遷移對於 Golang 服務,為什麼重要?

資料庫遷移對於 Golang 服務,為什麼重要?

Patricia Arquette
Patricia Arquette原創
2024-10-22 21:06:021063瀏覽

DB Migration For Golang Services, Why it matters?

資料庫遷移,為什麼重要?

您是否曾經遇到過這樣的情況:當您使用更新的資料庫架構在生產環境中部署新的更新時,但之後出現錯誤並需要恢復內容......這就是遷移出現的情況。

資料庫遷移有幾個關鍵目的:

  1. 架構演變:隨著應用程式的演變,它們的資料模型也會改變。遷移允許開發人員系統地更新資料庫架構以反映這些更改,確保資料庫結構與應用程式程式碼相符。
  2. 版本控制:遷移提供了一種對資料庫架構進行版本控制的方法,允許團隊追蹤一段時間內的變更。此版本控制有助於理解資料庫的演變並有助於開發人員之間的協作。
  3. 跨環境的一致性:遷移確保資料庫架構在不同環境(開發、測試、生產)中保持一致。這降低了可能導致錯誤和整合問題的差異風險。
  4. 回滾功能:許多遷移工具都支援回滾更改,允許開發人員在遷移導致問題時恢復到資料庫之前的狀態。這增強了開發和部署過程中的穩定性。
  5. 自動部署:遷移可以作為部署過程的一部分自動化,確保將必要的架構變更應用於資料庫,而無需手動幹預。這簡化了發布流程並減少了人為錯誤。

在golang專案中應用

要使用 GORM 和 MySQL 為 Golang 服務建立全面的生產級設置,以便輕鬆遷移、更新和回滾,您需要包含遷移工具、處理資料庫連接池並確保正確的結構定義。這是一個完整的範例來引導您完成整個過程:

專案結構

/golang-service
|-- main.go
|-- database
|   |-- migration.go
|-- models
|   |-- user.go
|-- config
|   |-- config.go
|-- migrations
|   |-- ...
|-- go.mod

1.資料庫配置(config/config.go)

package config

import (
    "fmt"
    "log"
    "os"
    "time"

    "github.com/joho/godotenv"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var DB *gorm.DB

func ConnectDB() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    // charset=utf8mb4: Sets the character set to utf8mb4, which supports all Unicode characters, including emojis.
    // parseTime=True: Tells the driver to automatically parse DATE and DATETIME values into Go's time.Time type.
    // loc=Local: Uses the local timezone of the server for time-related queries and storage.
    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASS"),
        os.Getenv("DB_HOST"),
        os.Getenv("DB_PORT"),
        os.Getenv("DB_NAME"),
    )

    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    sqlDB, err := db.DB()
    if err != nil {
        panic("failed to configure database connection")
    }

    // Set connection pool settings
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)
    sqlDB.SetConnMaxLifetime(time.Hour)

    // 1.sqlDB.SetMaxIdleConns(10)
    // Sets the maximum number of idle (unused but open) connections in the connection pool.
    // A value of 10 means up to 10 connections can remain idle, ready to be reused.

    // 2. sqlDB.SetMaxOpenConns(100):
    // Sets the maximum number of open (active or idle) connections that can be created to the database.
    // A value of 100 limits the total number of connections, helping to prevent overloading the database.

    // 3. sqlDB.SetConnMaxLifetime(time.Hour):
    // Sets the maximum amount of time a connection can be reused before it’s closed.
    // A value of time.Hour means that each connection will be kept for up to 1 hour, after which it will be discarded and a new connection will be created if needed.

    DB = db
}

2. 資料庫遷移(database/migration.go)

package database

import (
    "golang-service/models"
    "golang-service/migrations"
    "gorm.io/gorm"
)

func Migrate(db *gorm.DB) {
    db.AutoMigrate(&models.User{})
    // Apply additional custom migrations if needed
}

3.模型(models/user.go)

package models

import "gorm.io/gorm"

type User struct {
    gorm.Model
    Name  string `json:"name"`
}

4.環境配置(.env)

DB_USER=root
DB_PASS=yourpassword
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=yourdb

5. 主入口點(main.go)

package main

import (
    "golang-service/config"
    "golang-service/database"
    "golang-service/models"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

func main() {
    config.ConnectDB()
    database.Migrate(config.DB)

    r := gin.Default()
    r.POST("/users", createUser)
    r.GET("/users/:id", getUser)
    r.Run(":8080")
}

func createUser(c *gin.Context) {
    var user models.User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    if err := config.DB.Create(&user).Error; err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    c.JSON(201, user)
}

func getUser(c *gin.Context) {
    id := c.Param("id")
    var user models.User

    if err := config.DB.First(&user, id).Error; err != nil {
        c.JSON(404, gin.H{"error": "User not found"})
        return
    }

    c.JSON(200, user)
}

6、說明:

  • 資料庫配置:管理連接池以獲得生產級效能。
  • 遷移檔案:(在遷移資料夾中)幫助資料庫架構進行版本控制。
  • GORM 模型:將資料庫表對應到 Go 結構。
  • 資料庫遷移:(在資料庫資料夾中)隨著時間的推移更改表的自訂邏輯,以便輕鬆回滾。
  • 測試:您可以使用 httptest 和 testify 為此設定建立整合測試。

7. 建立第一個遷移

  1. 對於生產環境,我們可以使用像 golang-migrate 這樣的遷移程式庫來應用、回滾或重做遷移。

    安裝golang-migrate

    go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
    
  2. 為使用者表產生遷移檔案

    migrate create -ext=sql -dir=./migrations -seq create_users_table
    

    運行指令後,我們將得到一對 .up.sql (用於更新架構)和 down.sql (用於稍後可能的回滾)。數字000001是自動產生的遷移索引。

    /golang-service
    |-- migrations
    |   |-- 000001_create_users_table.down.sql
    |   |-- 000001_create_users_table.up.sql
    

    新增相關sql指令到.up檔和.down檔。

    000001_create_users_table.up.sql

    CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    created_at DATETIME,
    updated_at DATETIME,
    deleted_at DATETIME);
    

    000001_create_users_table.down.sql

    DROP TABLE IF EXISTS users;
    

    運行向上遷移並使用以下命令將變更套用到資料庫(-verbose 標誌以查看更多日誌詳細資訊):

    migrate -path ./migrations -database "mysql://user:password@tcp(localhost:3306)/dbname" -verbose up
    

    如果我們遇到遷移問題,我們可以使用以下命令查看目前的遷移版本及其狀態:

    migrate -path ./migrations -database "mysql://user:password@tcp(localhost:3306)/dbname" version
    

    如果因為某些原因導致遷移失敗,我們可以考慮使用強制(謹慎使用)指令以及髒遷移的版本號。如果版本是1(可以在migrations或schema_migrations表中檢查),我們將運行:

    migrate -path ./migrations -database "mysql://user:password@tcp(localhost:3306)/dbname" force 1
    

8.改變計劃

  1. 在某個時間點,我們可能想要新增功能,其中一些可能需要更改資料方案,例如我們想要在使用者表格中新增電子郵件欄位。我們將按照以下方式進行。

    進行新的遷移,將電子郵件列新增至使用者表

    migrate create -ext=sql -dir=./migrations -seq add_email_to_users
    

    現在我們有了一對新的 .up.sql 和 .down.sql

    /golang-service
    |-- migrations
    |   |-- 000001_create_users_table.down.sql
    |   |-- 000001_create_users_table.up.sql
    |   |-- 000002_add_email_to_users.down.sql
    |   |-- 000002_add_email_to_users.up.sql
    
  2. 將以下內容加入*_add_email_to_users.*.sql檔案

    000002_add_email_to_users.up.sql

    /golang-service
    |-- main.go
    |-- database
    |   |-- migration.go
    |-- models
    |   |-- user.go
    |-- config
    |   |-- config.go
    |-- migrations
    |   |-- ...
    |-- go.mod
    
    

    000002_add_email_to_users.down.sql

    package config
    
    import (
        "fmt"
        "log"
        "os"
        "time"
    
        "github.com/joho/godotenv"
        "gorm.io/driver/mysql"
        "gorm.io/gorm"
    )
    
    var DB *gorm.DB
    
    func ConnectDB() {
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
        }
    
        // charset=utf8mb4: Sets the character set to utf8mb4, which supports all Unicode characters, including emojis.
        // parseTime=True: Tells the driver to automatically parse DATE and DATETIME values into Go's time.Time type.
        // loc=Local: Uses the local timezone of the server for time-related queries and storage.
        dsn := fmt.Sprintf(
            "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
            os.Getenv("DB_USER"),
            os.Getenv("DB_PASS"),
            os.Getenv("DB_HOST"),
            os.Getenv("DB_PORT"),
            os.Getenv("DB_NAME"),
        )
    
        db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
        if err != nil {
            panic("failed to connect database")
        }
    
        sqlDB, err := db.DB()
        if err != nil {
            panic("failed to configure database connection")
        }
    
        // Set connection pool settings
        sqlDB.SetMaxIdleConns(10)
        sqlDB.SetMaxOpenConns(100)
        sqlDB.SetConnMaxLifetime(time.Hour)
    
        // 1.sqlDB.SetMaxIdleConns(10)
        // Sets the maximum number of idle (unused but open) connections in the connection pool.
        // A value of 10 means up to 10 connections can remain idle, ready to be reused.
    
        // 2. sqlDB.SetMaxOpenConns(100):
        // Sets the maximum number of open (active or idle) connections that can be created to the database.
        // A value of 100 limits the total number of connections, helping to prevent overloading the database.
    
        // 3. sqlDB.SetConnMaxLifetime(time.Hour):
        // Sets the maximum amount of time a connection can be reused before it’s closed.
        // A value of time.Hour means that each connection will be kept for up to 1 hour, after which it will be discarded and a new connection will be created if needed.
    
        DB = db
    }
    
    

    再次執行向上遷移指令以更新資料模式

    package database
    
    import (
        "golang-service/models"
        "golang-service/migrations"
        "gorm.io/gorm"
    )
    
    func Migrate(db *gorm.DB) {
        db.AutoMigrate(&models.User{})
        // Apply additional custom migrations if needed
    }
    
    

    我們還需要更新 golang users 結構(新增 Email 欄位)以使其與新模式保持同步..

    package models
    
    import "gorm.io/gorm"
    
    type User struct {
        gorm.Model
        Name  string `json:"name"`
    }
    
    

9. 回滾遷移:

如果因為某些原因我們在新更新的模式中出現錯誤,並且需要回滾,這種情況下我們將使用 down 指令:

DB_USER=root
DB_PASS=yourpassword
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=yourdb

數字 1 表示我們要回滾 1 個遷移。

這裡我們還需要手動更新 golang users 結構(刪除 Email 欄位)以反映資料架構變更。

package main

import (
    "golang-service/config"
    "golang-service/database"
    "golang-service/models"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

func main() {
    config.ConnectDB()
    database.Migrate(config.DB)

    r := gin.Default()
    r.POST("/users", createUser)
    r.GET("/users/:id", getUser)
    r.Run(":8080")
}

func createUser(c *gin.Context) {
    var user models.User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    if err := config.DB.Create(&user).Error; err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    c.JSON(201, user)
}

func getUser(c *gin.Context) {
    id := c.Param("id")
    var user models.User

    if err := config.DB.First(&user, id).Error; err != nil {
        c.JSON(404, gin.H{"error": "User not found"})
        return
    }

    c.JSON(200, user)
}

10. 與 Makefile 一起使用

為了簡化遷移和回滾的過程,我們可以新增一個Makefile。

go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

Makefile 內容如下。

migrate create -ext=sql -dir=./migrations -seq create_users_table

現在我們只需在 CLI 上執行 make migrate_up 或 make migrate_down 即可進行遷移和回滾。

11.注意事項:

  • 回滾期間資料遺失:回滾刪除列或表的移轉可能會導致資料遺失,因此在執行回溯之前始終備份資料。
  • CI/CD 整合:將遷移過程整合到 CI/CD 管道中,以在部署期間自動執行架構變更。
  • 資料庫備份:安排定期資料庫備份,以防止遷移錯誤時遺失資料。

關於資料庫備份

在回滾遷移或進行可能影響資料庫的變更之前,以下是需要考慮的一些關鍵點。

  1. 架構更改:如果遷移涉及更改架構(例如,新增或刪除列、更改資料類型),則回滾到先前的遷移可能會導致儲存在這些變更的列或表中的任何資料遺失.
  2. 資料刪除:如果遷移包含刪除資料的命令(例如刪除表或截斷表),則回滾將執行相應的「向下」遷移,這可能會永久刪除該資料。
  3. 事務處理:如果您的遷移工具支援事務,則回滾可能會更安全,因為變更是在事務中套用的。但是,如果您在交易之外手動執行 SQL 命令,則存在遺失資料的風險。
  4. 資料完整性:如果您以取決於目前架構的方式修改了數據,則回滾可能會使您的資料庫處於不一致的狀態。

所以備份資料至關重要。這是一個簡短的指南:

  1. 資料庫轉儲:
    使用特定於資料庫的工具建立資料庫的完整備份。對於 MySQL,您可以使用:

    /golang-service
    |-- main.go
    |-- database
    |   |-- migration.go
    |-- models
    |   |-- user.go
    |-- config
    |   |-- config.go
    |-- migrations
    |   |-- ...
    |-- go.mod
    
    

    這將建立一個檔案 (backup_before_rollback.sql),其中包含 dbname 資料庫的所有資料和架構。

  2. 匯出特定表:
    如果您只需要備份某些表,請在 mysqldump 指令中指定它們:

    package config
    
    import (
        "fmt"
        "log"
        "os"
        "time"
    
        "github.com/joho/godotenv"
        "gorm.io/driver/mysql"
        "gorm.io/gorm"
    )
    
    var DB *gorm.DB
    
    func ConnectDB() {
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
        }
    
        // charset=utf8mb4: Sets the character set to utf8mb4, which supports all Unicode characters, including emojis.
        // parseTime=True: Tells the driver to automatically parse DATE and DATETIME values into Go's time.Time type.
        // loc=Local: Uses the local timezone of the server for time-related queries and storage.
        dsn := fmt.Sprintf(
            "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
            os.Getenv("DB_USER"),
            os.Getenv("DB_PASS"),
            os.Getenv("DB_HOST"),
            os.Getenv("DB_PORT"),
            os.Getenv("DB_NAME"),
        )
    
        db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
        if err != nil {
            panic("failed to connect database")
        }
    
        sqlDB, err := db.DB()
        if err != nil {
            panic("failed to configure database connection")
        }
    
        // Set connection pool settings
        sqlDB.SetMaxIdleConns(10)
        sqlDB.SetMaxOpenConns(100)
        sqlDB.SetConnMaxLifetime(time.Hour)
    
        // 1.sqlDB.SetMaxIdleConns(10)
        // Sets the maximum number of idle (unused but open) connections in the connection pool.
        // A value of 10 means up to 10 connections can remain idle, ready to be reused.
    
        // 2. sqlDB.SetMaxOpenConns(100):
        // Sets the maximum number of open (active or idle) connections that can be created to the database.
        // A value of 100 limits the total number of connections, helping to prevent overloading the database.
    
        // 3. sqlDB.SetConnMaxLifetime(time.Hour):
        // Sets the maximum amount of time a connection can be reused before it’s closed.
        // A value of time.Hour means that each connection will be kept for up to 1 hour, after which it will be discarded and a new connection will be created if needed.
    
        DB = db
    }
    
    
  3. 驗證備份:
    確保備份檔案已建立並檢查其大小或開啟它以確保它包含必要的資料。

  4. 安全地儲存備份:
    將備份副本保存在安全位置,例如雲端儲存或單獨的伺服器,以防止回溯過程中資料遺失。

雲端備份

要在使用 Golang 並部署在 AWS EKS 上時備份 MySQL 數據,您可以按照以下步驟操作:

  1. 使用mysqldump進行資料庫備份:
    使用 Kubernetes cron 作業建立 MySQL 資料庫的 mysqldump。

    package database
    
    import (
        "golang-service/models"
        "golang-service/migrations"
        "gorm.io/gorm"
    )
    
    func Migrate(db *gorm.DB) {
        db.AutoMigrate(&models.User{})
        // Apply additional custom migrations if needed
    }
    
    

    將其儲存在持久性磁碟區或 S3 儲存桶中。

  2. 使用 Kubernetes CronJob 實現自動化:
    使用 Kubernetes CronJob 自動化 mysqldump 流程。
    YAML 設定範例:yaml

    package models
    
    import "gorm.io/gorm"
    
    type User struct {
        gorm.Model
        Name  string `json:"name"`
    }
    
    


    `

  3. 使用 AWS RDS 自動備份(如果使用 RDS):
    如果您的 MySQL 資料庫位於 AWS RDS 上,您可以利用 RDS 自動備份和快照。
    設定備份保留期並手動拍攝快照或使用 Lambda 函數自動拍攝快照。

  4. 使用 Velero 備份持久卷 (PV):
    使用 Kubernetes 備份工具 Velero 來備份保存 MySQL 資料的持久卷。
    在您的 EKS 叢集上安裝 Velero 並將其配置為備份到 S3。

透過使用這些方法,您可以確保您的 MySQL 資料得到定期備份和安全儲存。

以上是資料庫遷移對於 Golang 服務,為什麼重要?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn