数据库迁移对于 Golang 服务,为什么重要?

Patricia Arquette
发布: 2024-10-22 21:06:02
原创
973 人浏览过

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中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责声明 Sitemap
PHP中文网:公益在线PHP培训,帮助PHP学习者快速成长!