”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 数据库迁移对于 Golang 服务,为什么重要?

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

发布于2024-11-17
浏览:657

DB Migration For Golang Services, Why it matters?

DB Migration, why it matters?

Have you ever faced the situations when you deploy new update on production with updated database schemas, but got bugs after that and need to revert things.... that's when migration comes into place.

Database migration serves several key purposes:

  1. Schema Evolution: As applications evolve, their data models change. Migrations allow developers to systematically update the database schema to reflect these changes, ensuring that the database structure matches the application code.
  2. Version Control: Migrations provide a way to version the database schema, allowing teams to track changes over time. This versioning helps in understanding the evolution of the database and aids in collaboration among developers.
  3. Consistency Across Environments: Migrations ensure that the database schema is consistent across different environments (development, testing, production). This reduces the risk of discrepancies that can lead to bugs and integration issues.
  4. Rollback Capability: Many migration tools support rolling back changes, allowing developers to revert to a previous state of the database if a migration causes issues. This enhances stability during the development and deployment process.
  5. Automated Deployment: Migrations can be automated as part of the deployment process, ensuring that the necessary schema changes are applied to the database without manual intervention. This streamlines the release process and reduces human error.

Applying in golang projects

To create a comprehensive, production-grade setup for a Golang service using GORM with MySQL that allows for easy migrations, updates, and rollbacks, you need to include migration tooling, handle database connection pooling, and ensure proper struct definitions. Here’s a complete example to guide you through the process:

Project Structure

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

1. Database Configuration (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 (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 (models/user.go)

package models

import "gorm.io/gorm"

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

4. Environment Configuration (.env)

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

5. Main Entry Point (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. Explanation:

  • Database Config: Manages connection pooling for production-grade performance.
  • Migration Files: (in migrations folder) Helps in versioning the database schema.
  • GORM Models: Maps database tables to Go structs.
  • Database Migrations: (in database folder) Custom logic for altering tables over time, allowing for easy rollbacks.
  • Testing: You can create integration tests for this setup using httptest and testify.

7. Create First Migration

  1. For production environments, we could use a migration library like golang-migrate to apply, rollback, or redo migrations.

    Install golang-migrate:

    go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
    
  2. Generate migrate files for users table

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

    After running the command we'll get a pair of .up.sql (to update schema) and down.sql (for potential rollback later) . The number 000001 is the auto generated index of migration.

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

    Add relevant sql command to .up file , and .down file.

    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;
    

    Run the up migration and apply changes to the database with the following command (-verbose flag to see more log details):

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

    In case we got issue with migration we can use the following command to see the current migration version and its status:

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

    If we have a broken migration for some reasons we can consider to use the force (use carefully) command with the version number of the dirty migration. If the version is 1 (could check it in migrations or schema_migrations table), we would run:

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

8. Changing schemes

  1. At some point in time, we might like to add new features and some of those might require chaning data schemes, for instance we'd like to add email field to users table. We'd do it as following.

    Make a new migration for adding email column to users table

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

    Now we have a new pair of .up.sql and .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. Adding following content to *_add_email_to_users.*.sql files

    000002_add_email_to_users.up.sql

    ALTER TABLE `users` ADD COLUMN `email` VARCHAR(255) UNIQUE;
    

    000002_add_email_to_users.down.sql

    ALTER TABLE `users` DROP COLUMN `email`;
    

    Run the up migration command again to make update to the data schemas

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

    We'll also need to update the golang users struct (adding Email field) to keep it in sync with the new schemas..

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

9. Rolling Back Migrations:

In case for some reasons we got bugs with new updated schemas, and we need to rollback, this case we'll use the down command:

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

Number 1 indicates that we want to rollback 1 migration.

Here we also need manually to update golang users struct (remove the Email field) to reflect the data schema changes.

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

10. Use with Makefile

To simplify the process of migration and rolling back, we can add a Makefile .

/golang-service
|-- ...
|-- Makefile

The content of Makefile as following.

include .env

create_migration:
    migrate create -ext=sql -dir=./migrations -seq create_new_migration

migrate_up:
    migrate -path=./migrations -database "mysql://${DB_USER}:${DB_PASS}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME}" -verbose up 1

migrate_down:
    migrate -path=./migrations -database "mysql://${DB_USER}:${DB_PASS}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME}" -verbose down 1

.PHONY:  create_migration migrate_up migrate_down

Now we can simply run make migrate_up or make migrate_down on CLI to do the migration and the rollback.

11.Considerations:

  • Data Loss During Rollback: Rolling back migrations that delete columns or tables may result in data loss, so always backup data before running a rollback.
  • CI/CD Integration: Integrate the migration process into your CI/CD pipeline to automate schema changes during deployment.
  • DB Backups: Schedule regular database backups to prevent data loss in case of migration errors.

About DB backups

Before rolling back a migration or making changes that could potentially affect your database, here are some key points to consider.

  1. Schema Changes: If the migration involved altering the schema (e.g., adding or removing columns, changing data types), rolling back to a previous migration can result in the loss of any data stored in those altered columns or tables.
  2. Data Removal: If the migration includes commands that delete data (like dropping tables or truncating tables), rolling back will execute the corresponding "down" migration, which could permanently remove that data.
  3. Transaction Handling: If your migration tool supports transactions, the rollback might be safer since changes are applied in a transaction. However, if you manually run SQL commands outside of transactions, there is a risk of losing data.
  4. Data Integrity: If you have modified data in a way that depends on the current schema, rolling back could leave your database in an inconsistent state.

So it’s crucial to back up your data. Here’s a brief guide:

  1. Database Dump:
    Use database-specific tools to create a full backup of your database. For MySQL, you can use:

     mysqldump -u root -p dbname > backup_before_rollback.sql
    

    This creates a file (backup_before_rollback.sql) that contains all the data and schema of the dbname database.

  2. Export Specific Tables:
    If you only need to back up certain tables, specify them in the mysqldump command:

    mysqldump -u root -p golang_1 users > users_table_backup.sql
    
  3. Verify the Backup:
    Ensure that the backup file has been created and check its size or open it to ensure it contains the necessary data.

  4. Store Backups Securely:
    Keep a copy of the backup in a secure location, such as cloud storage or a separate server, to prevent data loss during the rollback process.

Backups on cloud

To back up your MySQL data when using Golang and deploying on AWS EKS, you can follow these steps:

  1. Use mysqldump for Database Backup:
    Create a mysqldump of your MySQL database using a Kubernetes cron job.

    mysqldump -h  -u  -p > backup.sql
    

    Store this in a persistent volume or an S3 bucket.

  2. Automate with Kubernetes CronJob:
    Use a Kubernetes CronJob to automate the mysqldump process.
    Example YAML configuration:yaml

    apiVersion: batch/v1
    kind: CronJob
    metadata:
    name: mysql-backup
    spec:
       schedule: "0 2 * * *" # Runs every day at 2 AM
       jobTemplate:
         spec:
           template:
             spec:
               containers:
                 - name: mysql-backup
                   image: mysql:5.7
                   args:
                     - /bin/sh
                     - -c
                     - "mysqldump -h  -u  -p | aws s3 cp - s3:///backup-$(date  \\%F).sql"
                   env:
                     - name: AWS_ACCESS_KEY_ID
                       value: ""
                     - name: AWS_SECRET_ACCESS_KEY
                       value: ""
               restartPolicy: OnFailure
    


    `

  3. Using AWS RDS Automated Backups (if using RDS):
    If your MySQL database is on AWS RDS, you can leverage RDS automated backups and snapshots.
    Set a backup retention period and take snapshots manually or automate snapshots using Lambda functions.

  4. Back Up Persistent Volumes (PV) with Velero:
    Use Velero, a backup tool for Kubernetes, to back up the persistent volume that holds MySQL data.
    Install Velero on your EKS cluster and configure it to back up to S3.

By using these methods, you can ensure your MySQL data is regularly backed up and securely stored.

版本声明 本文转载于:https://dev.to/truongpx396/db-migration-for-golang-services-why-it-matters-17a?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 如何使用 MySQL 查找今天生日的用户?
    如何使用 MySQL 查找今天生日的用户?
    如何使用 MySQL 识别今天生日的用户使用 MySQL 确定今天是否是用户的生日涉及查找生日匹配的所有行今天的日期。这可以通过一个简单的 MySQL 查询来实现,该查询将存储为 UNIX 时间戳的生日与今天的日期进行比较。以下 SQL 查询将获取今天有生日的所有用户: FROM USERS ...
    编程 发布于2024-11-17
  • 为什么我在 Django 数据库查询中收到“InterfaceError (0, \'\')\”?
    为什么我在 Django 数据库查询中收到“InterfaceError (0, \'\')\”?
    在 Django 查询执行中遇到 InterfaceError (0, '')Django 用户可能会遇到持久的“InterfaceError (0, '')” " 尝试数据库操作时出错,特别是在服务器重新启动后。此错误源于使用全局游标。根本原因:全局游标,...
    编程 发布于2024-11-17
  • Bootstrap 4 Beta 中的列偏移发生了什么?
    Bootstrap 4 Beta 中的列偏移发生了什么?
    Bootstrap 4 Beta:列偏移的删除和恢复Bootstrap 4 在其 Beta 1 版本中引入了重大更改柱子偏移了。然而,随着 Beta 2 的后续发布,这些变化已经逆转。从 offset-md-* 到 ml-auto在 Bootstrap 4 Beta 1 中, offset-md-*...
    编程 发布于2024-11-17
  • 除了“if”语句之外:还有哪些地方可以在不进行强制转换的情况下使用具有显式“bool”转换的类型?
    除了“if”语句之外:还有哪些地方可以在不进行强制转换的情况下使用具有显式“bool”转换的类型?
    无需强制转换即可上下文转换为 bool您的类定义了对 bool 的显式转换,使您能够在条件语句中直接使用其实例“t”。然而,这种显式转换提出了一个问题:“t”在哪里可以在不进行强制转换的情况下用作 bool?上下文转换场景C 标准指定了四种值可以根据上下文转换为 bool 的主要场景:语句:if、w...
    编程 发布于2024-11-17
  • MySQL 中的分片真的是大型数据集的最佳方法吗?
    MySQL 中的分片真的是大型数据集的最佳方法吗?
    MySQL 中的分片:一种关键方法在优化 MySQL 数据库时,分片作为处理大型数据集的潜在解决方案而出现。然而,在实施分片之前了解与分片相关的权衡和潜在陷阱至关重要。最佳方法:不分片与普遍看法相反,除非绝对必要,否则对 MySQL 表进行分片的最佳方法是避免这样做。为什么?分片带来了重大的技术挑战...
    编程 发布于2024-11-17
  • 如何在 MySQL SELECT 查询中使用 IF 语句:语法和最佳实践指南
    如何在 MySQL SELECT 查询中使用 IF 语句:语法和最佳实践指南
    MySQL SELECT 查询中的 IF 语句用法尝试在 MySQL SELECT 查询中实现 IF 语句时,您可能会遇到困难,因为具体语法要求。在 MySQL 中,传统的 IF/THEN/ELSE 结构仅在存储过程和函数中可用。要解决此问题,重构查询至关重要。查询中支持的 IF() 函数主要用于根...
    编程 发布于2024-11-17
  • 如何在Python中生成特定范围内的唯一随机数?
    如何在Python中生成特定范围内的唯一随机数?
    生成范围内的唯一随机数生成随机数时,确保每个数字都是唯一的可能是一个挑战。虽然可以使用条件语句来检查重复项,但在处理大范围或大数字时,这种方法会变得很麻烦。生成唯一随机数列表的一种简单方法是使用 Python 的 random.sample( ) 功能。该函数有两个参数:总体(生成随机数的数字范围)...
    编程 发布于2024-11-17
  • 如何使用占位符和参数执行带有“WHERE...IN”的 PDO 查询?
    如何使用占位符和参数执行带有“WHERE...IN”的 PDO 查询?
    PDO 查询“WHERE...IN”为了使用 PDO 增强数据库访问,许多开发人员遇到了挑战,特别是“WHERE...IN” IN”查询。让我们深入研究其中的复杂性,并发现在 PDO 准备好的语句中使用项目列表的正确方法。“WHERE... IN”难题考虑一个场景,您需要根据表单中已检查项目的列表从...
    编程 发布于2024-11-17
  • 如何高效地访问和删除Python字典中的任意元素?
    如何高效地访问和删除Python字典中的任意元素?
    访问 Python 字典中的任意元素在 Python 中,字典将键值对存储在无序集合中。如果字典不为空,则可以使用以下语法访问任意(随机)元素:mydict[list(mydict.keys())[0]]但是,这这种方法可能冗长且低效,特别是当您需要执行多次迭代时。让我们探索更有效的方法来实现此目的...
    编程 发布于2024-11-17
  • 如何使用 jQuery 制作背景颜色动画?
    如何使用 jQuery 制作背景颜色动画?
    使用 jQuery 淡化背景颜色引人注目的网站元素通常需要微妙的动画,例如淡入和淡出。虽然 jQuery 广泛用于动画文本内容,但它也可用于动态增强背景颜色。在 jQuery 中淡入/淡出背景颜色进行操作要使用 jQuery 设置元素的背景颜色,您首先需要合并 jQueryUI 库。集成后,可以使用...
    编程 发布于2024-11-17
  • 如何修复 macOS 上 Django 中的“配置不正确:加载 MySQLdb 模块时出错”?
    如何修复 macOS 上 Django 中的“配置不正确:加载 MySQLdb 模块时出错”?
    MySQL配置不正确:相对路径的问题在Django中运行python manage.py runserver时,可能会遇到以下错误:ImproperlyConfigured: Error loading MySQLdb module: dlopen(/Library/Python/2.7/site-...
    编程 发布于2024-11-17
  • 如何在 PHP 中组合两个关联数组,同时保留唯一 ID 并处理重复名称?
    如何在 PHP 中组合两个关联数组,同时保留唯一 ID 并处理重复名称?
    在 PHP 中组合关联数组在 PHP 中,将两个关联数组组合成一个数组是一项常见任务。考虑以下请求:问题描述:提供的代码定义了两个关联数组,$array1 和 $array2。目标是创建一个新数组 $array3,它合并两个数组中的所有键值对。 此外,提供的数组具有唯一的 ID,而名称可能重合。要求...
    编程 发布于2024-11-17
  • 在 Go 中嵌入结构:指针还是值?何时使用哪个?
    在 Go 中嵌入结构:指针还是值?何时使用哪个?
    Go 中嵌入结构:何时使用指针当考虑将一个结构嵌入另一个结构时,决定是否使用指针或者出现嵌入字段的值。本文探讨了这种实现选择的细微差别,并提供示例来说明潜在的好处和影响。通过指针嵌入Go 规范允许将结构体嵌入为指针或价值观。对于非接口类型,允许将匿名字段指定为类型名称 T 或指向非接口类型名称 *T...
    编程 发布于2024-11-17
  • PHP 中可以不继承地修改类方法吗?
    PHP 中可以不继承地修改类方法吗?
    我可以在没有继承的情况下对类进行猴子修补吗?您可能会遇到需要修改类或其方法而没有典型继承选项的情况。以下面的类为例:class third_party_library { function buggy_function() { return 'bad result'; ...
    编程 发布于2024-11-17
  • 如何使用纯 JavaScript 自动调整文本区域大小?
    如何使用纯 JavaScript 自动调整文本区域大小?
    Textarea 自动高度这个问题旨在消除文本区域的滚动条并调整其高度以匹配其中的内容。提供了使用纯JavaScript代码的解决方案:function auto_grow(element) { element.style.height = "5px"; element....
    编程 发布于2024-11-17

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3