Home Backend Development Golang How to use migrations with Golang

How to use migrations with Golang

Nov 09, 2024 am 07:28 AM

Simple sample application showing how to use golang-migrate

Why you should use migrations?

Many people ask this question and I have tried to make this list to highlight the main advantages about the use of migrations:

Version Control: One of the main and most important is to be able to have a versioning of the different modifications of the database schema. Without the migrations, these schema changes would be incoherent and impossible to track, which would cause versioning problems and possible errors.

Rollback: It's always necessary to have a rollback system in case of any failure. A migrations system always has two methods up to apply the changes in the database and down in charge of reverting the changes quickly and consistently :-)

Automation and CI/CD Integration: Migrations can be automated, allowing them to be part of the CI/CD pipeline. This helps in deploying changes smoothly and consistently without manual intervention.

We can find many more advantages but I think these points represent a good summary of the main advantages.

How to implement migrations in Golang?

Go doesn't support migrations natively for that propuso we can use the popular golang-migrate package also if you use an ORM like GORM you can use it for that.

Both packages are very popular but in this example I'll use golang-migrate because I am not interested in implementing an ORM.

Show me the code!

Let's see step by step how to implement a simple application to see how it is used.

To follow this article you'll need: Go and Docker with Docker Compose

Infrastructure

Create the file docker-compose.yml in your root directory where we'll define your favourite DB, in my case use a MariaDB but feel free to use another one.

services:
  mariadb:
    image: mariadb:11.5.2
    container_name: mariadb_example_go_migration
    ports:
      - "3306:3306"
    environment:
      - MYSQL_DATABASE=app
      - MYSQL_ROOT_PASSWORD=root
      - TZ=Europe/Berlin
    volumes:
      - mariadbdata:/var/lib/mysql

volumes:
  mariadbdata:
    driver: local

docker compose up -d 
Copy after login
Copy after login
Copy after login

If you prefer you can use Docker directly instead of docker-compose:

docker volume create -d local mariadbdata
docker run --name mariadb_example_go_migration -p 3306:3306 -e MYSQL_DATABASE=app -e MYSQL_ROOT_PASSWORD=root -e TZ=Europe/Berlin -v mariadbdata:/var/lib/mysql mariadb:11.5.2
Copy after login
Copy after login
Copy after login

Environment values

Create or update the file .env in your root directory where you need to define the variables to connect our data base.

DATABASE_DSN=root:root@tcp(localhost:3306)/app
Copy after login
Copy after login
Copy after login

Create a simple golang app

Create a simple golang application to ensure successful DB connection and list all tables and estructure in the database with their structure. cmd/main.go

package main

import (
 "database/sql"
 "fmt"
 "log"
 "os"
 "text/tabwriter"

 _ "github.com/go-sql-driver/mysql"
 "github.com/joho/godotenv"
)

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

 // Open connection with MySQL DB
 db, err := sql.Open("mysql", os.Getenv("DATABASE_DSN"))
 if err != nil {
  log.Fatalf("Error opening database: %v\n", err)
 }
 defer db.Close()

 // Ensure that the connection works
 err = db.Ping()
 if err != nil {
  log.Fatalf("Error connecting database: %v\n", err)
 }

 fmt.Println("Connected to database")

 // Execute the SHOW TABLES query to list all tables in the database
 tables, err := db.Query("SHOW TABLES")
 if err != nil {
  log.Fatalf("Failed to execute SHOW TABLES query: %v\n", err)
 }
 defer tables.Close()

 fmt.Println("Database structure:")

 for tables.Next() {
  var tableName string
  if err := tables.Scan(&tableName); err != nil {
   log.Fatalf("Failed to scan table name: %v\n", err)
  }

  w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)

  fmt.Printf("\n[Table: %s]\n\n", tableName)
  fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t\n", "Field", "Type", "Null", "Key", "Default", "Extra")

  // Get the structure of the current table
  structureQuery := fmt.Sprintf("DESCRIBE %s", tableName)
  columns, err := db.Query(structureQuery)
  if err != nil {
   log.Fatalf("Failed to describe table %s: %v\n", tableName, err)
  }
  defer columns.Close()

  for columns.Next() {
   var field, colType, null, key, defaultVal, extra sql.NullString
   err := columns.Scan(&field, &colType, &null, &key, &defaultVal, &extra)
   if err != nil {
    log.Fatalf("Failed to scan column: %v\n", err)
   }

   fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t\n",
    field.String, colType.String, null.String, key.String, defaultVal.String, extra.String)
  }

  w.Flush()
 }
}
Copy after login
Copy after login
Copy after login

And when we run it we have a similar output:

How to use migrations with Golang

Migrate CLI

To run golang-migrate CLI basically you have two methods install CLI locally or run through oficial Docker image: migrate/migrate.

Personally I prefer de docker variant but in this tutorial illustrate both variants.

How to generate migration

The first step is create an empty migration with the next command.

services:
  mariadb:
    image: mariadb:11.5.2
    container_name: mariadb_example_go_migration
    ports:
      - "3306:3306"
    environment:
      - MYSQL_DATABASE=app
      - MYSQL_ROOT_PASSWORD=root
      - TZ=Europe/Berlin
    volumes:
      - mariadbdata:/var/lib/mysql

volumes:
  mariadbdata:
    driver: local

docker compose up -d 
Copy after login
Copy after login
Copy after login
docker volume create -d local mariadbdata
docker run --name mariadb_example_go_migration -p 3306:3306 -e MYSQL_DATABASE=app -e MYSQL_ROOT_PASSWORD=root -e TZ=Europe/Berlin -v mariadbdata:/var/lib/mysql mariadb:11.5.2
Copy after login
Copy after login
Copy after login
  • ext: Extension of the file to be generated.
  • dir: Directory where our migration will be created.
  • seq: Migration sequence name.

This command will be generated two empty files on database/migrations/ folder: 000001createuserstable.up.sql and 000001createuserstable.down.sql

On 000001createuserstable.up.sql file define SQL for create a table users:

DATABASE_DSN=root:root@tcp(localhost:3306)/app
Copy after login
Copy after login
Copy after login

On 000001createuserstable.down.sql file define SQL to revert all changes made by up, in this case we have to delete users table:

package main

import (
 "database/sql"
 "fmt"
 "log"
 "os"
 "text/tabwriter"

 _ "github.com/go-sql-driver/mysql"
 "github.com/joho/godotenv"
)

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

 // Open connection with MySQL DB
 db, err := sql.Open("mysql", os.Getenv("DATABASE_DSN"))
 if err != nil {
  log.Fatalf("Error opening database: %v\n", err)
 }
 defer db.Close()

 // Ensure that the connection works
 err = db.Ping()
 if err != nil {
  log.Fatalf("Error connecting database: %v\n", err)
 }

 fmt.Println("Connected to database")

 // Execute the SHOW TABLES query to list all tables in the database
 tables, err := db.Query("SHOW TABLES")
 if err != nil {
  log.Fatalf("Failed to execute SHOW TABLES query: %v\n", err)
 }
 defer tables.Close()

 fmt.Println("Database structure:")

 for tables.Next() {
  var tableName string
  if err := tables.Scan(&tableName); err != nil {
   log.Fatalf("Failed to scan table name: %v\n", err)
  }

  w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)

  fmt.Printf("\n[Table: %s]\n\n", tableName)
  fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t\n", "Field", "Type", "Null", "Key", "Default", "Extra")

  // Get the structure of the current table
  structureQuery := fmt.Sprintf("DESCRIBE %s", tableName)
  columns, err := db.Query(structureQuery)
  if err != nil {
   log.Fatalf("Failed to describe table %s: %v\n", tableName, err)
  }
  defer columns.Close()

  for columns.Next() {
   var field, colType, null, key, defaultVal, extra sql.NullString
   err := columns.Scan(&field, &colType, &null, &key, &defaultVal, &extra)
   if err != nil {
    log.Fatalf("Failed to scan column: %v\n", err)
   }

   fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t\n",
    field.String, colType.String, null.String, key.String, defaultVal.String, extra.String)
  }

  w.Flush()
 }
}
Copy after login
Copy after login
Copy after login

How to apply migration

The following command applies all pending migrations. You can also define the number of migrations to apply by adding the number after the up.

#CLI variant
migrate create -ext sql -dir ./database/migrations -seq create_users_table
Copy after login
Copy after login
#Docker CLI variant
docker run --rm -v $(pwd)/database/migrations:/migrations migrate/migrate \
    create -ext sql -dir /migrations -seq create_users_table
Copy after login
  • path: Path to migrations directory.
  • database: Define you database DSN connection.

NOTE : When running the migration for the first time a table "schema_migrations" will be created in which the migration knows the version number applied.

And run our Golang application to display the results:

How to use migrations with Golang

Adding new migration

Add a new column phone on users table

CREATE TABLE `users` (
    `id` VARCHAR(36) NOT NULL PRIMARY KEY,
    `name` VARCHAR(255) NOT NULL,
    `email` VARCHAR(255) NOT NULL UNIQUE,
    `password` VARCHAR(255) NOT NULL
);
Copy after login
DROP TABLE IF EXISTS `users`;
Copy after login
#CLI variant
migrate -path=./database/migrations -database "mysql://root:root@tcp(localhost:3306)/app" up
Copy after login
#Docker CLI variant
docker run --rm -v $(pwd)/database/migrations:/migrations --network host migrate/migrate \
    -path=/migrations -database "mysql://root:root@tcp(localhost:3306)/app" up
Copy after login

And when you run it from our Golang application you can see the new field:

How to use migrations with Golang

How to revert migration

With the following command we can easily rollback the applied. migrations. In the following example we can see how we reverse the last migration applied:

#CLI variant
migrate create -ext sql -dir ./database/migrations -seq add_column_phone

#Docker CLI variant
docker run --rm -v $(pwd)/database/migrations:/migrations migrate/migrate \
    create -ext sql -dir /migrations -seq add_column_phone
Copy after login
-- 000002_add_column_phone.up.sql
ALTER TABLE `users` ADD `phone` VARCHAR(255) NULL;
Copy after login

WARNING : If you don't define the number of migrations, ROLLBACK will be applied to ALL MIGRATIONS!

And then we can show that the last migration has been reverted and the phone field has been removed :-)

Display table users without phone field

How to Resolve Migration Errors

If a migration contains errors and is executed, that migration cannot be applied and the migration system will prevent any further migrations on the database until this migration is fixed.

And when trying to apply we will get a message like this:

services:
  mariadb:
    image: mariadb:11.5.2
    container_name: mariadb_example_go_migration
    ports:
      - "3306:3306"
    environment:
      - MYSQL_DATABASE=app
      - MYSQL_ROOT_PASSWORD=root
      - TZ=Europe/Berlin
    volumes:
      - mariadbdata:/var/lib/mysql

volumes:
  mariadbdata:
    driver: local

docker compose up -d 
Copy after login
Copy after login
Copy after login

Don't panic, it is not difficult to get back to a consistent system.

First we have to resolve the corrupted migration, in this case version 2.

Once the migration is solved we have to force the system to the last valid version, in this case version 1.

docker volume create -d local mariadbdata
docker run --name mariadb_example_go_migration -p 3306:3306 -e MYSQL_DATABASE=app -e MYSQL_ROOT_PASSWORD=root -e TZ=Europe/Berlin -v mariadbdata:/var/lib/mysql mariadb:11.5.2
Copy after login
Copy after login
Copy after login
DATABASE_DSN=root:root@tcp(localhost:3306)/app
Copy after login
Copy after login
Copy after login

And now you can reapply the migrations without any problem ;-)

Makefile

To improve our productivity and facilitate the use of these commands we can use Makefile. Below you can see the two variants: native client and docker.

CLI variant

package main

import (
 "database/sql"
 "fmt"
 "log"
 "os"
 "text/tabwriter"

 _ "github.com/go-sql-driver/mysql"
 "github.com/joho/godotenv"
)

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

 // Open connection with MySQL DB
 db, err := sql.Open("mysql", os.Getenv("DATABASE_DSN"))
 if err != nil {
  log.Fatalf("Error opening database: %v\n", err)
 }
 defer db.Close()

 // Ensure that the connection works
 err = db.Ping()
 if err != nil {
  log.Fatalf("Error connecting database: %v\n", err)
 }

 fmt.Println("Connected to database")

 // Execute the SHOW TABLES query to list all tables in the database
 tables, err := db.Query("SHOW TABLES")
 if err != nil {
  log.Fatalf("Failed to execute SHOW TABLES query: %v\n", err)
 }
 defer tables.Close()

 fmt.Println("Database structure:")

 for tables.Next() {
  var tableName string
  if err := tables.Scan(&tableName); err != nil {
   log.Fatalf("Failed to scan table name: %v\n", err)
  }

  w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)

  fmt.Printf("\n[Table: %s]\n\n", tableName)
  fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t\n", "Field", "Type", "Null", "Key", "Default", "Extra")

  // Get the structure of the current table
  structureQuery := fmt.Sprintf("DESCRIBE %s", tableName)
  columns, err := db.Query(structureQuery)
  if err != nil {
   log.Fatalf("Failed to describe table %s: %v\n", tableName, err)
  }
  defer columns.Close()

  for columns.Next() {
   var field, colType, null, key, defaultVal, extra sql.NullString
   err := columns.Scan(&field, &colType, &null, &key, &defaultVal, &extra)
   if err != nil {
    log.Fatalf("Failed to scan column: %v\n", err)
   }

   fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t\n",
    field.String, colType.String, null.String, key.String, defaultVal.String, extra.String)
  }

  w.Flush()
 }
}
Copy after login
Copy after login
Copy after login

Docker CLI variant

#CLI variant
migrate create -ext sql -dir ./database/migrations -seq create_users_table
Copy after login
Copy after login

Repository

The code for this tutorial can be found in the public: GitHub - albertcolom/example-go-migration


Original published at: albertcolom.com

The above is the detailed content of How to use migrations with Golang. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Hot Topics

Java Tutorial
1664
14
PHP Tutorial
1266
29
C# Tutorial
1239
24
Golang's Purpose: Building Efficient and Scalable Systems Golang's Purpose: Building Efficient and Scalable Systems Apr 09, 2025 pm 05:17 PM

Go language performs well in building efficient and scalable systems. Its advantages include: 1. High performance: compiled into machine code, fast running speed; 2. Concurrent programming: simplify multitasking through goroutines and channels; 3. Simplicity: concise syntax, reducing learning and maintenance costs; 4. Cross-platform: supports cross-platform compilation, easy deployment.

Golang vs. Python: Performance and Scalability Golang vs. Python: Performance and Scalability Apr 19, 2025 am 12:18 AM

Golang is better than Python in terms of performance and scalability. 1) Golang's compilation-type characteristics and efficient concurrency model make it perform well in high concurrency scenarios. 2) Python, as an interpreted language, executes slowly, but can optimize performance through tools such as Cython.

Golang and C  : Concurrency vs. Raw Speed Golang and C : Concurrency vs. Raw Speed Apr 21, 2025 am 12:16 AM

Golang is better than C in concurrency, while C is better than Golang in raw speed. 1) Golang achieves efficient concurrency through goroutine and channel, which is suitable for handling a large number of concurrent tasks. 2)C Through compiler optimization and standard library, it provides high performance close to hardware, suitable for applications that require extreme optimization.

Golang's Impact: Speed, Efficiency, and Simplicity Golang's Impact: Speed, Efficiency, and Simplicity Apr 14, 2025 am 12:11 AM

Goimpactsdevelopmentpositivelythroughspeed,efficiency,andsimplicity.1)Speed:Gocompilesquicklyandrunsefficiently,idealforlargeprojects.2)Efficiency:Itscomprehensivestandardlibraryreducesexternaldependencies,enhancingdevelopmentefficiency.3)Simplicity:

Golang vs. Python: Key Differences and Similarities Golang vs. Python: Key Differences and Similarities Apr 17, 2025 am 12:15 AM

Golang and Python each have their own advantages: Golang is suitable for high performance and concurrent programming, while Python is suitable for data science and web development. Golang is known for its concurrency model and efficient performance, while Python is known for its concise syntax and rich library ecosystem.

Golang and C  : The Trade-offs in Performance Golang and C : The Trade-offs in Performance Apr 17, 2025 am 12:18 AM

The performance differences between Golang and C are mainly reflected in memory management, compilation optimization and runtime efficiency. 1) Golang's garbage collection mechanism is convenient but may affect performance, 2) C's manual memory management and compiler optimization are more efficient in recursive computing.

The Performance Race: Golang vs. C The Performance Race: Golang vs. C Apr 16, 2025 am 12:07 AM

Golang and C each have their own advantages in performance competitions: 1) Golang is suitable for high concurrency and rapid development, and 2) C provides higher performance and fine-grained control. The selection should be based on project requirements and team technology stack.

Golang vs. C  : Performance and Speed Comparison Golang vs. C : Performance and Speed Comparison Apr 21, 2025 am 12:13 AM

Golang is suitable for rapid development and concurrent scenarios, and C is suitable for scenarios where extreme performance and low-level control are required. 1) Golang improves performance through garbage collection and concurrency mechanisms, and is suitable for high-concurrency Web service development. 2) C achieves the ultimate performance through manual memory management and compiler optimization, and is suitable for embedded system development.

See all articles